From bda96d54d701b904aff4ddadc712ff2fb16e36ec Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:09:44 -0600 Subject: [PATCH 01/34] copy .gov branch --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/python-app.yaml | 10 +- README.md | 12 +- cases.csv | 2 +- createmodel.gms | 5 - docs/source/developer_best_practices.md | 10 +- docs/source/model_documentation.md | 6 +- docs/source/reeds_training_homework.md | 4 +- docs/source/setup.md | 2 +- docs/sources.csv | 805 ++ docs/sources_documentation.md | 4008 ++++++++++ helpers/interim_report.py | 60 + helpers/interim_report_batch.py | 70 + helpers/restart_runs.py | 180 + helpers/runstatus.py | 187 + hourlize/.pylintrc | 473 ++ hourlize/README.md | 2 +- hourlize/pyproject.toml | 20 + hourlize/requirements_dev.txt | 3 + hourlize/resource.py | 2 +- inputs/national_generation/README.md | 2 +- inputs/transmission/README.md | 2 +- inputs/zones/README.md | 2 +- postprocessing/.pre-commit-config.yaml | 30 + .../air_quality/health_damage_calculations.py | 2 +- .../bokehpivot/in/reeds2/process_style.csv | 38 +- postprocessing/cleanup_files.py | 4 +- postprocessing/input_plots.py | 2 +- postprocessing/raw_value_streams.py | 253 + .../ITC-PTC_expenditures.py | 4 +- .../retail_rate_calculations.py | 2 +- postprocessing/run_pcm.py | 391 + postprocessing/run_reeds2pras.py | 6 +- postprocessing/valuestreams.py | 85 + preprocessing/casemaker.py | 4 +- reeds/__init__.py | 7 +- reeds/core/setup/a_createmodel.gms | 10 + reeds/core/setup/b_inputs.gms | 6773 +++++++++++++++++ reeds/core/setup/c_model.gms | 3976 ++++++++++ reeds/core/setup/d_mga.gms | 77 + reeds/core/setup/d_objective.gms | 369 + reeds/core/setup/e_solveprep.gms | 254 + reeds/core/solve/1_tc_phaseout.py | 229 + reeds/core/solve/2_financials.gms | 156 + reeds/core/solve/2_temporal_params.gms | 910 +++ reeds/core/solve/3_solve_allyears.gms | 165 + reeds/core/solve/3_solve_oneyear.gms | 290 + reeds/core/solve/3_solve_window.gms | 154 + reeds/core/solve/4_post_solve_adjustments.gms | 131 + reeds/core/solve/5_varfix.gms | 125 + reeds/core/solve/6_data_dump.gms | 424 ++ reeds/core/solve/solve.py | 177 + reeds/core/solve_pcm/solve_pcm.gms | 25 + reeds/core/solve_pcm/unfix_op.gms | 115 + reeds/core/terminus/dump_alldata.gms | 4 + reeds/core/terminus/powfrac_calc.gms | 126 + reeds/core/terminus/report.gms | 2091 +++++ reeds/core/terminus/report_dump.py | 311 + reeds/core/terminus/report_params.csv | 276 + reeds/hpc/aws_setup.sh | 135 + reeds/hpc/srun_template.sh | 9 + reeds/input_processing/WriteHintage.py | 775 ++ reeds/input_processing/__init__.py | 0 reeds/input_processing/aggregate_regions.py | 1183 +++ .../input_processing/calc_financial_inputs.py | 527 ++ reeds/input_processing/check_inputs.py | 156 + reeds/input_processing/climateprep.py | 222 + reeds/input_processing/copy_files.py | 1683 ++++ reeds/input_processing/forecast.py | 526 ++ reeds/input_processing/fuelcostprep.py | 176 + reeds/input_processing/h2_storage.py | 142 + reeds/input_processing/hourly_load.py | 783 ++ reeds/input_processing/hourly_plots.py | 719 ++ reeds/input_processing/hourly_repperiods.py | 977 +++ .../hourly_writetimeseries.py | 1584 ++++ reeds/input_processing/hydcf.py | 470 ++ reeds/input_processing/mcs_sampler.py | 1649 ++++ reeds/input_processing/outage_rates.py | 507 ++ reeds/input_processing/plantcostprep.py | 505 ++ reeds/input_processing/recf.py | 509 ++ reeds/input_processing/runfiles.csv | 493 ++ reeds/input_processing/transmission.py | 641 ++ reeds/input_processing/writecapdat.py | 897 +++ reeds/input_processing/writedrshift.py | 87 + reeds/input_processing/writesupplycurves.py | 1139 +++ reeds/io.py | 6 +- reeds/parse.py | 840 ++ reeds/prasplots.py | 2 +- reeds/reedsplots.py | 6 +- reeds/resource_adequacy/Augur.py | 224 + reeds/resource_adequacy/__init__.py | 5 + reeds/resource_adequacy/augur_switches.csv | 16 + reeds/resource_adequacy/capacity_credit.py | 862 +++ reeds/resource_adequacy/diagnostic_plots.py | 1359 ++++ reeds/resource_adequacy/prep_data.py | 588 ++ reeds/resource_adequacy/ra.py | 125 + .../resource_adequacy/reeds2pras/Project.toml | 26 + .../reeds2pras/R2P_Test_Summary.png | 3 + reeds/resource_adequacy/reeds2pras/README.md | 106 + .../reeds2pras/src/ReEDS2PRAS.jl | 40 + .../reeds2pras/src/main/create_pras_system.jl | 206 + .../reeds2pras/src/main/parse_reeds_data.jl | 133 + .../reeds2pras/src/models/Battery.jl | 130 + .../reeds2pras/src/models/Gen_Storage.jl | 166 + .../reeds2pras/src/models/Generator.jl | 46 + .../reeds2pras/src/models/Interface.jl | 150 + .../reeds2pras/src/models/Line.jl | 155 + .../reeds2pras/src/models/Region.jl | 45 + .../reeds2pras/src/models/Storage.jl | 96 + .../reeds2pras/src/models/Thermal_Gen.jl | 82 + .../reeds2pras/src/models/Variable_Gen.jl | 109 + .../reeds2pras/src/models/utils.jl | 371 + .../reeds2pras/src/reeds_to_pras.jl | 73 + .../src/utils/reeds_data_parsing.jl | 1186 +++ .../src/utils/reeds_input_parsing.jl | 497 ++ .../reeds2pras/src/utils/runchecks.jl | 47 + .../reeds2pras/test/Project.toml | 8 + .../reeds2pras/test/runtests.jl | 38 + .../reeds2pras/test/test-ReEDS2PRAS.jl | 71 + .../reeds2pras/test/test-benchmark.jl | 35 + .../reeds2pras/test/utils.jl | 393 + reeds/resource_adequacy/run_pras.jl | 486 ++ reeds/resource_adequacy/stress_periods.py | 615 ++ reeds/solver/cbc.opt | 3 + reeds/solver/cplex.op2 | 71 + reeds/solver/cplex.opt | 71 + reeds/solver/gurobi.opt | 4 + run.py | 1811 +++++ tests/objective_function_params.yaml | 2 +- 129 files changed, 51592 insertions(+), 75 deletions(-) delete mode 100644 createmodel.gms create mode 100644 docs/sources.csv create mode 100644 docs/sources_documentation.md create mode 100644 helpers/interim_report.py create mode 100644 helpers/interim_report_batch.py create mode 100644 helpers/restart_runs.py create mode 100644 helpers/runstatus.py create mode 100644 hourlize/.pylintrc create mode 100644 hourlize/pyproject.toml create mode 100644 hourlize/requirements_dev.txt create mode 100644 postprocessing/.pre-commit-config.yaml create mode 100644 postprocessing/raw_value_streams.py create mode 100644 postprocessing/run_pcm.py create mode 100644 postprocessing/valuestreams.py create mode 100644 reeds/core/setup/a_createmodel.gms create mode 100644 reeds/core/setup/b_inputs.gms create mode 100644 reeds/core/setup/c_model.gms create mode 100644 reeds/core/setup/d_mga.gms create mode 100644 reeds/core/setup/d_objective.gms create mode 100644 reeds/core/setup/e_solveprep.gms create mode 100644 reeds/core/solve/1_tc_phaseout.py create mode 100644 reeds/core/solve/2_financials.gms create mode 100644 reeds/core/solve/2_temporal_params.gms create mode 100644 reeds/core/solve/3_solve_allyears.gms create mode 100644 reeds/core/solve/3_solve_oneyear.gms create mode 100644 reeds/core/solve/3_solve_window.gms create mode 100644 reeds/core/solve/4_post_solve_adjustments.gms create mode 100644 reeds/core/solve/5_varfix.gms create mode 100644 reeds/core/solve/6_data_dump.gms create mode 100644 reeds/core/solve/solve.py create mode 100644 reeds/core/solve_pcm/solve_pcm.gms create mode 100644 reeds/core/solve_pcm/unfix_op.gms create mode 100644 reeds/core/terminus/dump_alldata.gms create mode 100644 reeds/core/terminus/powfrac_calc.gms create mode 100644 reeds/core/terminus/report.gms create mode 100644 reeds/core/terminus/report_dump.py create mode 100644 reeds/core/terminus/report_params.csv create mode 100644 reeds/hpc/aws_setup.sh create mode 100644 reeds/hpc/srun_template.sh create mode 100644 reeds/input_processing/WriteHintage.py create mode 100644 reeds/input_processing/__init__.py create mode 100644 reeds/input_processing/aggregate_regions.py create mode 100644 reeds/input_processing/calc_financial_inputs.py create mode 100644 reeds/input_processing/check_inputs.py create mode 100644 reeds/input_processing/climateprep.py create mode 100644 reeds/input_processing/copy_files.py create mode 100644 reeds/input_processing/forecast.py create mode 100644 reeds/input_processing/fuelcostprep.py create mode 100644 reeds/input_processing/h2_storage.py create mode 100644 reeds/input_processing/hourly_load.py create mode 100644 reeds/input_processing/hourly_plots.py create mode 100644 reeds/input_processing/hourly_repperiods.py create mode 100644 reeds/input_processing/hourly_writetimeseries.py create mode 100644 reeds/input_processing/hydcf.py create mode 100644 reeds/input_processing/mcs_sampler.py create mode 100644 reeds/input_processing/outage_rates.py create mode 100644 reeds/input_processing/plantcostprep.py create mode 100644 reeds/input_processing/recf.py create mode 100644 reeds/input_processing/runfiles.csv create mode 100644 reeds/input_processing/transmission.py create mode 100644 reeds/input_processing/writecapdat.py create mode 100644 reeds/input_processing/writedrshift.py create mode 100644 reeds/input_processing/writesupplycurves.py create mode 100644 reeds/parse.py create mode 100644 reeds/resource_adequacy/Augur.py create mode 100644 reeds/resource_adequacy/__init__.py create mode 100644 reeds/resource_adequacy/augur_switches.csv create mode 100644 reeds/resource_adequacy/capacity_credit.py create mode 100644 reeds/resource_adequacy/diagnostic_plots.py create mode 100644 reeds/resource_adequacy/prep_data.py create mode 100644 reeds/resource_adequacy/ra.py create mode 100644 reeds/resource_adequacy/reeds2pras/Project.toml create mode 100644 reeds/resource_adequacy/reeds2pras/R2P_Test_Summary.png create mode 100644 reeds/resource_adequacy/reeds2pras/README.md create mode 100644 reeds/resource_adequacy/reeds2pras/src/ReEDS2PRAS.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/main/create_pras_system.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/main/parse_reeds_data.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Battery.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Gen_Storage.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Generator.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Interface.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Line.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Region.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Storage.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Thermal_Gen.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/Variable_Gen.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/models/utils.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/reeds_to_pras.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl create mode 100644 reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl create mode 100644 reeds/resource_adequacy/reeds2pras/test/Project.toml create mode 100644 reeds/resource_adequacy/reeds2pras/test/runtests.jl create mode 100644 reeds/resource_adequacy/reeds2pras/test/test-ReEDS2PRAS.jl create mode 100644 reeds/resource_adequacy/reeds2pras/test/test-benchmark.jl create mode 100644 reeds/resource_adequacy/reeds2pras/test/utils.jl create mode 100644 reeds/resource_adequacy/run_pras.jl create mode 100644 reeds/resource_adequacy/stress_periods.py create mode 100644 reeds/solver/cbc.opt create mode 100644 reeds/solver/cplex.op2 create mode 100644 reeds/solver/cplex.opt create mode 100644 reeds/solver/gurobi.opt create mode 100644 run.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b844cf9a..57b5973f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -76,7 +76,7 @@ Include additional illustrative plots describing input data, methods, testing, a - [ ] No large data file(s) added/modified - [ ] No substantive impact on runtime for full-US reference case - [ ] No substantive impact on folder size for full-US reference case -- [ ] No change to process flow (runbatch.py, d_solve_iterate.py) +- [ ] No change to process flow (run.py, d_solve_iterate.py) - [ ] No change to code organization - [ ] No change to package requirements (environment.yml or Project.toml) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 4914c486..b5396f5d 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -29,7 +29,7 @@ env: jobs: julia-setup: name: "Julia environment" - runs-on: ubuntu-latest + runs-on: reeds-runners steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: @@ -50,7 +50,7 @@ jobs: python-setup: name: "Python environment" - runs-on: ubuntu-latest + runs-on: reeds-runners permissions: contents: read actions: write @@ -69,7 +69,7 @@ jobs: name: "Get Zenodo files" needs: - python-setup - runs-on: ubuntu-latest + runs-on: reeds-runners permissions: contents: read actions: write @@ -116,7 +116,7 @@ jobs: run: python .github/scripts/download_test_zenodo_files.py run-ReEDS: - runs-on: ubuntu-latest + runs-on: reeds-runners needs: - julia-setup - python-setup @@ -230,7 +230,7 @@ jobs: env: SCENARIO: ${{ matrix.scenario }} run: | - python runbatch.py -b "$batch" -c test -s "$SCENARIO" + python run.py -b "$batch" -c test -s "$SCENARIO" echo "RUN_FOLDER=$GITHUB_WORKSPACE/runs/${batch}_${SCENARIO}" >> "$GITHUB_ENV" - name: Print GAMS log diff --git a/README.md b/README.md index 919dd5eb..383e55ba 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,11 @@ This GitHub repository contains the source code for NLR's ReEDS model. The ReEDS model source code is available at no cost from the National Laboratory of the Rockies. -The ReEDS model can be downloaded or cloned from [https://github.com/ReEDS-Model/ReEDS](https://github.com/ReEDS-Model/ReEDS). +The ReEDS model can be downloaded or cloned from [https://github.com/NatLabRockies/ReEDS-2.0](https://github.com/NatLabRockies/ReEDS-2.0). -**For more information about the model, see the [open source ReEDS-2.0 Documentation](https://reeds-model.github.io/ReEDS).** +**For more information about the model, see the [ReEDS-2.0 Documentation](https://pages.github.nrel.gov/ReEDS/ReEDS-2.0).** + + ReEDS training videos are available on the [NLR Learning YouTube channel](https://youtube.com/playlist?list=PLmIn8Hncs7bG558qNlmz2QbKhsv7QCKiC&si=NgGBaL_MxNcYiIEX). @@ -60,10 +62,10 @@ A step-by-step guide for getting started with ReEDS is available [here](https:// These files are downloaded automatically as needed during a ReEDS run, but to finish all the internet-requiring steps up front, you can download them all by running `python reeds/remote.py`. Additional details on remote files and other topics can be found in the [user guide](https://pages.github.nrel.gov/ReEDS/ReEDS-2.0/user_guide.html#large-input-files). 5. Run ReEDS on a test case from the root of the cloned repository: - 1. For interactive setup: `python runbatch.py` - 2. For one-line operation: `python runbatch.py -b v20250314_main -c test`. + 1. For interactive setup: `python run.py` + 2. For one-line operation: `python run.py -b v20250314_main -c test`. In this example, "v20250314_main" is the prefix for this batch of cases, and "test" is the suffix of the cases file, in this case `cases_test.csv`, located in the root of the repository. - Run `python runbatch.py -h` for information on other optional command-line arguments for ReEDS. + Run `python run.py -h` for information on other optional command-line arguments for ReEDS. diff --git a/cases.csv b/cases.csv index 9d0752e7..5a99444a 100644 --- a/cases.csv +++ b/cases.csv @@ -355,7 +355,7 @@ diagnose,Write A and B matrix of the model [0 = no diagnose ; 1 = diagnose ],0; diagnose_year,Year in which to start report diagnose,N/A,2022, dump_alldata,switch to automatically dump data from final solve year into .gdx file,0; 1,0, file_replacements,"List of files to replace from run folder, e.g: inputs_case/national_gen_frac.csv << //nrelnas01/ReEDS/some proj/national_gen_frac.csv || c_supplymodel.gms << //nrelnas01/ReEDS/some proj/c_supplymodel.gms",N/A,none, -input_processing_only,Only run input_processing scripts; stop before creating and solving model,0; 1,0, +input_processing_only,Only run input-processing scripts; stop before creating and solving model,0; 1,0, keep_augur_files,Indicate whether to keep (1) or delete (0) Augur csv and h5 files after Augur finishes. If you plan to run PRAS via ReEDS2PRAS then set this switch to 1.,0; 1,0, keep_g00_files,Keep (1) or delete (0) .g00 files for completed solve years,0; 1,0, keep_run_terminal,"0=close run terminal, 1=keep run terminal open",0; 1,0, diff --git a/createmodel.gms b/createmodel.gms deleted file mode 100644 index 36ab4603..00000000 --- a/createmodel.gms +++ /dev/null @@ -1,5 +0,0 @@ -$include b_inputs.gms -$include c_supplymodel.gms -$include c_supplyobjective.gms -$include c_mga.gms -$include d_solveprep.gms diff --git a/docs/source/developer_best_practices.md b/docs/source/developer_best_practices.md index 63421d1d..403f6602 100644 --- a/docs/source/developer_best_practices.md +++ b/docs/source/developer_best_practices.md @@ -162,7 +162,7 @@ _Some exceptions to this might exist due to number scaling (e.g., emission rates * If preprocessing is needed to create an input file that is placed in the ReEDS repository, the preprocessing scripts or workbooks should be included in the [ReEDS_Input_Processing repository](https://github.com/ReEDS-Model/ReEDS_Input_Processing). Data from external sources should be downloaded programmatically when possible. -* Any scripts that preprocess data after a ReEDS run is started should be placed in the input_processing folder. +* Any scripts that preprocess data after a ReEDS run is started should be placed in the `reeds/inputs` folder. * Input processing scripts should start with a block of descriptive comments describing the purpose and methodology, and internal functions should use docstrings and liberal comments on functionality and assumptions. * Any costs read into b_inputs should already be in 2004$. Cost adjustments in preprocessing scripts should rely on the deflator.csv file rather than have hard-coded conversions. @@ -174,9 +174,9 @@ _Some exceptions to this might exist due to number scaling (e.g., emission rates * Data column headers should use the ReEDS set names when practical. * Example: data that include regions should use "r" for the column name rather than "ba", "reeds_ba", or "region". -* Preprocessing scripts in input_processing should not change the working directory or use relative filepaths; absolute filepaths should be used wherever possible. +* Preprocessing scripts in `reeds/inputs` should not change the working directory or use relative filepaths; absolute filepaths should be used wherever possible. -* When feasible, inputs used in the objective function (c_supplyobjective.gms) should be included in tests/objective_function_params.yaml. Inputs included in this .yaml file will be checked for missing values using input_processing/check_inputs.py. +* When feasible, inputs used in the objective function (c_supplyobjective.gms) should be included in tests/objective_function_params.yaml. Inputs included in this .yaml file will be checked for missing values using `reeds/inputs/check_inputs.py`. #### Input Data @@ -555,7 +555,7 @@ The following are best practices that should be considered when reviewing pull r - Check out the branch locally (optional) - You should check the branch out locally and run the test scenario (cases_test.csv) to ensure there are no issues - - If there are a large amount of changes to one of the scripts or code files (ex. input_processing scripts or GAMS files), it could be helpful to run just that script and walk through it line by line with a debugging tool (ex. [pdb](https://docs.python.org/3/library/pdb.html)) to more deeply understand how the revised script functions and any issues we might face with the way that script is now written. + - If there are a large amount of changes to one of the scripts or code files (ex. input processing scripts or GAMS files), it could be helpful to run just that script and walk through it line by line with a debugging tool (ex. [pdb](https://docs.python.org/3/library/pdb.html)) to more deeply understand how the revised script functions and any issues we might face with the way that script is now written. **A few notes on reviewing pull requests:** - When reviewing PRs, be sure to provide constructive feedback and highlight positive aspects as well. Reviewing PRs is an opportunity to learn from one another and support each other's development as developers! @@ -629,7 +629,7 @@ Additionally, if you want to re-run a given scenario without having to run all o * To avoid the prompts when kicking off a run, you can use the command line arguments: * The following example runs the scenarios in cases_test.csv with the batch name '20240717_test'. The '-r -1' means that all cases will run simultaneously. ``` - python runbatch.py -c test -b 20240717_test -r -1 + python run.py -c test -b 20240717_test -r -1 ``` * All options for command line arguments that can be used: | Flag | | diff --git a/docs/source/model_documentation.md b/docs/source/model_documentation.md index cc9c6a9c..b56bfa5c 100644 --- a/docs/source/model_documentation.md +++ b/docs/source/model_documentation.md @@ -1922,7 +1922,7 @@ import reeds # GSw_ZoneSet can be any of the supported values listed in the "Choices" column # for the `GSw_ZoneSet` switch in `cases.csv` GSw_ZoneSet = 'z132' -reeds.inputs.get_itls(GSw_ZoneSet=GSw_ZoneSet) +reeds.parse.get_itls(GSw_ZoneSet=GSw_ZoneSet) ``` ``` @@ -3751,7 +3751,9 @@ For all ReEDS system cost results, we assume the operational costs for the nonmo The marginal electricity prices in ReEDS are taken as the shadow prices from the constraints in the model that are directly impacted by the need to serve electricity. These include the load balance constraint, the operating reserve requirement, the RPS and CES requirements, and the planning reserve margin requirement (applied either in stress periods or seasonally via capacity credits). Taken together, these values show the total marginal cost of serving electricity in a given region and time slice. Weighted average versions are also calculated to report national annual marginal electricity prices. These marginal prices are most analogous to wholesale electricity prices but within a model that has full coordination and foresight. ```{admonition} Marginal Price Outputs -Marginal electricity price outputs are calculated with `reqt_price` in `e_report.gms` and are outputs in `reqt_price.csv`. The quantity required by the model is reported in the `reqt_quant.csv` output. These two taken together can be used to calculate a $/MWh electricity price, which is done in make of the ReEDS outputs and reported as "Bulk System Electricity Price" in bokehpivot HTML outputs and in other output locations. +Marginal electricity price outputs are recorded in `reqt_price.csv`. +The quantity required by the model is reported in `reqt_quant.csv`. +These two can be combined to calculate a \$/MWh electricity price. ``` diff --git a/docs/source/reeds_training_homework.md b/docs/source/reeds_training_homework.md index f984daad..93a66cbb 100644 --- a/docs/source/reeds_training_homework.md +++ b/docs/source/reeds_training_homework.md @@ -25,7 +25,7 @@ orphan: true - Open a new terminal and activate the reeds2 environment 7. Start a new run - - `python runbatch.py` + - `python run.py` - when prompted for case file name, enter `examples` - when prompted for how many simultaneous runs you would like to execute, enter 2 - The 'ERCOT_county' and 'ERCOT_BA' runs should start @@ -74,7 +74,7 @@ Create an informal slide deck with the following results: - Open a new terminal and activate the reeds2 environment 7. Start a new run - - `python runbatch.py` + - `python run.py` - when prompted for case file name, enter `examples` - The 'Western_BA_Decarb' run should start diff --git a/docs/source/setup.md b/docs/source/setup.md index 72996f18..10b61d8d 100644 --- a/docs/source/setup.md +++ b/docs/source/setup.md @@ -231,7 +231,7 @@ If that doesn't resolve the issue, the following may help: **Quick Start:** 1. Navigate to the ReEDS directory from the command line 2. Activate environment: `conda activate reeds2` -3. Run the model: `python runbatch.py` +3. Run the model: `python run.py` 4. Follow the prompts for batch configuration 5. Check for a successful run: 1. Look for CSV files in `runs/[batchname_scenario]/outputs` (a successful run should have 100+ csv files in the outputs folder) diff --git a/docs/sources.csv b/docs/sources.csv new file mode 100644 index 00000000..6ecdbcef --- /dev/null +++ b/docs/sources.csv @@ -0,0 +1,805 @@ +RelativeFilePath,RelativeFolderPath,FileName_new,FileExtension,Description_new,Indices,DollarYear,Citation,Filetype,Units +/cases.csv,/,cases,.csv,Contains the configuration settings for the ReEDS run(s).,,2004,https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv,Switches file, +/cases_examples.csv,/,cases_examples,.csv,,,,,, +/cases_small.csv,/,cases_small,.csv,Contains settings to run ReEDS at a smaller scale to test operability of the ReEDS model. Turns off several technologies and reduces the model size to significantly improve solve times.,,,,, +/cases_standardscenarios.csv,/,cases_standardscenarios,.csv,Contains the configuration settings for the Standard Scenarios ReEDS runs.,,,,StdScen Cases file, +/cases_test.csv,/,cases_test,.csv,Contains the configuration settings for doing test runs including the default Pacific census division test case.,,,,, +/e_report_params.csv,/,e_report_params,.csv,Contains a parameter list used in the model along with descriptions of what they are and units used.,,,,, +/hourlize/inputs/load/dummy_agg_op_datacenters.csv,/hourlize/inputs/load,dummy_agg_op_datacenters,.csv,,,,,, +/hourlize/inputs/load/legacy_ba_state_map.csv,/hourlize/inputs/load,legacy_ba_state_map,.csv,,,,,, +/hourlize/inputs/load/legacy_ba_timezone.csv,/hourlize/inputs/load,legacy_ba_timezone,.csv,,,,,, +/hourlize/inputs/resource/egs_resource_classes.csv,/hourlize/inputs/resource,egs_resource_classes,.csv,,,,,, +/hourlize/inputs/resource/geohydro_resource_classes.csv,/hourlize/inputs/resource,geohydro_resource_classes,.csv,,,,,, +/hourlize/inputs/resource/offshore_zone_names.csv,/hourlize/inputs/resource,offshore_zone_names,.csv,,,,,, +/hourlize/inputs/resource/rev_sc_columns.csv,/hourlize/inputs/resource,rev_sc_columns,.csv,,,,,, +/hourlize/inputs/resource/state_abbrev.csv,/hourlize/inputs/resource,state_abbrev,.csv,Contains state names and codesfor the US.,,,,, +/hourlize/inputs/resource/upv_resource_classes.csv,/hourlize/inputs/resource,upv_resource_classes,.csv,Contains information related to UPV class segregation based on mean irradiance levels.,,,,, +/hourlize/inputs/resource/wind-ofs_resource_classes.csv,/hourlize/inputs/resource,wind-ofs_resource_classes,.csv,Contains information related to Offshore wind class segregation and turbine type (fixed vs floating) based on water depth and site lcoe,n/a,,,supply curve input, +/hourlize/inputs/resource/wind-ons_resource_classes.csv,/hourlize/inputs/resource,wind-ons_resource_classes,.csv,Contains information related to Onshore wind class segregation based on mean wind speeds.,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/expected_results,df_sc_out_upv_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,hierarchy_original,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,maxage,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,rev_paths,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,site_bin_map,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,switches,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap_exog,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap_new_bin_out,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,systemcost,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves/upv_supply_curve_raw_unpacked.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves,upv_supply_curve_raw_unpacked,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/expected_results,df_sc_out_upv_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,hierarchy_original,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,maxage,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,rev_paths,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,site_bin_map,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,switches,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap_exog,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap_new_bin_out,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,systemcost,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves/upv_supply_curve_raw_unpacked.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves,upv_supply_curve_raw_unpacked,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/expected_results,df_sc_out_upv_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,hierarchy_original,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,maxage,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,rev_paths,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,site_bin_map,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,switches,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap_exog,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap_new_bin_out,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,systemcost,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves/upv_supply_curve_raw_unpacked.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves,upv_supply_curve_raw_unpacked,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results,df_sc_out_wind-ons_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,hierarchy_original,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,maxage,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,rev_paths,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,site_bin_map,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata/rev_supply_curves.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata,rev_supply_curves,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,switches,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap_exog,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap_new_bin_out,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,systemcost,.csv,,,,,, +/hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves/wind-ons_supply_curve_raw.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves,wind-ons_supply_curve_raw,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs,df_sc_out_upv_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs,df_sc_out_wind-ofs_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs,df_sc_out_wind-ons_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint,df_sc_out_upv_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint,df_sc_out_wind-ofs_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint,df_sc_out_wind-ons_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs,df_sc_out_upv_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs,df_sc_out_wind-ofs_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs,df_sc_out_wind-ons_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_upv,.csv,,,,,, +/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_upv_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced_simul_fill.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_upv_reduced_simul_fill,.csv,,,,,, +/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ofs,.csv,,,,,, +/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ofs_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ons,.csv,,,,,, +/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ons_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,hierarchy_original,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,maxage,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,rev_paths,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,site_bin_map,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,switches,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap_exog,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap_new_bin_out,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, +/hourlize/tests/data/r2r_integration/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,systemcost,.csv,,,,,, +/hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results/upv_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results,upv_supply_curve_raw,.csv,,,,,, +/hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results/wind-ofs_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results,wind-ofs_supply_curve_raw,.csv,,,,,, +/hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results/wind-ons_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results,wind-ons_supply_curve_raw,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_egs_allkm,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm_reduced.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_egs_allkm_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_geohydro_allkm,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm_reduced.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_geohydro_allkm_reduced,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,hierarchy_original,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,maxage,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,rev_paths,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,site_bin_map,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,switches,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap_exog,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap_new_bin_out,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,systemcost,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/supply_curves/egs_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration_geothermal/supply_curves,egs_supply_curve_raw,.csv,,,,,, +/hourlize/tests/data/r2r_integration_geothermal/supply_curves/geohydro_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration_geothermal/supply_curves,geohydro_supply_curve_raw,.csv,,,,,, +/inputs/canada_imports/can_exports.csv,/inputs/canada_imports,can_exports,.csv,Annual exports to Canada by BA,"r,t",,,Input,MWh +/inputs/canada_imports/can_exports_szn_frac.csv,/inputs/canada_imports,can_exports_szn_frac,.csv,Fraction of annual exports to Canada by season,N/A,,,Input,rate (unitless) +/inputs/canada_imports/can_imports.csv,/inputs/canada_imports,can_imports,.csv,Annual imports from Canada by BA,"r,t",,,Input,MWh +/inputs/canada_imports/can_imports_quarter_frac.csv,/inputs/canada_imports,can_imports_quarter_frac,.csv,Fraction of annual imports from Canada by season,N/A,,,Input,rate (unitless) +/inputs/capacity_exogenous/cappayments.csv,/inputs/capacity_exogenous,cappayments,.csv,,,,,, +/inputs/capacity_exogenous/cappayments_ba.csv,/inputs/capacity_exogenous,cappayments_ba,.csv,,,,,, +/inputs/capacity_exogenous/demonstration_plants.csv,/inputs/capacity_exogenous,demonstration_plants,.csv,Nuclear-smr demonstration plants; active when GSw_NuclearDemo=1,"t,r,i,coolingwatertech,ctt,wst,value",,See 'notes' column in the file and https://www.energy.gov/oced/advanced-reactor-demonstration-projects-0,Prescribed capacity,MW +/inputs/capacity_exogenous/exog_cap_geohydro_allkm_reference.csv,/inputs/capacity_exogenous,exog_cap_geohydro_allkm_reference,.csv,,,,,, +/inputs/capacity_exogenous/exog_cap_geohydro_reference.csv,/inputs/capacity_exogenous,exog_cap_geohydro_reference,.csv,,,,,, +/inputs/capacity_exogenous/exog_cap_upv_limited.csv,/inputs/capacity_exogenous,exog_cap_upv_limited,.csv,,,,,, +/inputs/capacity_exogenous/exog_cap_upv_open.csv,/inputs/capacity_exogenous,exog_cap_upv_open,.csv,,,,,, +/inputs/capacity_exogenous/exog_cap_upv_reference.csv,/inputs/capacity_exogenous,exog_cap_upv_reference,.csv,,,,,, +/inputs/capacity_exogenous/exog_cap_wind-ons_limited.csv,/inputs/capacity_exogenous,exog_cap_wind-ons_limited,.csv,,,,,, +/inputs/capacity_exogenous/exog_cap_wind-ons_open.csv,/inputs/capacity_exogenous,exog_cap_wind-ons_open,.csv,,,,,, +/inputs/capacity_exogenous/exog_cap_wind-ons_reference.csv,/inputs/capacity_exogenous,exog_cap_wind-ons_reference,.csv,,,,,, +/inputs/capacity_exogenous/interconnection_queues.csv,/inputs/capacity_exogenous,interconnection_queues,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_limited.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_meshed_limited,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_open.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_meshed_open,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_reference.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_meshed_reference,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_limited.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_radial_limited,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_open.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_radial_open,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_reference.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_radial_reference,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ons_limited.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ons_limited,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ons_open.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ons_open,.csv,,,,,, +/inputs/capacity_exogenous/prescribed_builds_wind-ons_reference.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ons_reference,.csv,,,,,, +/inputs/capacity_exogenous/ReEDS_generator_database_final_EIA-NEMS.csv,/inputs/capacity_exogenous,ReEDS_generator_database_final_EIA-NEMS,.csv,EIA-NEMS database of existing generators,,,,Input, +/inputs/climate/climate_heuristics_finalyear.csv,/inputs/climate,climate_heuristics_finalyear,.csv,,,,,, +/inputs/climate/climate_heuristics_yearfrac.csv,/inputs/climate,climate_heuristics_yearfrac,.csv,,,,,, +/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjann.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario,"r,t",,,,multipliers (unitless) +/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjsea.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario,"r,month,t",,,,multipliers (unitless) +/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMult.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMultAnn.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario,"wst,r,t",,,,multipliers (unitless) +/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterSeaAnnDistr.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the GFDL-ESM2M_RCP4p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_RCP2p6,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_RCP2p6,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario,"wst,r,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_RCP2p6,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP2p6 climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjann.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario,"r,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjsea.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario,"r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario,"wst,r,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp45_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_RCP4p5,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_RCP4p5,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario,"wst,r,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_RCP4p5,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP4p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjann.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario,"r,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjsea.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario,"r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario,"wst,r,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp85_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_RCP8p5,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_RCP8p5,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario,"wst,r,t",,,,multipliers (unitless) +/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_RCP8p5,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP8p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjann.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"r,t",,,,multipliers (unitless) +/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjsea.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"r,month,t",,,,multipliers (unitless) +/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMult.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMultAnn.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"wst,r,t",,,,multipliers (unitless) +/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterSeaAnnDistr.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) +/inputs/consume/consume_char_low.csv,/inputs/consume,consume_char_low,.csv,"Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Conservative assumptions.","i,t",Units vary based on the parameter - see commented text in b_inputs.gms.,N/A,Inputs, +/inputs/consume/consume_char_ref.csv,/inputs/consume,consume_char_ref,.csv,"Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Reference assumptions.","i,t",Units vary based on the parameter - see commented text in b_inputs.gms.,N/A,Inputs, +/inputs/consume/dac_elec_BVRE_2021_high.csv,/inputs/consume,dac_elec_BVRE_2021_high,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions.","i,t",As specified in inputs/consume/dollaryear,https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a,Inputs, +/inputs/consume/dac_elec_BVRE_2021_low.csv,/inputs/consume,dac_elec_BVRE_2021_low,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions.","i,t",As specified in inputs/consume/dollaryear,https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a,Inputs, +/inputs/consume/dac_elec_BVRE_2021_mid.csv,/inputs/consume,dac_elec_BVRE_2021_mid,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions.","i,t",As specified in inputs/consume/dollaryear,https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a,Inputs, +/inputs/consume/dac_gas_BVRE_2021_high.csv,/inputs/consume,dac_gas_BVRE_2021_high,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions.","i,t",As specified in inputs/consume/dollaryear,https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987,Inputs, +/inputs/consume/dac_gas_BVRE_2021_low.csv,/inputs/consume,dac_gas_BVRE_2021_low,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions.","i,t",As specified in inputs/consume/dollaryear,https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987,Inputs, +/inputs/consume/dac_gas_BVRE_2021_mid.csv,/inputs/consume,dac_gas_BVRE_2021_mid,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions.","i,t",As specified in inputs/consume/dollaryear,https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987,Inputs, +/inputs/consume/dollaryear.csv,/inputs/consume,dollaryear,.csv,Dollar year for various Beyond VRE scenarios. ,N/A,Stated in document.,N/A,Inputs, +/inputs/consume/h2_demand_county_share.csv,/inputs/consume,h2_demand_county_share,.csv,"The fraction of national hydrogen demand in that year that corresponds to each county. Demand estimates come from https://data.openei.org/submissions/5655. 2021 demand shares correspond to the ""Reference"" scenario with light-duty vehicles / biofuels / methanol demand removed and 2050 shares correspond to the ""Low Cost Electrolysis"" scenario.","r,t",N/A,N/A,Inputs, +/inputs/consume/h2_exogenous_demand.csv,/inputs/consume,h2_exogenous_demand,.csv,Exogenous hydrogen demand by industries other than the power sector per year,t,N/A,N/A,Inputs, +/inputs/consume/h2_transport_and_storage_costs.csv,/inputs/consume,h2_transport_and_storage_costs,.csv,Transport and storage costs of hydrogen per year,t,2004,N/A,Inputs, +/inputs/county2zone.csv,/inputs,county2zone,.csv,,,,,, +/inputs/ctus/co2_site_char.csv,/inputs/ctus,co2_site_char,.csv,,,2018,,, +/inputs/ctus/cs.csv,/inputs/ctus,cs,.csv,,,,,, +/inputs/degradation/degradation_annual_default.csv,/inputs/degradation,degradation_annual_default,.csv,,,,,, +/inputs/demand_response/dr_shed_avail_scalar.csv,/inputs/demand_response,dr_shed_avail_scalar,.csv,,,,,, +/inputs/demand_response/dr_shed_capacity_scalar_demo_data_January_2025.csv,/inputs/demand_response,dr_shed_capacity_scalar_demo_data_January_2025,.csv,,,,,, +/inputs/demand_response/dr_shed_hourly.h5,/inputs/demand_response,dr_shed_hourly,.h5,,,,,, +/inputs/demand_response/ev_load_Baseline.h5,/inputs/demand_response,ev_load_Baseline,.h5,Baseline electricity load from EV charging by timeslice h and year t,,,,inputs,MW +/inputs/demand_response/evmc_rsc_Baseline.csv,/inputs/demand_response,evmc_rsc_Baseline,.csv,,,,,, +/inputs/demand_response/evmc_shape_decrease_profile_Baseline.h5,/inputs/demand_response,evmc_shape_decrease_profile_Baseline,.h5,,,,,, +/inputs/demand_response/evmc_shape_increase_profile_Baseline.h5,/inputs/demand_response,evmc_shape_increase_profile_Baseline,.h5,,,,,, +/inputs/demand_response/evmc_storage_decrease_profile_Baseline.h5,/inputs/demand_response,evmc_storage_decrease_profile_Baseline,.h5,,,,,, +/inputs/demand_response/evmc_storage_increase_profile_Baseline.h5,/inputs/demand_response,evmc_storage_increase_profile_Baseline,.h5,,,,,, +/inputs/demand_response/evmc_storage_profile_energy_Baseline.h5,/inputs/demand_response,evmc_storage_profile_energy_Baseline,.h5,,,,,, +/inputs/dgen_model_inputs/stscen2023_electrification/distpvcap_stscen2023_electrification.csv,/inputs/dgen_model_inputs/stscen2023_electrification,distpvcap_stscen2023_electrification,.csv,,,,,, +/inputs/dgen_model_inputs/stscen2023_highng/distpvcap_stscen2023_highng.csv,/inputs/dgen_model_inputs/stscen2023_highng,distpvcap_stscen2023_highng,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with high NG (including distpv) costs,,,,distribution PV inputs , +/inputs/dgen_model_inputs/stscen2023_highre/distpvcap_stscen2023_highre.csv,/inputs/dgen_model_inputs/stscen2023_highre,distpvcap_stscen2023_highre,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with high RE (including distpv) costs,,,,distribution PV inputs , +/inputs/dgen_model_inputs/stscen2023_lowng/distpvcap_stscen2023_lowng.csv,/inputs/dgen_model_inputs/stscen2023_lowng,distpvcap_stscen2023_lowng,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with low NG (including distpv) costs,,,,distribution PV inputs , +/inputs/dgen_model_inputs/stscen2023_lowre/distpvcap_stscen2023_lowre.csv,/inputs/dgen_model_inputs/stscen2023_lowre,distpvcap_stscen2023_lowre,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with low RE (including distpv) costs,,,,distribution PV inputs , +/inputs/dgen_model_inputs/stscen2023_mid_case/distpvcap_stscen2023_mid_case.csv,/inputs/dgen_model_inputs/stscen2023_mid_case,distpvcap_stscen2023_mid_case,.csv,,,,,distribution PV inputs , +/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035/distpvcap_stscen2023_mid_case_95_by_2035.csv,/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035,distpvcap_stscen2023_mid_case_95_by_2035,.csv,,,,,distribution PV inputs , +/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050/distpvcap_stscen2023_mid_case_95_by_2050.csv,/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050,distpvcap_stscen2023_mid_case_95_by_2050,.csv,,,,,distribution PV inputs , +/inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050/distpvcap_stscen2023_taxcredit_extended2050.csv,/inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050,distpvcap_stscen2023_taxcredit_extended2050,.csv,,,,,distribution PV inputs , +/inputs/disaggregation/county_population.csv,/inputs/disaggregation,county_population,.csv,"The population of each county, relative values are used as multipliers for downselecting data. Data come from the U.S. Census Bureau 2021 county population estimates (https://www.census.gov/data/tables/time-series/demo/popest/2020s-counties-total.html).",FIPS,,,, +/inputs/disaggregation/county_state_lpf.csv,/inputs/disaggregation,county_state_lpf,.csv,,,,,, +/inputs/disaggregation/disagg_hydroexist.csv,/inputs/disaggregation,disagg_hydroexist,.csv,"The hydropower capacity fraction of each county within a given ReEDS BA, used as multipliers for downselecting data",r,,,, +/inputs/emission_constraints/ccs_link.csv,/inputs/emission_constraints,ccs_link,.csv,,,,,, +/inputs/emission_constraints/ccs_link_water.csv,/inputs/emission_constraints,ccs_link_water,.csv,,,,,, +/inputs/emission_constraints/co2_cap.csv,/inputs/emission_constraints,co2_cap,.csv,Annual nationwide carbon cap,,,,, +/inputs/emission_constraints/co2_tax.csv,/inputs/emission_constraints,co2_tax,.csv,Annual co2 tax,,,,, +/inputs/emission_constraints/county_co2_share_egrid_2022.csv,/inputs/emission_constraints,county_co2_share_egrid_2022,.csv,,,,,, +/inputs/emission_constraints/csapr_group1_ex.csv,/inputs/emission_constraints,csapr_group1_ex,.csv,,,,,, +/inputs/emission_constraints/csapr_group2_ex.csv,/inputs/emission_constraints,csapr_group2_ex,.csv,,,,,, +/inputs/emission_constraints/csapr_ozone_season.csv,/inputs/emission_constraints,csapr_ozone_season,.csv,,,,,, +/inputs/emission_constraints/emitrate.csv,/inputs/emission_constraints,emitrate,.csv,Emission rates for thermal generators with values from Table 5 of https://docs.nrel.gov/docs/fy25osti/93005.pdf,"i,e",,,, +/inputs/emission_constraints/gwp.csv,/inputs/emission_constraints,gwp,.csv,,,,,, +/inputs/emission_constraints/h2_leakage_rate.csv,/inputs/emission_constraints,h2_leakage_rate,.csv,,,,,, +/inputs/emission_constraints/methane_leakage_rate.csv,/inputs/emission_constraints,methane_leakage_rate,.csv,,,,,, +/inputs/emission_constraints/ng_crf_penalty.csv,/inputs/emission_constraints,ng_crf_penalty,.csv,Cost adjustment for NG techs in scenarios with national decarbonization targets,allt,N/A,https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220,Inputs,rate (unitless) +/inputs/emission_constraints/rggi_states.csv,/inputs/emission_constraints,rggi_states,.csv,Participating RGGI states,,,https://www.rggi.org/program-overview-and-design/elements,, +/inputs/emission_constraints/rggicon.csv,/inputs/emission_constraints,rggicon,.csv,CO2 caps for RGGI states in metric tons,,,https://www.rggi.org/allowance-tracking/allowance-distribution,, +/inputs/emission_constraints/state_cap.csv,/inputs/emission_constraints,state_cap,.csv,,,,,, +/inputs/financials/cap_penalty.csv,/inputs/financials,cap_penalty,.csv,,,,,, +/inputs/financials/construction_schedules_default.csv,/inputs/financials,construction_schedules_default,.csv,,,,,, +/inputs/financials/construction_times_default.csv,/inputs/financials,construction_times_default,.csv,,,,,, +/inputs/financials/currency_incentives.csv,/inputs/financials,currency_incentives,.csv,,,,,, +/inputs/financials/deflator.csv,/inputs/financials,deflator,.csv,Dollar year deflator to convert values to 2004$,,,,, +/inputs/financials/depreciation_schedules_default.csv,/inputs/financials,depreciation_schedules_default,.csv,,,,,, +/inputs/financials/energy_communities.csv,/inputs/financials,energy_communities,.csv,,,,,, +/inputs/financials/financials_hydrogen.csv,/inputs/financials,financials_hydrogen,.csv,,,,,, +/inputs/financials/financials_sys_ATB2023.csv,/inputs/financials,financials_sys_ATB2023,.csv,,,,,, +/inputs/financials/financials_sys_ATB2024.csv,/inputs/financials,financials_sys_ATB2024,.csv,,,,,, +/inputs/financials/financials_tech_ATB2023.csv,/inputs/financials,financials_tech_ATB2023,.csv,,,,,, +/inputs/financials/financials_tech_ATB2023_CRP20.csv,/inputs/financials,financials_tech_ATB2023_CRP20,.csv,,,,,, +/inputs/financials/financials_tech_ATB2024.csv,/inputs/financials,financials_tech_ATB2024,.csv,,,,,, +/inputs/financials/financials_transmission_30ITC_0pen_2022_2031.csv,/inputs/financials,financials_transmission_30ITC_0pen_2022_2031,.csv,,,,,, +/inputs/financials/financials_transmission_default.csv,/inputs/financials,financials_transmission_default,.csv,,,,,, +/inputs/financials/incentives_annual.csv,/inputs/financials,incentives_annual,.csv,,,,,, +/inputs/financials/incentives_biennial.csv,/inputs/financials,incentives_biennial,.csv,,,,,, +/inputs/financials/incentives_ira.csv,/inputs/financials,incentives_ira,.csv,,,,,, +/inputs/financials/incentives_ira_45q_45v_extension.csv,/inputs/financials,incentives_ira_45q_45v_extension,.csv,,,,,, +/inputs/financials/incentives_noira.csv,/inputs/financials,incentives_noira,.csv,,,,,, +/inputs/financials/incentives_none.csv,/inputs/financials,incentives_none,.csv,,,,,, +/inputs/financials/incentives_obbba.csv,/inputs/financials,incentives_obbba,.csv,,,,,, +/inputs/financials/incentives_obbba_conservative.csv,/inputs/financials,incentives_obbba_conservative,.csv,,,,,, +/inputs/financials/inflation_default.csv,/inputs/financials,inflation_default,.csv,Annual inflation factors from 1914 through 2200; historical values use the avg-avg values from https://www.usinflationcalculator.com/inflation/consumer-price-index-and-annual-percent-changes-from-1913-to-2008/,t,,,, +/inputs/financials/nuclear_energy_communities.csv,/inputs/financials,nuclear_energy_communities,.csv,"""Counties belonging to metropolitan statistical areas (MSAs) for which at least 0.17 percent of direct employment has been related to nuclear power at any point since 2010. These are determined partly by following the process described in Section 2.6 of https://home.treasury.gov/system/files/8861/EnergyCommunities_Data_Documentation.pdf and substituting in the NAICS code for nuclear electric power generation (221113) and partly by determining counties that belong to MSAs where the number of people employed by national labs engaged in nuclear research and development (PNNL, INL, ORNL, SNL, LLNL, Argonne, and LANL) has been at least 0.17 percent of the MSA's total employment at any point since 2010.""",,,,, +/inputs/financials/reg_cap_cost_diff_default.csv,/inputs/financials,reg_cap_cost_diff_default,.csv,region-specific differences for capital cost of all resources. Add to 1 to produce a multiplier,"i,r",,,parameter, +/inputs/financials/retire_penalty.csv,/inputs/financials,retire_penalty,.csv,,,,,, +/inputs/financials/supply_chain_adjust.csv,/inputs/financials,supply_chain_adjust,.csv,,,,,, +/inputs/financials/tc_phaseout_schedule_ira2022.csv,/inputs/financials,tc_phaseout_schedule_ira2022,.csv,,,,,, +/inputs/fuelprices/alpha_AEO_2023_HOG.csv,/inputs/fuelprices,alpha_AEO_2023_HOG,.csv,"High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2023,Input, +/inputs/fuelprices/alpha_AEO_2023_LOG.csv,/inputs/fuelprices,alpha_AEO_2023_LOG,.csv,"Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2023,Input, +/inputs/fuelprices/alpha_AEO_2023_reference.csv,/inputs/fuelprices,alpha_AEO_2023_reference,.csv,"reference census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2023,Input, +/inputs/fuelprices/alpha_AEO_2025_HOG.csv,/inputs/fuelprices,alpha_AEO_2025_HOG,.csv,"High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2025,Input, +/inputs/fuelprices/alpha_AEO_2025_LOG.csv,/inputs/fuelprices,alpha_AEO_2025_LOG,.csv,"Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2025,Input, +/inputs/fuelprices/alpha_AEO_2025_reference.csv,/inputs/fuelprices,alpha_AEO_2025_reference,.csv,"reference census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2025,Input, +/inputs/fuelprices/cd_beta0.csv,/inputs/fuelprices,cd_beta0,.csv,reference census division beta levels electric sector,cendiv,2004,,Input, +/inputs/fuelprices/cd_beta0_allsector.csv,/inputs/fuelprices,cd_beta0_allsector,.csv,reference census division beta levels all sectors,cendiv,2004,,Input, +/inputs/fuelprices/cendivweights.csv,/inputs/fuelprices,cendivweights,.csv,weights to smooth gas prices between census regions to avoid abrupt price changes at the cendiv borders,"r,cendiv",,,, +/inputs/fuelprices/coal_AEO_2023_reference.csv,/inputs/fuelprices,coal_AEO_2023_reference,.csv,reference case census division fuel price of coal,"t,cendiv",2022,,, +/inputs/fuelprices/coal_AEO_2025_reference.csv,/inputs/fuelprices,coal_AEO_2025_reference,.csv,reference case census division fuel price of coal with missing values forward-filled from earlier years,"t,cendiv",2024,,, +/inputs/fuelprices/dollaryear.csv,/inputs/fuelprices,dollaryear,.csv,Dollar year mapping for each fuel price scenario,,,,, +/inputs/fuelprices/h2-combustion_10.csv,/inputs/fuelprices,h2-combustion_10,.csv,price of hydrogen for combustion technologies (h2-ct and cc) at $10/MMBtu for all years,,,,, +/inputs/fuelprices/h2-combustion_30.csv,/inputs/fuelprices,h2-combustion_30,.csv,price of hydrogen for combustion technologies (h2-ct and cc) at $30/MMBtu for all years,,,,, +/inputs/fuelprices/h2-combustion_reference.csv,/inputs/fuelprices,h2-combustion_reference,.csv,price of hydrogen for combustion technologies (h2-ct and cc) at $20/MMBtu for all years,,,,, +/inputs/fuelprices/ng_AEO_2023_HOG.csv,/inputs/fuelprices,ng_AEO_2023_HOG,.csv,High Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu +/inputs/fuelprices/ng_AEO_2023_LOG.csv,/inputs/fuelprices,ng_AEO_2023_LOG,.csv,Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu +/inputs/fuelprices/ng_AEO_2023_reference.csv,/inputs/fuelprices,ng_AEO_2023_reference,.csv,Reference scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu +/inputs/fuelprices/ng_AEO_2025_HOG.csv,/inputs/fuelprices,ng_AEO_2025_HOG,.csv,High Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu +/inputs/fuelprices/ng_AEO_2025_LOG.csv,/inputs/fuelprices,ng_AEO_2025_LOG,.csv,Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu +/inputs/fuelprices/ng_AEO_2025_reference.csv,/inputs/fuelprices,ng_AEO_2025_reference,.csv,Reference scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu +/inputs/fuelprices/ng_demand_AEO_2023_HOG.csv,/inputs/fuelprices,ng_demand_AEO_2023_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_demand_AEO_2023_LOG.csv,/inputs/fuelprices,ng_demand_AEO_2023_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_demand_AEO_2023_reference.csv,/inputs/fuelprices,ng_demand_AEO_2023_reference,.csv,"Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_demand_AEO_2025_HOG.csv,/inputs/fuelprices,ng_demand_AEO_2025_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_demand_AEO_2025_LOG.csv,/inputs/fuelprices,ng_demand_AEO_2025_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_demand_AEO_2025_reference.csv,/inputs/fuelprices,ng_demand_AEO_2025_reference,.csv,"Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_tot_demand_AEO_2023_HOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2023_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_tot_demand_AEO_2023_LOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2023_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_tot_demand_AEO_2023_reference.csv,/inputs/fuelprices,ng_tot_demand_AEO_2023_reference,.csv,"Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_tot_demand_AEO_2025_HOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2025_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_tot_demand_AEO_2025_LOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2025_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/ng_tot_demand_AEO_2025_reference.csv,/inputs/fuelprices,ng_tot_demand_AEO_2025_reference,.csv,"Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads +/inputs/fuelprices/uranium_AEO_2023_reference.csv,/inputs/fuelprices,uranium_AEO_2023_reference,.csv,,,,,, +/inputs/fuelprices/uranium_AEO_2025_reference.csv,/inputs/fuelprices,uranium_AEO_2025_reference,.csv,,,,,, +/inputs/geothermal/geo_discovery_BAU.csv,/inputs/geothermal,geo_discovery_BAU,.csv,,,,,, +/inputs/geothermal/geo_discovery_factor_ATB_2023.csv,/inputs/geothermal,geo_discovery_factor_ATB_2023,.csv,,,,,, +/inputs/geothermal/geo_discovery_factor_reV.csv,/inputs/geothermal,geo_discovery_factor_reV,.csv,,,,,, +/inputs/geothermal/geo_discovery_TI.csv,/inputs/geothermal,geo_discovery_TI,.csv,,,,,, +/inputs/geothermal/geo_rsc_ATB_2023.csv,/inputs/geothermal,geo_rsc_ATB_2023,.csv,,,,,, +/inputs/growth_constraints/gbin_min.csv,/inputs/growth_constraints,gbin_min,.csv,,,,,, +/inputs/growth_constraints/growth_bin_size_mult.csv,/inputs/growth_constraints,growth_bin_size_mult,.csv,,,,,, +/inputs/growth_constraints/growth_limit_absolute.csv,/inputs/growth_constraints,growth_limit_absolute,.csv,"Maximum expected annual builds for wind, batteries, and UPV from 2024-2026 using observed record builds.",,,,,MW/year +/inputs/growth_constraints/growth_penalty.csv,/inputs/growth_constraints,growth_penalty,.csv,,,,,, +/inputs/hierarchy.csv,/inputs,hierarchy,.csv,,,,,, +/inputs/hierarchy_agg125.csv,/inputs,hierarchy_agg125,.csv,,,,,, +/inputs/hierarchy_agg54.csv,/inputs,hierarchy_agg54,.csv,,,,,, +/inputs/hierarchy_agg69.csv,/inputs,hierarchy_agg69,.csv,,,,,, +/inputs/zones/hierarchy_offshore.csv,/inputs/zones,hierarchy_offshore,.csv,,,,,, +/inputs/hydro/cap_existing_hydro.h5,/inputs/hydro,cap_existing_hydro,.h5,"Annual capacities for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset.",t,,,Input,MW +/inputs/hydro/hyd_fom.csv,/inputs/hydro,hyd_fom,.csv,Regional FOM costs for hydro,,,,, +/inputs/hydro/hydcf_fixed.h5,/inputs/hydro,hydcf_fixed,.h5,Fixed monthly zonal hydro capacity factor data partially created by ORNL and partially derived from ORNL's Existing Hydropower Assets dataset.,"i,month",,,Input,unitless +/inputs/hydro/hydro_mingen.csv,/inputs/hydro,hydro_mingen,.csv,,,,,, +/inputs/hydro/net_gen_existing_hydro.h5,/inputs/hydro,net_gen_existing_hydro,.h5,"Monthly net generation values for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset.","t,month",,,Input,MWh +/inputs/hydro/SeaCapAdj_hy.csv,/inputs/hydro,SeaCapAdj_hy,.csv,,,,,, +/inputs/load/cangrowth.csv,/inputs/load,cangrowth,.csv,Canada load growth multiplier,,,,, +/inputs/load/demand_AEO_2023_high.csv,/inputs/load,demand_AEO_2023_high,.csv,Load growth projection from the AEO2023 High Economic Growth scenario,,,,,unitless +/inputs/load/demand_AEO_2023_low.csv,/inputs/load,demand_AEO_2023_low,.csv,Load growth projection from the AEO2023 Low Economic Growth scenario,,,,,unitless +/inputs/load/demand_AEO_2023_reference.csv,/inputs/load,demand_AEO_2023_reference,.csv,Load growth projection from the AEO2023 Reference scenario,,,,,unitless +/inputs/load/demand_AEO_2025_high.csv,/inputs/load,demand_AEO_2025_high,.csv,Load growth projection from the AEO2025 High Economic Growth scenario,,,,,unitless +/inputs/load/demand_AEO_2025_low.csv,/inputs/load,demand_AEO_2025_low,.csv,Load growth projection from the AEO2025 Low Economic Growth scenario,,,,,unitless +/inputs/load/demand_AEO_2025_reference.csv,/inputs/load,demand_AEO_2025_reference,.csv,Load growth projection from the AEO2025 Reference scenario,,,,,unitless +/inputs/load/EIA_loadbystate.csv,/inputs/load,EIA_loadbystate,.csv,,,,,, +/inputs/load/loadsite_country_test.csv,/inputs/load,loadsite_country_test,.csv,,,,,, +/inputs/load/mex_growth_rate.csv,/inputs/load,mex_growth_rate,.csv,Mexico load growth multiplier,,,,, +/inputs/national_generation/gen_mandate_tech_list.csv,/inputs/national_generation,gen_mandate_tech_list,.csv,,,,,, +/inputs/national_generation/gen_mandate_trajectory.csv,/inputs/national_generation,gen_mandate_trajectory,.csv,,,,,, +/inputs/national_generation/national_rps_frac_allScen.csv,/inputs/national_generation,national_rps_frac_allScen,.csv,,,,,, +/inputs/outages/temperature_celsius-st.h5,/inputs/outages,temperature_celsius-st,.h5,,,,,, +/inputs/plant_characteristics/battery_ATB_2024_advanced.csv,/inputs/plant_characteristics,battery_ATB_2024_advanced,.csv,,,2021,,, +/inputs/plant_characteristics/battery_ATB_2024_conservative.csv,/inputs/plant_characteristics,battery_ATB_2024_conservative,.csv,,,2021,,, +/inputs/plant_characteristics/battery_ATB_2024_moderate.csv,/inputs/plant_characteristics,battery_ATB_2024_moderate,.csv,,,2021,,, +/inputs/plant_characteristics/beccs_BVRE_2021_high.csv,/inputs/plant_characteristics,beccs_BVRE_2021_high,.csv,,,,,, +/inputs/plant_characteristics/beccs_BVRE_2021_low.csv,/inputs/plant_characteristics,beccs_BVRE_2021_low,.csv,,,,,, +/inputs/plant_characteristics/beccs_BVRE_2021_mid.csv,/inputs/plant_characteristics,beccs_BVRE_2021_mid,.csv,,,,,, +/inputs/plant_characteristics/beccs_lowcost.csv,/inputs/plant_characteristics,beccs_lowcost,.csv,,,,,, +/inputs/plant_characteristics/beccs_reference.csv,/inputs/plant_characteristics,beccs_reference,.csv,,,,,, +/inputs/plant_characteristics/biopower_ATB_2024_moderate.csv,/inputs/plant_characteristics,biopower_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/ccsflex_ATB_2020_cost.csv,/inputs/plant_characteristics,ccsflex_ATB_2020_cost,.csv,,,,,, +/inputs/plant_characteristics/ccsflex_ATB_2020_perf.csv,/inputs/plant_characteristics,ccsflex_ATB_2020_perf,.csv,,,,,, +/inputs/plant_characteristics/coal-ccs_ATB_2024_advanced.csv,/inputs/plant_characteristics,coal-ccs_ATB_2024_advanced,.csv,,,,,, +/inputs/plant_characteristics/coal-ccs_ATB_2024_conservative.csv,/inputs/plant_characteristics,coal-ccs_ATB_2024_conservative,.csv,,,,,, +/inputs/plant_characteristics/coal-ccs_ATB_2024_moderate.csv,/inputs/plant_characteristics,coal-ccs_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/coal_ATB_2024_moderate.csv,/inputs/plant_characteristics,coal_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/cost_opres_default.csv,/inputs/plant_characteristics,cost_opres_default,.csv,,,,,, +/inputs/plant_characteristics/cost_opres_market.csv,/inputs/plant_characteristics,cost_opres_market,.csv,,,,,, +/inputs/plant_characteristics/csp_ATB_2023_advanced.csv,/inputs/plant_characteristics,csp_ATB_2023_advanced,.csv,,,,,, +/inputs/plant_characteristics/csp_ATB_2023_conservative.csv,/inputs/plant_characteristics,csp_ATB_2023_conservative,.csv,,,,,, +/inputs/plant_characteristics/csp_ATB_2023_moderate.csv,/inputs/plant_characteristics,csp_ATB_2023_moderate,.csv,,,,,, +/inputs/plant_characteristics/csp_ATB_2024_advanced.csv,/inputs/plant_characteristics,csp_ATB_2024_advanced,.csv,,,,,, +/inputs/plant_characteristics/csp_ATB_2024_conservative.csv,/inputs/plant_characteristics,csp_ATB_2024_conservative,.csv,,,,,, +/inputs/plant_characteristics/csp_ATB_2024_moderate.csv,/inputs/plant_characteristics,csp_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/csp_SunShot2030.csv,/inputs/plant_characteristics,csp_SunShot2030,.csv,Csp costs from the SunShot2030 cost scenario,,,,, +/inputs/plant_characteristics/dollaryear.csv,/inputs/plant_characteristics,dollaryear,.csv,Dollar year mapping for each plant cost scenario,,,,, +/inputs/plant_characteristics/dr_shed_capcost_demo_data_IEF_January_2025.csv,/inputs/plant_characteristics,dr_shed_capcost_demo_data_IEF_January_2025,.csv,,,,,, +/inputs/plant_characteristics/dr_shed_fom.csv,/inputs/plant_characteristics,dr_shed_fom,.csv,,,,,, +/inputs/plant_characteristics/dr_shed_vom.csv,/inputs/plant_characteristics,dr_shed_vom,.csv,,,,,, +/inputs/plant_characteristics/evmc_shape_Baseline.csv,/inputs/plant_characteristics,evmc_shape_Baseline,.csv,,,,,, +/inputs/plant_characteristics/evmc_storage_Baseline.csv,/inputs/plant_characteristics,evmc_storage_Baseline,.csv,,,,,, +/inputs/plant_characteristics/gas-ccs_ATB_2024_advanced.csv,/inputs/plant_characteristics,gas-ccs_ATB_2024_advanced,.csv,,,,,, +/inputs/plant_characteristics/gas-ccs_ATB_2024_conservative.csv,/inputs/plant_characteristics,gas-ccs_ATB_2024_conservative,.csv,,,,,, +/inputs/plant_characteristics/gas-ccs_ATB_2024_moderate.csv,/inputs/plant_characteristics,gas-ccs_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/gas_ATB_2024_moderate.csv,/inputs/plant_characteristics,gas_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/geo_ATB_2023_advanced.csv,/inputs/plant_characteristics,geo_ATB_2023_advanced,.csv,,,,,, +/inputs/plant_characteristics/geo_ATB_2023_conservative.csv,/inputs/plant_characteristics,geo_ATB_2023_conservative,.csv,,,,,, +/inputs/plant_characteristics/geo_ATB_2023_moderate.csv,/inputs/plant_characteristics,geo_ATB_2023_moderate,.csv,,,,,, +/inputs/plant_characteristics/geo_ATB_2024_advanced.csv,/inputs/plant_characteristics,geo_ATB_2024_advanced,.csv,,,,,, +/inputs/plant_characteristics/geo_ATB_2024_conservative.csv,/inputs/plant_characteristics,geo_ATB_2024_conservative,.csv,,,,,, +/inputs/plant_characteristics/geo_ATB_2024_moderate.csv,/inputs/plant_characteristics,geo_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/h2-combustion_ATB_2023.csv,/inputs/plant_characteristics,h2-combustion_ATB_2023,.csv,,,,,, +/inputs/plant_characteristics/h2-combustion_ATB_2024.csv,/inputs/plant_characteristics,h2-combustion_ATB_2024,.csv,Hydrogen CT and CC plant costs generated in preprocessing from moderate case NREL ATB 2024 data,,,,, +/inputs/plant_characteristics/heat_rate_adj.csv,/inputs/plant_characteristics,heat_rate_adj,.csv,Heat rate adjustment multiplier by technology,,,,, +/inputs/plant_characteristics/heat_rate_penalty_spin.csv,/inputs/plant_characteristics,heat_rate_penalty_spin,.csv,,,,,, +/inputs/plant_characteristics/hydro_ATB_2019_constant.csv,/inputs/plant_characteristics,hydro_ATB_2019_constant,.csv,Hydro costs from the 2019 ATB constant cost scenario,,,,, +/inputs/plant_characteristics/hydro_ATB_2019_low.csv,/inputs/plant_characteristics,hydro_ATB_2019_low,.csv,Hydro costs from the 2019 ATB low cost scenario,,,,, +/inputs/plant_characteristics/hydro_ATB_2019_mid.csv,/inputs/plant_characteristics,hydro_ATB_2019_mid,.csv,Hydro costs from the 2019 ATB mid cost scenario,,,,, +/inputs/plant_characteristics/maxage.csv,/inputs/plant_characteristics,maxage,.csv,Maximum age allowed for each technology,,,,, +/inputs/plant_characteristics/maxdailycf.csv,/inputs/plant_characteristics,maxdailycf,.csv,maximum daily capacity factor--dr_shed input supply curves are based on one 4-hour event per day,,,,, +/inputs/plant_characteristics/min_retire_age.csv,/inputs/plant_characteristics,min_retire_age,.csv,Minimum retirement age for given technology,,,,, +/inputs/plant_characteristics/minCF.csv,/inputs/plant_characteristics,minCF,.csv,minimum annual capacity factor for each tech fleet - applied to i-rto,,,,, +/inputs/plant_characteristics/mingen_fixed.csv,/inputs/plant_characteristics,mingen_fixed,.csv,,,,,, +/inputs/plant_characteristics/minloadfrac0.csv,/inputs/plant_characteristics,minloadfrac0,.csv,characteristics/minloadfrac0 database of minloadbed generator cs,,,,, +/inputs/plant_characteristics/mttr.csv,/inputs/plant_characteristics,mttr,.csv,,,,,, +/inputs/plant_characteristics/nuclear-smr_ATB_2024_advanced.csv,/inputs/plant_characteristics,nuclear-smr_ATB_2024_advanced,.csv,,,,,, +/inputs/plant_characteristics/nuclear-smr_ATB_2024_conservative.csv,/inputs/plant_characteristics,nuclear-smr_ATB_2024_conservative,.csv,,,,,, +/inputs/plant_characteristics/nuclear-smr_ATB_2024_moderate.csv,/inputs/plant_characteristics,nuclear-smr_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/nuclear_ATB_2024_advanced.csv,/inputs/plant_characteristics,nuclear_ATB_2024_advanced,.csv,,,,,, +/inputs/plant_characteristics/nuclear_ATB_2024_conservative.csv,/inputs/plant_characteristics,nuclear_ATB_2024_conservative,.csv,,,,,, +/inputs/plant_characteristics/nuclear_ATB_2024_moderate.csv,/inputs/plant_characteristics,nuclear_ATB_2024_moderate,.csv,,,,,, +/inputs/plant_characteristics/ofs-wind_ATB_2023_advanced.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_advanced,.csv,"2023 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2004,,Inputs file, +/inputs/plant_characteristics/ofs-wind_ATB_2023_conservative.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_conservative,.csv,"2023 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2004,,Inputs file, +/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_moderate,.csv,"2023 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2004,,Inputs file, +/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate_noFloating.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_moderate_noFloating,.csv,,,,,, +/inputs/plant_characteristics/ofs-wind_ATB_2024_advanced.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_advanced,.csv,"2024 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2022,,Inputs file, +/inputs/plant_characteristics/ofs-wind_ATB_2024_conservative.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_conservative,.csv,"2024 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2022,,Inputs file, +/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_moderate,.csv,"2024 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2022,,Inputs file, +/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate_noFloating.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_moderate_noFloating,.csv,"2024 moderate_noFloating ofs-wind capital (5x floating capital cost), fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year",,2022,,Inputs file, +/inputs/plant_characteristics/ons-wind_ATB_2023_advanced.csv,/inputs/plant_characteristics,ons-wind_ATB_2023_advanced,.csv,,,,,, +/inputs/plant_characteristics/ons-wind_ATB_2023_conservative.csv,/inputs/plant_characteristics,ons-wind_ATB_2023_conservative,.csv,,,,,, +/inputs/plant_characteristics/ons-wind_ATB_2023_moderate.csv,/inputs/plant_characteristics,ons-wind_ATB_2023_moderate,.csv,,,,,, +/inputs/plant_characteristics/ons-wind_ATB_2024_advanced.csv,/inputs/plant_characteristics,ons-wind_ATB_2024_advanced,.csv,Advanced cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind,,2022,,Inputs file, +/inputs/plant_characteristics/ons-wind_ATB_2024_conservative.csv,/inputs/plant_characteristics,ons-wind_ATB_2024_conservative,.csv,Conservative cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind,,2022,,Inputs file, +/inputs/plant_characteristics/ons-wind_ATB_2024_moderate.csv,/inputs/plant_characteristics,ons-wind_ATB_2024_moderate,.csv,Moderate cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind,,2022,,Inputs file, +/inputs/plant_characteristics/other_plantchar.csv,/inputs/plant_characteristics,other_plantchar,.csv,,,,,, +/inputs/plant_characteristics/outage_forced_static.csv,/inputs/plant_characteristics,outage_forced_static,.csv,Forced outage rates by technology,,,,Inputs file, +/inputs/plant_characteristics/outage_forced_temperature_murphy2019.csv,/inputs/plant_characteristics,outage_forced_temperature_murphy2019,.csv,,,,,, +/inputs/plant_characteristics/outage_scheduled_monthly.csv,/inputs/plant_characteristics,outage_scheduled_monthly,.csv,,,,,, +/inputs/plant_characteristics/outage_scheduled_static.csv,/inputs/plant_characteristics,outage_scheduled_static,.csv,Scheduled outage rate by technology,,,,, +/inputs/plant_characteristics/pvb_benchmark2020.csv,/inputs/plant_characteristics,pvb_benchmark2020,.csv,,,,,, +/inputs/plant_characteristics/ramprate.csv,/inputs/plant_characteristics,ramprate,.csv,Generator ramp rates by technology,,,,, +/inputs/plant_characteristics/startcost.csv,/inputs/plant_characteristics,startcost,.csv,,,,,, +/inputs/plant_characteristics/unitsize_atb.csv,/inputs/plant_characteristics,unitsize_atb,.csv,,,,,, +/inputs/plant_characteristics/upv_ATB_2023_advanced.csv,/inputs/plant_characteristics,upv_ATB_2023_advanced,.csv,"2023 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, +/inputs/plant_characteristics/upv_ATB_2023_conservative.csv,/inputs/plant_characteristics,upv_ATB_2023_conservative,.csv,"2023 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, +/inputs/plant_characteristics/upv_ATB_2023_moderate.csv,/inputs/plant_characteristics,upv_ATB_2023_moderate,.csv,"2023 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, +/inputs/plant_characteristics/upv_ATB_2024_advanced.csv,/inputs/plant_characteristics,upv_ATB_2024_advanced,.csv,"2024 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, +/inputs/plant_characteristics/upv_ATB_2024_conservative.csv,/inputs/plant_characteristics,upv_ATB_2024_conservative,.csv,"2024 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, +/inputs/plant_characteristics/upv_ATB_2024_moderate.csv,/inputs/plant_characteristics,upv_ATB_2024_moderate,.csv,"2024 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, +/inputs/plant_characteristics/years_until_endogenous.csv,/inputs/plant_characteristics,years_until_endogenous,.csv,,,,,, +/inputs/profiles_cf/cf_distpv_county.h5,/inputs/profiles_cf,cf_distpv_county,.h5,,,,,, +/inputs/profiles_cf/cf_upv_limited_ba.h5,/inputs/profiles_cf,cf_upv_limited_ba,.h5,,,,,, +/inputs/profiles_cf/cf_upv_limited_county.h5,/inputs/profiles_cf,cf_upv_limited_county,.h5,,,,,, +/inputs/profiles_cf/cf_upv_open_ba.h5,/inputs/profiles_cf,cf_upv_open_ba,.h5,,,,,, +/inputs/profiles_cf/cf_upv_open_county.h5,/inputs/profiles_cf,cf_upv_open_county,.h5,,,,,, +/inputs/profiles_cf/cf_upv_reference_ba.h5,/inputs/profiles_cf,cf_upv_reference_ba,.h5,,,,,, +/inputs/profiles_cf/cf_upv_reference_county.h5,/inputs/profiles_cf,cf_upv_reference_county,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_meshed_limited_ba.h5,/inputs/profiles_cf,cf_wind-ofs_meshed_limited_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_meshed_open_ba.h5,/inputs/profiles_cf,cf_wind-ofs_meshed_open_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_meshed_reference_ba.h5,/inputs/profiles_cf,cf_wind-ofs_meshed_reference_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_radial_limited_ba.h5,/inputs/profiles_cf,cf_wind-ofs_radial_limited_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_radial_limited_county.h5,/inputs/profiles_cf,cf_wind-ofs_radial_limited_county,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_radial_open_ba.h5,/inputs/profiles_cf,cf_wind-ofs_radial_open_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_radial_open_county.h5,/inputs/profiles_cf,cf_wind-ofs_radial_open_county,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_radial_reference_ba.h5,/inputs/profiles_cf,cf_wind-ofs_radial_reference_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ofs_radial_reference_county.h5,/inputs/profiles_cf,cf_wind-ofs_radial_reference_county,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ons_limited_ba.h5,/inputs/profiles_cf,cf_wind-ons_limited_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ons_limited_county.h5,/inputs/profiles_cf,cf_wind-ons_limited_county,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ons_open_ba.h5,/inputs/profiles_cf,cf_wind-ons_open_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ons_open_county.h5,/inputs/profiles_cf,cf_wind-ons_open_county,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ons_reference_ba.h5,/inputs/profiles_cf,cf_wind-ons_reference_ba,.h5,,,,,, +/inputs/profiles_cf/cf_wind-ons_reference_county.h5,/inputs/profiles_cf,cf_wind-ons_reference_county,.h5,,,,,, +/inputs/profiles_demand/demand_EER2023_100by2050.h5,/inputs/profiles_demand,demand_EER2023_100by2050,.h5,,,,,, +/inputs/profiles_demand/demand_EER2023_Baseline_AEO2022.h5,/inputs/profiles_demand,demand_EER2023_Baseline_AEO2022,.h5,,,,,, +/inputs/profiles_demand/demand_EER2023_IRAlow.h5,/inputs/profiles_demand,demand_EER2023_IRAlow,.h5,,,,,, +/inputs/profiles_demand/demand_EER2023_IRAmoderate.h5,/inputs/profiles_demand,demand_EER2023_IRAmoderate,.h5,,,,,, +/inputs/profiles_demand/demand_EER2025_100by2050.h5,/inputs/profiles_demand,demand_EER2025_100by2050,.h5,,,,,, +/inputs/profiles_demand/demand_EER2025_Baseline_AEO2023.h5,/inputs/profiles_demand,demand_EER2025_Baseline_AEO2023,.h5,,,,,, +/inputs/profiles_demand/demand_EER2025_IRAlow.h5,/inputs/profiles_demand,demand_EER2025_IRAlow,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_Baseline.h5,/inputs/profiles_demand,demand_EFS_Baseline,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_Clean2035.h5,/inputs/profiles_demand,demand_EFS_Clean2035,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_Clean2035_LTS.h5,/inputs/profiles_demand,demand_EFS_Clean2035_LTS,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_Clean2035clip1pct.h5,/inputs/profiles_demand,demand_EFS_Clean2035clip1pct,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_HIGH.h5,/inputs/profiles_demand,demand_EFS_HIGH,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_MEDIUM.h5,/inputs/profiles_demand,demand_EFS_MEDIUM,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_MEDIUMStretch2040.h5,/inputs/profiles_demand,demand_EFS_MEDIUMStretch2040,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_MEDIUMStretch2046.h5,/inputs/profiles_demand,demand_EFS_MEDIUMStretch2046,.h5,,,,,, +/inputs/profiles_demand/demand_EFS_REFERENCE.h5,/inputs/profiles_demand,demand_EFS_REFERENCE,.h5,,,,,, +/inputs/profiles_demand/demand_historic.h5,/inputs/profiles_demand,demand_historic,.h5,,,,,, +/inputs/remote/cf_distpv_county_18421977.h5,/inputs/remote,cf_distpv_county_18421977,.h5,,,,,, +/inputs/remote/cf_upv_limited_ba_18407660.h5,/inputs/remote,cf_upv_limited_ba_18407660,.h5,,,,,, +/inputs/remote/cf_upv_limited_county_18407660.h5,/inputs/remote,cf_upv_limited_county_18407660,.h5,,,,,, +/inputs/remote/cf_upv_open_ba_18407660.h5,/inputs/remote,cf_upv_open_ba_18407660,.h5,,,,,, +/inputs/remote/cf_upv_open_county_18407660.h5,/inputs/remote,cf_upv_open_county_18407660,.h5,,,,,, +/inputs/remote/cf_upv_reference_ba_18407660.h5,/inputs/remote,cf_upv_reference_ba_18407660,.h5,,,,,, +/inputs/remote/cf_upv_reference_county_18407660.h5,/inputs/remote,cf_upv_reference_county_18407660,.h5,,,,,, +/inputs/remote/cf_wind-ofs_meshed_limited_ba_18423723.h5,/inputs/remote,cf_wind-ofs_meshed_limited_ba_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_meshed_open_ba_18423723.h5,/inputs/remote,cf_wind-ofs_meshed_open_ba_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_meshed_reference_ba_18423723.h5,/inputs/remote,cf_wind-ofs_meshed_reference_ba_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_radial_limited_ba_18423723.h5,/inputs/remote,cf_wind-ofs_radial_limited_ba_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_radial_limited_county_18423723.h5,/inputs/remote,cf_wind-ofs_radial_limited_county_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_radial_open_ba_18423723.h5,/inputs/remote,cf_wind-ofs_radial_open_ba_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_radial_open_county_18423723.h5,/inputs/remote,cf_wind-ofs_radial_open_county_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_radial_reference_ba_18423723.h5,/inputs/remote,cf_wind-ofs_radial_reference_ba_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ofs_radial_reference_county_18423723.h5,/inputs/remote,cf_wind-ofs_radial_reference_county_18423723,.h5,,,,,, +/inputs/remote/cf_wind-ons_limited_ba_18422200.h5,/inputs/remote,cf_wind-ons_limited_ba_18422200,.h5,,,,,, +/inputs/remote/cf_wind-ons_limited_county_18422200.h5,/inputs/remote,cf_wind-ons_limited_county_18422200,.h5,,,,,, +/inputs/remote/cf_wind-ons_open_ba_18422200.h5,/inputs/remote,cf_wind-ons_open_ba_18422200,.h5,,,,,, +/inputs/remote/cf_wind-ons_open_county_18422200.h5,/inputs/remote,cf_wind-ons_open_county_18422200,.h5,,,,,, +/inputs/remote/cf_wind-ons_reference_ba_18422200.h5,/inputs/remote,cf_wind-ons_reference_ba_18422200,.h5,,,,,, +/inputs/remote/cf_wind-ons_reference_county_18422200.h5,/inputs/remote,cf_wind-ons_reference_county_18422200,.h5,,,,,, +/inputs/remote/demand_EER2023_100by2050_18423998.h5,/inputs/remote,demand_EER2023_100by2050_18423998,.h5,,,,,, +/inputs/remote/demand_EER2023_Baseline_AEO2022_18423998.h5,/inputs/remote,demand_EER2023_Baseline_AEO2022_18423998,.h5,,,,,, +/inputs/remote/demand_EER2023_IRAlow_18423998.h5,/inputs/remote,demand_EER2023_IRAlow_18423998,.h5,,,,,, +/inputs/remote/demand_EER2023_IRAmoderate_18423998.h5,/inputs/remote,demand_EER2023_IRAmoderate_18423998,.h5,,,,,, +/inputs/remote/demand_EER2025_100by2050_18435264.h5,/inputs/remote,demand_EER2025_100by2050_18435264,.h5,,,,,, +/inputs/remote/demand_EER2025_Baseline_AEO2023_18435264.h5,/inputs/remote,demand_EER2025_Baseline_AEO2023_18435264,.h5,,,,,, +/inputs/remote/demand_EER2025_IRAlow_18435264.h5,/inputs/remote,demand_EER2025_IRAlow_18435264,.h5,,,,,, +/inputs/remote/demand_EFS_Baseline_18461543.h5,/inputs/remote,demand_EFS_Baseline_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_Clean2035_18461543.h5,/inputs/remote,demand_EFS_Clean2035_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_Clean2035_LTS_18461543.h5,/inputs/remote,demand_EFS_Clean2035_LTS_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_Clean2035clip1pct_18461543.h5,/inputs/remote,demand_EFS_Clean2035clip1pct_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_HIGH_18461543.h5,/inputs/remote,demand_EFS_HIGH_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_MEDIUM_18461543.h5,/inputs/remote,demand_EFS_MEDIUM_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_MEDIUMStretch2040_18461543.h5,/inputs/remote,demand_EFS_MEDIUMStretch2040_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_MEDIUMStretch2046_18461543.h5,/inputs/remote,demand_EFS_MEDIUMStretch2046_18461543,.h5,,,,,, +/inputs/remote/demand_EFS_REFERENCE_18461543.h5,/inputs/remote,demand_EFS_REFERENCE_18461543,.h5,,,,,, +/inputs/remote/demand_historic_18462671.h5,/inputs/remote,demand_historic_18462671,.h5,,,,,, +/inputs/remote_files.csv,/inputs,remote_files,.csv,,,,,, +/inputs/reserves/ccseason_dates.csv,/inputs/reserves,ccseason_dates,.csv,,,,,, +/inputs/reserves/opres_periods.csv,/inputs/reserves,opres_periods,.csv,,,,,, +/inputs/reserves/orperc.csv,/inputs/reserves,orperc,.csv,,,,,, +/inputs/reserves/peak_net_imports.csv,/inputs/reserves,peak_net_imports,.csv,,,,,, +/inputs/reserves/prm_annual.csv,/inputs/reserves,prm_annual,.csv,Annual planning reserve margin by NERC region,,,,, +/inputs/reserves/ramptime.csv,/inputs/reserves,ramptime,.csv,,,,,, +/inputs/scalars.csv,/inputs,scalars,.csv,,,,,, +/inputs/sets/aclike.csv,/inputs/sets,aclike,.csv,set of AC transmission capacity types,,,,GAMS set, +/inputs/sets/allt.csv,/inputs/sets,allt,.csv,set of all potential years,,,,GAMS set, +/inputs/sets/bioclass.csv,/inputs/sets,bioclass,.csv,set of bio tech classes,,,,GAMS set, +/inputs/sets/ccsflex_cat.csv,/inputs/sets,ccsflex_cat,.csv,set of flexible ccs performance parameter categories,,,,GAMS set, +/inputs/sets/climate_param.csv,/inputs/sets,climate_param,.csv,set of parameters defined in climate_heuristics_finalyear,,,,GAMS set, +/inputs/sets/consumecat.csv,/inputs/sets,consumecat,.csv,set of categories for consuming facility characteristics,,,,GAMS set, +/inputs/sets/csapr_cat.csv,/inputs/sets,csapr_cat,.csv,set of CSAPR regulation categories,,,,GAMS set, +/inputs/sets/csapr_group.csv,/inputs/sets,csapr_group,.csv,set of CSAPR trading groups,,,,GAMS set, +/inputs/sets/ctt.csv,/inputs/sets,ctt,.csv,set of cooling technology types,,,,GAMS set, +/inputs/sets/e.csv,/inputs/sets,e,.csv,set of emission categories used in model,,,,GAMS set, +/inputs/sets/eall.csv,/inputs/sets,eall,.csv,set of emission categories used in reporting,,,,GAMS set, +/inputs/sets/etype.csv,/inputs/sets,etype,.csv,,,,,, +/inputs/sets/f.csv,/inputs/sets,f,.csv,set of fuel types,,,,GAMS set, +/inputs/sets/flex_type.csv,/inputs/sets,flex_type,.csv,set of demand flexibility types,,,,GAMS set, +/inputs/sets/fuel2tech.csv,/inputs/sets,fuel2tech,.csv,mapping between fuel types and generations,,,,GAMS set, +/inputs/sets/fuelbin.csv,/inputs/sets,fuelbin,.csv,set of gas usage brackets,,,,GAMS set, +/inputs/sets/gb.csv,/inputs/sets,gb,.csv,set of gas price bins,,,,GAMS set, +/inputs/sets/gbin.csv,/inputs/sets,gbin,.csv,set of growth bins,,,,GAMS set, +/inputs/sets/geotech.csv,/inputs/sets,geotech,.csv,set of geothermal technology categories,,,,GAMS set, +/inputs/sets/h2_st.csv,/inputs/sets,h2_st,.csv,defines investments needed to store and transport H2,,,,GAMS set, +/inputs/sets/h2_stor.csv,/inputs/sets,h2_stor,.csv,set of H2 storage options,,,,GAMS set, +/inputs/sets/hintage_char.csv,/inputs/sets,hintage_char,.csv,set of characteristics available in hintage_data,,,,GAMS set, +/inputs/sets/i.csv,/inputs/sets,i,.csv,set of technologies,,,,GAMS set, +/inputs/sets/i_geotech.csv,/inputs/sets,i_geotech,.csv,crosswalk between an individual geothermal technology and its category,,,,GAMS set, +/inputs/sets/i_h2_ptc_gen.csv,/inputs/sets,i_h2_ptc_gen,.csv,set of technologies which can produce energy for electrolyzers claiming the hydrogen production tax credit due to their low lifecycle carbon emissions,,,,GAMS set, +/inputs/sets/i_p.csv,/inputs/sets,i_p,.csv,mapping from technologies to the products they produce,,,,GAMS set, +/inputs/sets/i_subtech.csv,/inputs/sets,i_subtech,.csv,set of categories for subtechs,,,,GAMS set, +/inputs/sets/i_water_nocooling.csv,/inputs/sets,i_water_nocooling,.csv,"set of technologies that use water, but are not differentiated by cooling tech and water source",,,,GAMS set, +/inputs/sets/lcclike.csv,/inputs/sets,lcclike,.csv,set of transmission capacity types where lines are bundled with AC/DC converters,,,,GAMS set, +/inputs/sets/month.csv,/inputs/sets,month,.csv,,,,,GAMS set, +/inputs/sets/noretire.csv,/inputs/sets,noretire,.csv,set of technologies that will never be retired,,,,GAMS set, +/inputs/sets/notvsc.csv,/inputs/sets,notvsc,.csv,set of transmission capacity types that are not VSC,,,,GAMS set, +/inputs/sets/ofstype.csv,/inputs/sets,ofstype,.csv,set of offshore types used in offshore requirement constraint (eq_RPS_OFSWind),,,,GAMS set, +/inputs/sets/ofstype_i.csv,/inputs/sets,ofstype_i,.csv,crosswalk between ofstype and i,,,,GAMS set, +/inputs/sets/orcat.csv,/inputs/sets,orcat,.csv,set of operating reserve categories,,,,GAMS set, +/inputs/sets/ortype.csv,/inputs/sets,ortype,.csv,set of types of operating reserve constraints,,,,GAMS set, +/inputs/sets/p.csv,/inputs/sets,p,.csv,set of products produced,,,,GAMS set, +/inputs/sets/pcat.csv,/inputs/sets,pcat,.csv,set of prescribed technology categories,,,,GAMS set, +/inputs/sets/plantcat.csv,/inputs/sets,plantcat,.csv,set of categories for plant characteristics,,,,GAMS set, +/inputs/sets/prepost.csv,/inputs/sets,prepost,.csv,,,,,GAMS set, +/inputs/sets/prescriptivelink0.csv,/inputs/sets,prescriptivelink0,.csv,initial set of prescribed categories and their technologies - used in assigning prescribed builds,,,,GAMS set, +/inputs/sets/pvb_agg.csv,/inputs/sets,pvb_agg,.csv,crosswalk between hybrid pv+battery configurations and technology options,,,,GAMS set, +/inputs/sets/pvb_config.csv,/inputs/sets,pvb_config,.csv,set of hybrid pv+battery configurations,,,,GAMS set, +/inputs/sets/quarter.csv,/inputs/sets,quarter,.csv,,,,,GAMS set, +/inputs/sets/resourceclass.csv,/inputs/sets,resourceclass,.csv,set of renewable resource classes,,,,GAMS set, +/inputs/sets/RPSCat.csv,/inputs/sets,RPSCat,.csv,"set of RPS constraint categories, including clean energy standards",,,,GAMS set, +/inputs/sets/sc_cat.csv,/inputs/sets,sc_cat,.csv,set of supply curve categories (capacity and cost),,,,GAMS set, +/inputs/sets/sdbin.csv,/inputs/sets,sdbin,.csv,set of storage durage bins,,,,GAMS set, +/inputs/sets/sw.csv,/inputs/sets,sw,.csv,set of surface water types where access is based on consumption not withdrawal,,,,GAMS set, +/inputs/sets/tg.csv,/inputs/sets,tg,.csv,set of technology groups,,,,GAMS set, +/inputs/sets/tg_rsc_cspagg.csv,/inputs/sets,tg_rsc_cspagg,.csv,set of csp technologies that belong to the same class,,,,GAMS set, +/inputs/sets/tg_rsc_upvagg.csv,/inputs/sets,tg_rsc_upvagg,.csv,set of pv and pvb technologies that belong to the same class,,,,GAMS set, +/inputs/sets/trancap_fut_cat.csv,/inputs/sets,trancap_fut_cat,.csv,set of categories of near-term transmission projects that describe the likelihood of being completed,,,,GAMS set, +/inputs/sets/trtype.csv,/inputs/sets,trtype,.csv,set of transmission capacity types,,,,GAMS set, +/inputs/sets/unitspec_upgrades.csv,/inputs/sets,unitspec_upgrades,.csv,set of upgraded technologies that get unit-specific characteristics,,,,GAMS set, +/inputs/sets/upgrade_hintage_char.csv,/inputs/sets,upgrade_hintage_char,.csv,set to operate over in extension of hintage_data characteristics when sw_upgrades = 1,,,,GAMS set, +/inputs/sets/w.csv,/inputs/sets,w,.csv,set of water withdrawal or consumption options for water techs,,,,GAMS set, +/inputs/sets/wst.csv,/inputs/sets,wst,.csv,set of water source types,,,,GAMS set, +/inputs/sets/wst_climate.csv,/inputs/sets,wst_climate,.csv,set of water sources affected by climate change,,,,GAMS set, +/inputs/sets/yearafter.csv,/inputs/sets,yearafter,.csv,set to loop over for the final year calculation,,,,GAMS set, +/inputs/shapefiles/state_fips_codes.csv,/inputs/shapefiles,state_fips_codes,.csv,Mapping of states to FIPS codes and postcal code abbreviations,,,,, +/inputs/state_policies/acp_disallowed.csv,/inputs/state_policies,acp_disallowed,.csv,List of states which do not allow alternative compliance payments in place of meeting RPS or CES requirements ,,,,, +/inputs/state_policies/acp_prices.csv,/inputs/state_policies,acp_prices,.csv,,,,,, +/inputs/state_policies/ces_fraction.csv,/inputs/state_policies,ces_fraction,.csv,Annual compliance for states with a CES policy,,,,, +/inputs/state_policies/forced_retirements.csv,/inputs/state_policies,forced_retirements,.csv,List of regions with mandatory retirement policies for certain technologies,,,,, +/inputs/state_policies/hydrofrac_policy.csv,/inputs/state_policies,hydrofrac_policy,.csv,,,,,, +/inputs/state_policies/ng_crf_penalty_st.csv,/inputs/state_policies,ng_crf_penalty_st,.csv,Cost adjustment for NG techs in states where all NG techs must be retired by a certain year,"allt,st",N/A,https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220,Inputs,rate (unitless) +/inputs/state_policies/nuclear_subsidies.csv,/inputs/state_policies,nuclear_subsidies,.csv,,,,,, +/inputs/state_policies/offshore_req_default.csv,/inputs/state_policies,offshore_req_default,.csv,"default state mandates of offshore wind capacity, updated in November 2025","st,allt",,,Inputs,MW +/inputs/state_policies/oosfrac.csv,/inputs/state_policies,oosfrac,.csv,Defines the fraction of renewable and clean energy credits can be purchased from out of state (oos). Applied for RPS and CES,,,,, +/inputs/state_policies/recstyle.csv,/inputs/state_policies,recstyle,.csv,"Indication for how to apply state requirement (0 = end-use sales, 1 = bus-bar sales, 2 = generation). Default is 0.",,,,, +/inputs/state_policies/rectable.csv,/inputs/state_policies,rectable,.csv,Table defining which states are allowed to trade RECs,,,,, +/inputs/state_policies/rps_fraction.csv,/inputs/state_policies,rps_fraction,.csv,Indicates what fraction of sales or generation (based on recstyle.csv) must be from renewable energy ,,,,, +/inputs/state_policies/storage_mandates.csv,/inputs/state_policies,storage_mandates,.csv,Energy storage mandates by region,,,,, +/inputs/state_policies/techs_banned_ces.csv,/inputs/state_policies,techs_banned_ces,.csv,Indicates which technolgies are not eligible to contribute to CES ,,,,, +/inputs/state_policies/techs_banned_imports_rps.csv,/inputs/state_policies,techs_banned_imports_rps,.csv,,,,,, +/inputs/state_policies/techs_banned_rps.csv,/inputs/state_policies,techs_banned_rps,.csv,Indicates which technolgies are not eligible to contribute to RPS,,,,, +/inputs/state_policies/unbundled_limit_ces.csv,/inputs/state_policies,unbundled_limit_ces,.csv,Limit on fraction of credits towards CES which can be purchased unbundled from other states ,,,,, +/inputs/state_policies/unbundled_limit_rps.csv,/inputs/state_policies,unbundled_limit_rps,.csv,Limit on fraction of credits towards RPS which can be purchased unbundled from other states ,,,,, +/inputs/storage/cap_existing_psh.csv,/inputs/storage,cap_existing_psh,.csv,"County-wide PSH operational capacity, pump capacity, and max energy, based on plant-level data from https://www.hydropower.org/hydropower-pumped-storage-tool",,,,,MW/MWh +/inputs/storage/PSH_supply_curves_durations.csv,/inputs/storage,PSH_supply_curves_durations,.csv,,,,,, +/inputs/storage/storage_duration.csv,/inputs/storage,storage_duration,.csv,,,,,, +/inputs/supply_curve/bio_supplycurve.csv,/inputs/supply_curve,bio_supplycurve,.csv,Regional biomass supply and costs by resource class,,2015,,, +/inputs/supply_curve/dollaryear.csv,/inputs/supply_curve,dollaryear,.csv,,,,,, +/inputs/supply_curve/dr_shed_cap.csv,/inputs/supply_curve,dr_shed_cap,.csv,,,,,, +/inputs/supply_curve/dr_shed_cost.csv,/inputs/supply_curve,dr_shed_cost,.csv,,,,,, +/inputs/supply_curve/hyd_add_upg_cap.csv,/inputs/supply_curve,hyd_add_upg_cap,.csv,,,,,, +/inputs/supply_curve/hydcap.csv,/inputs/supply_curve,hydcap,.csv,,,,,, +/inputs/supply_curve/hydcost.csv,/inputs/supply_curve,hydcost,.csv,,,,,, +/inputs/supply_curve/interconnection_land.h5,/inputs/supply_curve,interconnection_land,.h5,,,,,, +/inputs/supply_curve/interconnection_offshore.h5,/inputs/supply_curve,interconnection_offshore,.h5,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_ref_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_ref_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wEphemeral_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_ref_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_ref_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wEphemeral_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_ref_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_ref_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wEphemeral_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_ref_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_ref_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wEphemeral_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_wEph_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_ref_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_ref_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wEphemeral_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_wEph_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_ref_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_ref_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wEphemeral_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_wEph_apr2025,.csv,,,,,, +/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_wEph_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, +/inputs/supply_curve/rev_paths.csv,/inputs/supply_curve,rev_paths,.csv,,,,,, +/inputs/supply_curve/sc_point_gid_old2new.csv,/inputs/supply_curve,sc_point_gid_old2new,.csv,,,,,, +/inputs/supply_curve/sitemap.h5,/inputs/supply_curve,sitemap,.h5,,,,,, +/inputs/supply_curve/supplycurve_egs-reference.csv,/inputs/supply_curve,supplycurve_egs-reference,.csv,,,,,, +/inputs/supply_curve/supplycurve_upv-limited.csv,/inputs/supply_curve,supplycurve_upv-limited,.csv,UPV supply curve from reV for the limited siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,,capacity numbers are in MW_DC and cost numbers are in $/MW_AC +/inputs/supply_curve/supplycurve_upv-open.csv,/inputs/supply_curve,supplycurve_upv-open,.csv,UPV supply curve from reV for the open siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,,capacity numbers are in MW_DC and cost numbers are in $/MW_AC +/inputs/supply_curve/supplycurve_upv-reference.csv,/inputs/supply_curve,supplycurve_upv-reference,.csv,UPV supply curve from reV for the reference siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,,capacity numbers are in MW_DC and cost numbers are in $/MW_AC +/inputs/supply_curve/supplycurve_wind-ofs-limited.csv,/inputs/supply_curve,supplycurve_wind-ofs-limited,.csv,Offshore sind supply curve from reV for the limited siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, +/inputs/supply_curve/supplycurve_wind-ofs-open.csv,/inputs/supply_curve,supplycurve_wind-ofs-open,.csv,Offshore wind supply curve from reV for the open siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, +/inputs/supply_curve/supplycurve_wind-ofs-reference.csv,/inputs/supply_curve,supplycurve_wind-ofs-reference,.csv,Offshore wind supply curve from reV for the reference siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, +/inputs/supply_curve/supplycurve_wind-ons-limited.csv,/inputs/supply_curve,supplycurve_wind-ons-limited,.csv,Land-based wind supply curve from reV for the limited siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, +/inputs/supply_curve/supplycurve_wind-ons-open.csv,/inputs/supply_curve,supplycurve_wind-ons-open,.csv,Land-based wind supply curve from reV for the open siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, +/inputs/supply_curve/supplycurve_wind-ons-reference.csv,/inputs/supply_curve,supplycurve_wind-ons-reference,.csv,Land-based wind supply curve from reV for the reference siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, +/inputs/supply_curve/trans_intra_cost_adder.csv,/inputs/supply_curve,trans_intra_cost_adder,.csv,,,,,, +/inputs/tech-subset-table.csv,/inputs,tech-subset-table,.csv,Maps all technologies to specific subsets of technologies,,,,, +/inputs/techs/tech_resourceclass.csv,/inputs/techs,tech_resourceclass,.csv,,,,,, +/inputs/techs/techs_default.csv,/inputs/techs,techs_default,.csv,List of technologies to be used in the model,,,,, +/inputs/techs/techs_subsetForTesting.csv,/inputs/techs,techs_subsetForTesting,.csv,Short list of technologies for testing,,,,, +/inputs/temporal/month2quarter.csv,/inputs/temporal,month2quarter,.csv,,,,,, +/inputs/temporal/period_szn_user.csv,/inputs/temporal,period_szn_user,.csv,,,,,, +/inputs/temporal/reeds_region_tz_map.csv,/inputs/temporal,reeds_region_tz_map,.csv,,,,,, +/inputs/temporal/stressperiods_user.csv,/inputs/temporal,stressperiods_user,.csv,,,,,, +/inputs/transmission/cost_hurdle_country.csv,/inputs/transmission,cost_hurdle_country,.csv,Cost for transmission hurdle rate by country,country,2004,,GAMS set, +/inputs/transmission/cost_hurdle_intra.csv,/inputs/transmission,cost_hurdle_intra,.csv,,,,,, +/inputs/transmission/rev_transmission_basecost.csv,/inputs/transmission,rev_transmission_basecost,.csv,Unweighted average base cost across the four regions for which we have transmission cost data.,Transreg,2004,,inputs, +/inputs/transmission/transmission_capacity_future_ba_baseline.csv,/inputs/transmission,transmission_capacity_future_ba_baseline,.csv,Future transmission capacity additions for the baseline case at BA resolution,"r,rr",,,inputs, +/inputs/transmission/transmission_capacity_future_ba_default.csv,/inputs/transmission,transmission_capacity_future_ba_default,.csv,Future transmission capacity additions for the default case at BA resolution,"r,rr",,,inputs, +/inputs/transmission/transmission_capacity_future_ba_LCC_all.csv,/inputs/transmission,transmission_capacity_future_ba_LCC_all,.csv,Future transmission capacity additions for the LCC_all case at BA resolution,"r,rr",,,inputs, +/inputs/transmission/transmission_capacity_future_ba_VSC_all.csv,/inputs/transmission,transmission_capacity_future_ba_VSC_all,.csv,Future transmission capacity additions for the VSC_all_case at BA resolution,"r,rr",,,inputs, +/inputs/transmission/transmission_capacity_future_county_baseline.csv,/inputs/transmission,transmission_capacity_future_county_baseline,.csv,Future transmission capacity additions for the baseline case at county resolution,"r,rr",,,inputs, +/inputs/transmission/transmission_capacity_future_county_default.csv,/inputs/transmission,transmission_capacity_future_county_default,.csv,Future transmission capacity additions for the default case at county resolution,"r,rr",,,inputs, +/inputs/transmission/transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629.csv,/inputs/transmission,transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629,.csv,Future transmission capacity additions for the LCC_1000miles_demand1_wind1_subferc_20230629 case at BA resolution,"r,rr",,,inputs, +/inputs/transmission/transmission_cost_ac_500kv_ba.h5,/inputs/transmission,transmission_cost_ac_500kv_ba,.h5,Transmission costs for new 500 kV AC at BA resolution,,,,, +/inputs/transmission/transmission_cost_ac_500kv_county.h5,/inputs/transmission,transmission_cost_ac_500kv_county,.h5,Transmission costs for new 500 kV AC at county resolution,,,,, +/inputs/transmission/transmission_cost_dc_ba.csv,/inputs/transmission,transmission_cost_dc_ba,.csv,Transmission costs for new 500 kV DC at BA resolution,,,,, +/inputs/transmission/transmission_cost_dc_county.csv,/inputs/transmission,transmission_cost_dc_county,.csv,Transmission costs for new 500 kV DC at county resolution,,,,, +/inputs/transmission/transmission_distance_ba.h5,/inputs/transmission,transmission_distance_ba,.h5,Length of least-cost transmission paths between zones at BA resolution,,,,, +/inputs/transmission/transmission_distance_county.h5,/inputs/transmission,transmission_distance_county,.h5,Length of least-cost transmission paths between zones at county resolution,,,,, +/inputs/upgrades/i_coolingtech_watersource_upgrades.csv,/inputs/upgrades,i_coolingtech_watersource_upgrades,.csv,List of cooling technologies for water sources that can be upgraded.,i,N/A,N/A,Inputs, +/inputs/upgrades/i_coolingtech_watersource_upgrades_link.csv,/inputs/upgrades,i_coolingtech_watersource_upgrades_link,.csv,"List of cooling technologies for water sources that can be upgraded + their to, from, ctt (cooling technology type) and wst (water source type)","i, ctt, wst",N/A,N/A,Inputs, +/inputs/upgrades/upgrade_costs_ccs_coal.csv,/inputs/upgrades,upgrade_costs_ccs_coal,.csv,,,,,, +/inputs/upgrades/upgrade_costs_ccs_gas.csv,/inputs/upgrades,upgrade_costs_ccs_gas,.csv,,,,,, +/inputs/upgrades/upgrade_link.csv,/inputs/upgrades,upgrade_link,.csv,"Techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta.",i,N/A,N/A,Inputs, +/inputs/upgrades/upgrade_mult_atb23_ccs_adv.csv,/inputs/upgrades,upgrade_mult_atb23_ccs_adv,.csv,Cost adjustment (advanced) over various years for upgrade technologies,"i,t",N/A,N/A,Inputs, +/inputs/upgrades/upgrade_mult_atb23_ccs_con.csv,/inputs/upgrades,upgrade_mult_atb23_ccs_con,.csv,Cost adjustment (conservative) over various years for upgrade technologies,"i,t",N/A,N/A,Inputs, +/inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv,/inputs/upgrades,upgrade_mult_atb23_ccs_mid,.csv,Cost adjustment (Mid) over various years for upgrade technologies,"i,t",N/A,N/A,Inputs, +/inputs/upgrades/upgradelink_water.csv,/inputs/upgrades,upgradelink_water,.csv,"Water techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta",i,N/A,N/A,Inputs, +/inputs/userinput/futurefiles.csv,/inputs/userinput,futurefiles,.csv,,,,,, +/inputs/userinput/ivt_default.csv,/inputs/userinput,ivt_default,.csv,,,,,, +/inputs/userinput/ivt_small.csv,/inputs/userinput,ivt_small,.csv,,,,,, +/inputs/userinput/ivt_step.csv,/inputs/userinput,ivt_step,.csv,ivt steps for endyears beyond 2050,,,,, +/inputs/userinput/modeled_regions.csv,/inputs/userinput,modeled_regions,.csv,Sets of BA regions that a user can model in a run. Each column is a different region option and can be specified in cases using GSw_Region.,,,,, +/inputs/userinput/windows_2100.csv,/inputs/userinput,windows_2100,.csv,Window size for using window solve method to 2100,,,,, +/inputs/userinput/windows_default.csv,/inputs/userinput,windows_default,.csv,Window size for using window solve method,,,,, +/inputs/userinput/windows_step10.csv,/inputs/userinput,windows_step10,.csv,Window size for beyond2050step10,,,,, +/inputs/userinput/windows_step5.csv,/inputs/userinput,windows_step5,.csv,Window size for beyond2050step5,,,,, +/inputs/valuestreams/var_map.csv,/inputs/valuestreams,var_map,.csv,,,,,, +/inputs/waterclimate/cost_cap_mult.csv,/inputs/waterclimate,cost_cap_mult,.csv,,,,,, +/inputs/waterclimate/cost_vom_mult.csv,/inputs/waterclimate,cost_vom_mult,.csv,,,,,, +/inputs/waterclimate/heat_rate_mult.csv,/inputs/waterclimate,heat_rate_mult,.csv,,,,,, +/inputs/waterclimate/i_coolingtech_watersource.csv,/inputs/waterclimate,i_coolingtech_watersource,.csv,,,,,, +/inputs/waterclimate/i_coolingtech_watersource_link.csv,/inputs/waterclimate,i_coolingtech_watersource_link,.csv,,,,,, +/inputs/waterclimate/tg_rsc_cspagg_tmp.csv,/inputs/waterclimate,tg_rsc_cspagg_tmp,.csv,,,,,, +/inputs/waterclimate/unapp_water_sea_distr.csv,/inputs/waterclimate,unapp_water_sea_distr,.csv,,,,,, +/inputs/waterclimate/wat_access_cap_cost.csv,/inputs/waterclimate,wat_access_cap_cost,.csv,,,,,, +/inputs/waterclimate/water_req_psh_10h_1_51.csv,/inputs/waterclimate,water_req_psh_10h_1_51,.csv,,,,,, +/inputs/waterclimate/water_with_cons_rate.csv,/inputs/waterclimate,water_with_cons_rate,.csv,,,,,, +/postprocessing/air_quality/rcm_data/counties_ACS_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,counties_ACS_high_stack_2017,.csv,,,,,, +/postprocessing/air_quality/rcm_data/counties_H6C_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,counties_H6C_high_stack_2017,.csv,,,,,, +/postprocessing/air_quality/rcm_data/states_ACS_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,states_ACS_high_stack_2017,.csv,,,,,, +/postprocessing/air_quality/rcm_data/states_H6C_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,states_H6C_high_stack_2017,.csv,,,,,, +/postprocessing/air_quality/scenarios.csv,/postprocessing/air_quality,scenarios,.csv,,,,,, +/postprocessing/bokehpivot/in/example_custom_styles.csv,/postprocessing/bokehpivot/in,example_custom_styles,.csv,Examples of custom styles used for bokehpivot,,,,, +/postprocessing/bokehpivot/in/example_data_US_electric_power_generation.csv,/postprocessing/bokehpivot/in,example_data_US_electric_power_generation,.csv,Example data for US electric power generation,,,,, +/postprocessing/bokehpivot/in/gis_centroid_rb.csv,/postprocessing/bokehpivot/in,gis_centroid_rb,.csv,,,,,, +/postprocessing/bokehpivot/in/gis_nercr.csv,/postprocessing/bokehpivot/in,gis_nercr,.csv,,,,,, +/postprocessing/bokehpivot/in/gis_nercr_new.csv,/postprocessing/bokehpivot/in,gis_nercr_new,.csv,,,,,, +/postprocessing/bokehpivot/in/gis_rb.csv,/postprocessing/bokehpivot/in,gis_rb,.csv,,,,,, +/postprocessing/bokehpivot/in/gis_rs.csv,/postprocessing/bokehpivot/in,gis_rs,.csv,,,,,, +/postprocessing/bokehpivot/in/gis_rto.csv,/postprocessing/bokehpivot/in,gis_rto,.csv,,,,,, +/postprocessing/bokehpivot/in/gis_st.csv,/postprocessing/bokehpivot/in,gis_st,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/class_map.csv,/postprocessing/bokehpivot/in/reeds2,class_map,.csv,Class mapping for bokehpivot postprocessing,,,,, +/postprocessing/bokehpivot/in/reeds2/class_style.csv,/postprocessing/bokehpivot/in/reeds2,class_style,.csv,Custom styles for classes in bokehpivot ,,,,, +/postprocessing/bokehpivot/in/reeds2/con_adj_map.csv,/postprocessing/bokehpivot/in/reeds2,con_adj_map,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/con_adj_style.csv,/postprocessing/bokehpivot/in/reeds2,con_adj_style,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/cost_cat_map.csv,/postprocessing/bokehpivot/in/reeds2,cost_cat_map,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/cost_cat_style.csv,/postprocessing/bokehpivot/in/reeds2,cost_cat_style,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/ctt_map.csv,/postprocessing/bokehpivot/in/reeds2,ctt_map,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/ctt_style.csv,/postprocessing/bokehpivot/in/reeds2,ctt_style,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/hours.csv,/postprocessing/bokehpivot/in/reeds2,hours,.csv,Hours for each of the 17 timeslices,,,,, +/postprocessing/bokehpivot/in/reeds2/m_bar_width.csv,/postprocessing/bokehpivot/in/reeds2,m_bar_width,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/m_map.csv,/postprocessing/bokehpivot/in/reeds2,m_map,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/m_style.csv,/postprocessing/bokehpivot/in/reeds2,m_style,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/process_style.csv,/postprocessing/bokehpivot/in/reeds2,process_style,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/tech_ctt_wst.csv,/postprocessing/bokehpivot/in/reeds2,tech_ctt_wst,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/tech_map.csv,/postprocessing/bokehpivot/in/reeds2,tech_map,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/tech_style.csv,/postprocessing/bokehpivot/in/reeds2,tech_style,.csv,Custom colors for each technology used by bokehpivot,,,,, +/postprocessing/bokehpivot/in/reeds2/trtype_map.csv,/postprocessing/bokehpivot/in/reeds2,trtype_map,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/trtype_style.csv,/postprocessing/bokehpivot/in/reeds2,trtype_style,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/wst_map.csv,/postprocessing/bokehpivot/in/reeds2,wst_map,.csv,,,,,, +/postprocessing/bokehpivot/in/reeds2/wst_style.csv,/postprocessing/bokehpivot/in/reeds2,wst_style,.csv,,,,,, +/postprocessing/bokehpivot/in/state_code.csv,/postprocessing/bokehpivot/in,state_code,.csv,Abbreviation and code for each state,,,,, +/postprocessing/bokehpivot/reeds_scenarios.csv,/postprocessing/bokehpivot,reeds_scenarios,.csv,"Example data for ReEDS scenarios, each scenario with a custom style ",,,,, +/postprocessing/combine_runs/combinefiles.csv,/postprocessing/combine_runs,combinefiles,.csv,,,,,, +/postprocessing/combine_runs/runlist.csv,/postprocessing/combine_runs,runlist,.csv,,,,,, +/postprocessing/example.csv,/postprocessing,example,.csv,,,,,, +/postprocessing/land_use/inputs/federal_land_categories.csv,/postprocessing/land_use/inputs,federal_land_categories,.csv,,,,,, +/postprocessing/land_use/inputs/field_definitions.csv,/postprocessing/land_use/inputs,field_definitions,.csv,,,,,, +/postprocessing/land_use/inputs/nlcd_categories.csv,/postprocessing/land_use/inputs,nlcd_categories,.csv,,,,,, +/postprocessing/land_use/inputs/nlcd_combined_categories.csv,/postprocessing/land_use/inputs,nlcd_combined_categories,.csv,,,,,, +/postprocessing/land_use/inputs/usgs_categories.csv,/postprocessing/land_use/inputs,usgs_categories,.csv,,,,,, +/postprocessing/land_use/inputs/usgs_combined_categories.csv,/postprocessing/land_use/inputs,usgs_combined_categories,.csv,,,,,, +/postprocessing/plots/scghg_annual.csv,/postprocessing/plots,scghg_annual,.csv,,,,,, +/postprocessing/plots/transmission-interface-coords.csv,/postprocessing/plots,transmission-interface-coords,.csv,,,,,, +/postprocessing/retail_rate_module/calc_historical_capex/existing_transmission_cost_bystate_USD2024.csv,/postprocessing/retail_rate_module/calc_historical_capex,existing_transmission_cost_bystate_USD2024,.csv,,,,,, +/postprocessing/retail_rate_module/capital_financing_assumptions.csv,/postprocessing/retail_rate_module,capital_financing_assumptions,.csv,,,,,, +/postprocessing/retail_rate_module/df_f861_contiguous.csv,/postprocessing/retail_rate_module,df_f861_contiguous,.csv,,,,,, +/postprocessing/retail_rate_module/df_f861_state.csv,/postprocessing/retail_rate_module,df_f861_state,.csv,,,,,, +/postprocessing/retail_rate_module/inputs.csv,/postprocessing/retail_rate_module,inputs,.csv,,,,,, +/postprocessing/retail_rate_module/inputs/Electric O & M Expenses-IOU-1993-2019.csv,/postprocessing/retail_rate_module/inputs,Electric O & M Expenses-IOU-1993-2019,.csv,values taken from FERC Form 1 -- see https://docs.nrel.gov/docs/fy22osti/78224.pdf sections 2.2.2 and 2.2.3,,,,, +/postprocessing/retail_rate_module/inputs/Electric Operating Revenues-IOU-1993-2019.csv,/postprocessing/retail_rate_module/inputs,Electric Operating Revenues-IOU-1993-2019,.csv,values taken from FERC Form 1 -- see https://docs.nrel.gov/docs/fy22osti/78224.pdf sections 2.2.2 and 2.2.3,,,,, +/postprocessing/retail_rate_module/inputs/Electric Plant in Service-IOU-1993-2019.csv,/postprocessing/retail_rate_module/inputs,Electric Plant in Service-IOU-1993-2019,.csv,values taken from FERC Form 1 -- see https://docs.nrel.gov/docs/fy22osti/78224.pdf sections 2.2.2 and 2.2.3,,,,, +/postprocessing/retail_rate_module/inputs/f861_cust_counts.csv,/postprocessing/retail_rate_module/inputs,f861_cust_counts,.csv,,,,,, +/postprocessing/retail_rate_module/inputs/overwrite-utility-energy_sales.csv,/postprocessing/retail_rate_module/inputs,overwrite-utility-energy_sales,.csv,,,,,, +/postprocessing/retail_rate_module/inputs/state-meanbiaserror_rate-aggregation.csv,/postprocessing/retail_rate_module/inputs,state-meanbiaserror_rate-aggregation,.csv,,,,,, +/postprocessing/retail_rate_module/inputs/Table_9.8_Average_Retail_Prices_of_Electricity.xlsx,/postprocessing/retail_rate_module/inputs,Table_9.8_Average_Retail_Prices_of_Electricity,.xlsx,Historical EIA861 rates (annual and monthly),,,,, +/postprocessing/retail_rate_module/inputs_default.csv,/postprocessing/retail_rate_module,inputs_default,.csv,,,,,, +/postprocessing/retail_rate_module/load_by_state_eia.csv,/postprocessing/retail_rate_module,load_by_state_eia,.csv,End use load by state since 1960,,,,, +/postprocessing/retail_rate_module/map_i_to_tech.csv,/postprocessing/retail_rate_module,map_i_to_tech,.csv,Maps i to tech with custom coloring for each,,,,, +/postprocessing/reValue/scenarios.csv,/postprocessing/reValue,scenarios,.csv,,,,,, +/postprocessing/tableau/tables_to_aggregate.csv,/postprocessing/tableau,tables_to_aggregate,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/batt_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,batt_plant_char_format,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/conv_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,conv_plant_char_format,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/csp_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,csp_plant_char_format,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/geo_fom_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,geo_fom_plant_char_format,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/h2-combustion_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,h2-combustion_plant_char_format,.csv,Plant characteristics for which the H2-CC and CT ATB estimates are made using Gas-CC and CT data in preprocessing,,,,, +/preprocessing/atb_updates_processing/input_files/ofs-wind_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,ofs-wind_plant_char_format,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/ofs-wind_rsc_mult_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,ofs-wind_rsc_mult_plant_char_format,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/ons-wind_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,ons-wind_plant_char_format,.csv,,,,,, +/preprocessing/atb_updates_processing/input_files/upv_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,upv_plant_char_format,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/cases_reeds2pras.csv,/reeds2pras/test/reeds_cases/test,cases_reeds2pras,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/hydcapadj.csv,/reeds2pras/test/reeds_cases/test/inputs_case,hydcapadj,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/hydcf.csv,/reeds2pras/test/reeds_cases/test/inputs_case,hydcf,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/mttr.csv,/reeds2pras/test/reeds_cases/test/inputs_case,mttr,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_hourly.h5,/reeds2pras/test/reeds_cases/test/inputs_case,outage_forced_hourly,.h5,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_static.csv,/reeds2pras/test/reeds_cases/test/inputs_case,outage_forced_static,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/outage_scheduled_hourly.h5,/reeds2pras/test/reeds_cases/test/inputs_case,outage_scheduled_hourly,.h5,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/resources.csv,/reeds2pras/test/reeds_cases/test/inputs_case,resources,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/tech-subset-table.csv,/reeds2pras/test/reeds_cases/test/inputs_case,tech-subset-table,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/unitdata.csv,/reeds2pras/test/reeds_cases/test/inputs_case,unitdata,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/inputs_case/unitsize.csv,/reeds2pras/test/reeds_cases/test/inputs_case,unitsize,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/meta.csv,/reeds2pras/test/reeds_cases/test,meta,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/cap_converter_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,cap_converter_2035,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/charge_eff_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,charge_eff_2035,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/discharge_eff_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,discharge_eff_2035,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/energy_cap_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,energy_cap_2035,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_cap_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,max_cap_2035,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_unitsize_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,max_unitsize_2035,.csv,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_load_2035.h5,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,pras_load_2035,.h5,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_vre_gen_2035.h5,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,pras_vre_gen_2035,.h5,,,,,, +/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/tran_cap_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,tran_cap_2035,.csv,,,,,, +/ReEDS_Augur/augur_switches.csv,/ReEDS_Augur,augur_switches,.csv,,,,,, +/runfiles.csv,/,runfiles,.csv,Contains the locations of input data that is copied from the repository into the runs folder for each respective case.,,,,, +/sources.csv,/,sources,.csv,"CSV file containing a list of all input files (csv, h5, csv.gz)",,,,, +/tests/data/county/csp.h5,/tests/data/county,csp,.h5,Subset of county-level data for the github runner county test,,,,, +/tests/data/county/distpv.h5,/tests/data/county,distpv,.h5,Subset of county-level data for the github runner county test,,,,, +/tests/data/county/upv.h5,/tests/data/county,upv,.h5,Subset of county-level data for the github runner county test,,,,, +/tests/data/county/wind-ofs.h5,/tests/data/county,wind-ofs,.h5,Subset of county-level data for the github runner county test,,,,, +/tests/data/county/wind-ons.h5,/tests/data/county,wind-ons,.h5,Subset of county-level data for the github runner county test,,,,, diff --git a/docs/sources_documentation.md b/docs/sources_documentation.md new file mode 100644 index 00000000..2604c5a5 --- /dev/null +++ b/docs/sources_documentation.md @@ -0,0 +1,4008 @@ +## Table of Contents + + + - [hourlize](#hourlize) + - [inputs](#hourlize-inputs) + - [load](#hourlize-inputs-load) + - [resource](#hourlize-inputs-resource) + - [tests](#hourlize-tests) + - [data](#hourlize-tests-data) + - [r2r_expanded](#hourlize-tests-data-r2r-expanded) + - [r2r_from_config](#hourlize-tests-data-r2r-from-config) + - [r2r_integration](#hourlize-tests-data-r2r-integration) + - [r2r_integration_geothermal](#hourlize-tests-data-r2r-integration-geothermal) + - [inputs](#inputs) + - [canada_imports](#inputs-canada-imports) + - [capacity_exogenous](#inputs-capacity-exogenous) + - [climate](#inputs-climate) + - [GFDL-ESM2M_RCP4p5_WM](#inputs-climate-gfdl-esm2m-rcp4p5-wm) + - [HadGEM2-ES_RCP2p6](#inputs-climate-hadgem2-es-rcp2p6) + - [HadGEM2-ES_rcp45_AT](#inputs-climate-hadgem2-es-rcp45-at) + - [HadGEM2-ES_RCP4p5](#inputs-climate-hadgem2-es-rcp4p5) + - [HadGEM2-ES_rcp85_AT](#inputs-climate-hadgem2-es-rcp85-at) + - [HadGEM2-ES_RCP8p5](#inputs-climate-hadgem2-es-rcp8p5) + - [IPSL-CM5A-LR_RCP8p5_WM](#inputs-climate-ipsl-cm5a-lr-rcp8p5-wm) + - [consume](#inputs-consume) + - [ctus](#inputs-ctus) + - [degradation](#inputs-degradation) + - [demand_response](#inputs-demand-response) + - [dgen_model_inputs](#inputs-dgen-model-inputs) + - [stscen2023_electrification](#inputs-dgen-model-inputs-stscen2023-electrification) + - [stscen2023_highng](#inputs-dgen-model-inputs-stscen2023-highng) + - [stscen2023_highre](#inputs-dgen-model-inputs-stscen2023-highre) + - [stscen2023_lowng](#inputs-dgen-model-inputs-stscen2023-lowng) + - [stscen2023_lowre](#inputs-dgen-model-inputs-stscen2023-lowre) + - [stscen2023_mid_case](#inputs-dgen-model-inputs-stscen2023-mid-case) + - [stscen2023_mid_case_95_by_2035](#inputs-dgen-model-inputs-stscen2023-mid-case-95-by-2035) + - [stscen2023_mid_case_95_by_2050](#inputs-dgen-model-inputs-stscen2023-mid-case-95-by-2050) + - [stscen2023_taxcredit_extended2050](#inputs-dgen-model-inputs-stscen2023-taxcredit-extended2050) + - [disaggregation](#inputs-disaggregation) + - [emission_constraints](#inputs-emission-constraints) + - [financials](#inputs-financials) + - [fuelprices](#inputs-fuelprices) + - [geothermal](#inputs-geothermal) + - [growth_constraints](#inputs-growth-constraints) + - [hydro](#inputs-hydro) + - [load](#inputs-load) + - [national_generation](#inputs-national-generation) + - [outages](#inputs-outages) + - [plant_characteristics](#inputs-plant-characteristics) + - [profiles_cf](#inputs-profiles-cf) + - [profiles_demand](#inputs-profiles-demand) + - [remote](#inputs-remote) + - [reserves](#inputs-reserves) + - [sets](#inputs-sets) + - [shapefiles](#inputs-shapefiles) + - [state_policies](#inputs-state-policies) + - [storage](#inputs-storage) + - [supply_curve](#inputs-supply-curve) + - [techs](#inputs-techs) + - [temporal](#inputs-temporal) + - [transmission](#inputs-transmission) + - [upgrades](#inputs-upgrades) + - [userinput](#inputs-userinput) + - [valuestreams](#inputs-valuestreams) + - [waterclimate](#inputs-waterclimate) + - [postprocessing](#postprocessing) + - [air_quality](#postprocessing-air-quality) + - [rcm_data](#postprocessing-air-quality-rcm-data) + - [bokehpivot](#postprocessing-bokehpivot) + - [in](#postprocessing-bokehpivot-in) + - [reeds2](#postprocessing-bokehpivot-in-reeds2) + - [combine_runs](#postprocessing-combine-runs) + - [land_use](#postprocessing-land-use) + - [inputs](#postprocessing-land-use-inputs) + - [plots](#postprocessing-plots) + - [retail_rate_module](#postprocessing-retail-rate-module) + - [calc_historical_capex](#postprocessing-retail-rate-module-calc-historical-capex) + - [inputs](#postprocessing-retail-rate-module-inputs) + - [reValue](#postprocessing-revalue) + - [tableau](#postprocessing-tableau) + - [preprocessing](#preprocessing) + - [atb_updates_processing](#preprocessing-atb-updates-processing) + - [input_files](#preprocessing-atb-updates-processing-input-files) + - [reeds2pras](#reeds2pras) + - [test](#reeds2pras-test) + - [reeds_cases](#reeds2pras-test-reeds-cases) + - [test](#reeds2pras-test-reeds-cases-test) + - [ReEDS_Augur](#reeds-augur) + - [tests](#tests) + - [data](#tests-data) + - [county](#tests-data-county) + + +## Input Files +- [cases.csv](/cases.csv) + - **File Type:** Switches file + - **Description:** Contains the configuration settings for the ReEDS run(s). + - **Dollar year:** 2004 + - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv](https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv) +--- + +- [cases_county.csv](/cases_county.csv) +--- + +- [cases_examples.csv](/cases_examples.csv) +--- + +- [cases_small.csv](/cases_small.csv) + - **Description:** Contains settings to run ReEDS at a smaller scale to test operability of the ReEDS model. Turns off several technologies and reduces the model size to significantly improve solve times. +--- + +- [cases_standardscenarios.csv](/cases_standardscenarios.csv) + - **File Type:** StdScen Cases file + - **Description:** Contains the configuration settings for the Standard Scenarios ReEDS runs. +--- + +- [cases_test.csv](/cases_test.csv) + - **Description:** Contains the configuration settings for doing test runs including the default Pacific census division test case. +--- + +- [e_report_params.csv](/e_report_params.csv) + - **Description:** Contains a parameter list used in the model along with descriptions of what they are and units used. +--- + +- [runfiles.csv](/runfiles.csv) + - **Description:** Contains the locations of input data that is copied from the repository into the runs folder for each respective case. +--- + +- [sources.csv](/sources.csv) + - **Description:** CSV file containing a list of all input files (csv, h5, csv.gz) +--- + + + +### hourlize + + + +#### hourlize/inputs + + + +##### hourlize/inputs/load + + - [dummy_agg_op_datacenters.csv](/hourlize/inputs/load/dummy_agg_op_datacenters.csv) +--- + + - [legacy_ba_state_map.csv](/hourlize/inputs/load/legacy_ba_state_map.csv) +--- + + - [legacy_ba_timezone.csv](/hourlize/inputs/load/legacy_ba_timezone.csv) +--- + + + +##### hourlize/inputs/resource + + - [egs_resource_classes.csv](/hourlize/inputs/resource/egs_resource_classes.csv) +--- + + - [geohydro_resource_classes.csv](/hourlize/inputs/resource/geohydro_resource_classes.csv) +--- + + - [offshore_zone_names.csv](/hourlize/inputs/resource/offshore_zone_names.csv) +--- + + - [rev_sc_columns.csv](/hourlize/inputs/resource/rev_sc_columns.csv) +--- + + - [state_abbrev.csv](/hourlize/inputs/resource/state_abbrev.csv) + - **Description:** Contains state names and codesfor the US. +--- + + - [upv_resource_classes.csv](/hourlize/inputs/resource/upv_resource_classes.csv) + - **Description:** Contains information related to UPV class segregation based on mean irradiance levels. +--- + + - [wind-ofs_resource_classes.csv](/hourlize/inputs/resource/wind-ofs_resource_classes.csv) + - **File Type:** supply curve input + - **Description:** Contains information related to Offshore wind class segregation and turbine type (fixed vs floating) based on water depth and site lcoe + - **Indices:** n/a +--- + + - [wind-ons_resource_classes.csv](/hourlize/inputs/resource/wind-ons_resource_classes.csv) + - **Description:** Contains information related to Onshore wind class segregation based on mean wind speeds. +--- + + + +#### hourlize/tests + + + +##### hourlize/tests/data + + + +###### hourlize/tests/data/r2r_expanded + + + +###### hourlize/tests/data/r2r_expanded/upv_case_1 + + + +###### hourlize/tests/data/r2r_expanded/upv_case_1/expected_results + + - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/expected_results/df_sc_out_upv_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_1/reeds + + + +###### hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case + + - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/hierarchy_original.csv) +--- + + - [maxage.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/maxage.csv) +--- + + - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/rev_paths.csv) +--- + + - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/site_bin_map.csv) +--- + + - [switches.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/switches.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs + + - [cap.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap.csv) +--- + + - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_exog.csv) +--- + + - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_bin_out.csv) +--- + + - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_ivrt_refurb.csv) +--- + + - [systemcost.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/systemcost.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves + + - [upv_supply_curve_raw_unpacked.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves/upv_supply_curve_raw_unpacked.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_2 + + + +###### hourlize/tests/data/r2r_expanded/upv_case_2/expected_results + + - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/expected_results/df_sc_out_upv_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_2/reeds + + + +###### hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case + + - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/hierarchy_original.csv) +--- + + - [maxage.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/maxage.csv) +--- + + - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/rev_paths.csv) +--- + + - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/site_bin_map.csv) +--- + + - [switches.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/switches.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs + + - [cap.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap.csv) +--- + + - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_exog.csv) +--- + + - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_bin_out.csv) +--- + + - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_ivrt_refurb.csv) +--- + + - [systemcost.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/systemcost.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves + + - [upv_supply_curve_raw_unpacked.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves/upv_supply_curve_raw_unpacked.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_3 + + + +###### hourlize/tests/data/r2r_expanded/upv_case_3/expected_results + + - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/expected_results/df_sc_out_upv_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_3/reeds + + + +###### hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case + + - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/hierarchy_original.csv) +--- + + - [maxage.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/maxage.csv) +--- + + - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/rev_paths.csv) +--- + + - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/site_bin_map.csv) +--- + + - [switches.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/switches.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs + + - [cap.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap.csv) +--- + + - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_exog.csv) +--- + + - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_bin_out.csv) +--- + + - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_ivrt_refurb.csv) +--- + + - [systemcost.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/systemcost.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves + + - [upv_supply_curve_raw_unpacked.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves/upv_supply_curve_raw_unpacked.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/wind-ons_case_1 + + + +###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results + + - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results/df_sc_out_wind-ons_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds + + + +###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case + + - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/hierarchy_original.csv) +--- + + - [maxage.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/maxage.csv) +--- + + - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/rev_paths.csv) +--- + + - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/site_bin_map.csv) +--- + + - [switches.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/switches.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata + + - [rev_supply_curves.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata/rev_supply_curves.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs + + - [cap.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap.csv) +--- + + - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_exog.csv) +--- + + - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_bin_out.csv) +--- + + - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_ivrt_refurb.csv) +--- + + - [systemcost.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/systemcost.csv) +--- + + + +###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves + + - [wind-ons_supply_curve_raw.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves/wind-ons_supply_curve_raw.csv) +--- + + + +###### hourlize/tests/data/r2r_from_config + + + +###### hourlize/tests/data/r2r_from_config/expected_results + + + +###### hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs + + - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_upv_reduced.csv) +--- + + - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ofs_reduced.csv) +--- + + - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ons_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint + + - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_upv_reduced.csv) +--- + + - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ofs_reduced.csv) +--- + + - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ons_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_from_config/expected_results/priority_inputs + + - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_upv_reduced.csv) +--- + + - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ofs_reduced.csv) +--- + + - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ons_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_integration + + + +###### hourlize/tests/data/r2r_integration/expected_results + + - [df_sc_out_upv.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv.csv) +--- + + - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced.csv) +--- + + - [df_sc_out_upv_reduced_simul_fill.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced_simul_fill.csv) +--- + + - [df_sc_out_wind-ofs.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs.csv) +--- + + - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs_reduced.csv) +--- + + - [df_sc_out_wind-ons.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons.csv) +--- + + - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_integration/reeds + + + +###### hourlize/tests/data/r2r_integration/reeds/inputs_case + + - [hierarchy_original.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/hierarchy_original.csv) +--- + + - [maxage.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/maxage.csv) +--- + + - [rev_paths.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/rev_paths.csv) +--- + + - [site_bin_map.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/site_bin_map.csv) +--- + + - [switches.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/switches.csv) +--- + + + +###### hourlize/tests/data/r2r_integration/reeds/outputs + + - [cap.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap.csv) +--- + + - [cap_exog.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap_exog.csv) +--- + + - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_bin_out.csv) +--- + + - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_ivrt_refurb.csv) +--- + + - [systemcost.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/systemcost.csv) +--- + + + +###### hourlize/tests/data/r2r_integration/supply_curves + + + +###### hourlize/tests/data/r2r_integration/supply_curves/upv_reference + + + +###### hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results + + - [upv_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results/upv_supply_curve_raw.csv) +--- + + + +###### hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate + + + +###### hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results + + - [wind-ofs_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results/wind-ofs_supply_curve_raw.csv) +--- + + + +###### hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference + + + +###### hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results + + - [wind-ons_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results/wind-ons_supply_curve_raw.csv) +--- + + + +###### hourlize/tests/data/r2r_integration_geothermal + + + +###### hourlize/tests/data/r2r_integration_geothermal/expected_results + + - [df_sc_out_egs_allkm.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm.csv) +--- + + - [df_sc_out_egs_allkm_reduced.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm_reduced.csv) +--- + + - [df_sc_out_geohydro_allkm.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm.csv) +--- + + - [df_sc_out_geohydro_allkm_reduced.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm_reduced.csv) +--- + + + +###### hourlize/tests/data/r2r_integration_geothermal/reeds + + + +###### hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case + + - [hierarchy_original.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/hierarchy_original.csv) +--- + + - [maxage.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/maxage.csv) +--- + + - [rev_paths.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/rev_paths.csv) +--- + + - [site_bin_map.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/site_bin_map.csv) +--- + + - [switches.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/switches.csv) +--- + + + +###### hourlize/tests/data/r2r_integration_geothermal/reeds/outputs + + - [cap.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap.csv) +--- + + - [cap_exog.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_exog.csv) +--- + + - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_bin_out.csv) +--- + + - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_ivrt_refurb.csv) +--- + + - [systemcost.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/systemcost.csv) +--- + + + +###### hourlize/tests/data/r2r_integration_geothermal/supply_curves + + - [egs_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration_geothermal/supply_curves/egs_supply_curve_raw.csv) +--- + + - [geohydro_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration_geothermal/supply_curves/geohydro_supply_curve_raw.csv) +--- + + + +### inputs + + - [county2zone.csv](/inputs/county2zone.csv) +--- + + - [hierarchy.csv](/inputs/hierarchy.csv) +--- + + - [hierarchy_agg125.csv](/inputs/hierarchy_agg125.csv) +--- + + - [hierarchy_agg54.csv](/inputs/hierarchy_agg54.csv) +--- + + - [hierarchy_agg69.csv](/inputs/hierarchy_agg69.csv) +--- + + - [hierarchy_offshore.csv](/inputs/zones/hierarchy_offshore.csv) +--- + + - [remote_files.csv](/inputs/remote_files.csv) +--- + + - [scalars.csv](/inputs/scalars.csv) +--- + + - [tech-subset-table.csv](/inputs/tech-subset-table.csv) + - **Description:** Maps all technologies to specific subsets of technologies +--- + + + +#### inputs/canada_imports + + - [can_exports.csv](/inputs/canada_imports/can_exports.csv) + - **File Type:** Input + - **Description:** Annual exports to Canada by BA + - **Indices:** r,t + - **Units:** MWh + +--- + + - [can_exports_szn_frac.csv](/inputs/canada_imports/can_exports_szn_frac.csv) + - **File Type:** Input + - **Description:** Fraction of annual exports to Canada by season + - **Indices:** N/A + - **Units:** rate (unitless) + +--- + + - [can_imports.csv](/inputs/canada_imports/can_imports.csv) + - **File Type:** Input + - **Description:** Annual imports from Canada by BA + - **Indices:** r,t + - **Units:** MWh + +--- + + - [can_imports_quarter_frac.csv](/inputs/canada_imports/can_imports_quarter_frac.csv) + - **File Type:** Input + - **Description:** Fraction of annual imports from Canada by season + - **Indices:** N/A + - **Units:** rate (unitless) + +--- + + + +#### inputs/capacity_exogenous + + - [cappayments.csv](/inputs/capacity_exogenous/cappayments.csv) +--- + + - [cappayments_ba.csv](/inputs/capacity_exogenous/cappayments_ba.csv) +--- + + - [demonstration_plants.csv](/inputs/capacity_exogenous/demonstration_plants.csv) + - **File Type:** Prescribed capacity + - **Description:** Nuclear-smr demonstration plants; active when GSw_NuclearDemo=1 + - **Indices:** t,r,i,coolingwatertech,ctt,wst,value + - **Citation:** See 'notes' column in the file and https://www.energy.gov/oced/advanced-reactor-demonstration-projects-0 + - **Units:** MW + +--- + + - [exog_cap_geohydro_allkm_reference.csv](/inputs/capacity_exogenous/exog_cap_geohydro_allkm_reference.csv) +--- + + - [exog_cap_geohydro_reference.csv](/inputs/capacity_exogenous/exog_cap_geohydro_reference.csv) +--- + + - [exog_cap_upv_limited.csv](/inputs/capacity_exogenous/exog_cap_upv_limited.csv) +--- + + - [exog_cap_upv_open.csv](/inputs/capacity_exogenous/exog_cap_upv_open.csv) +--- + + - [exog_cap_upv_reference.csv](/inputs/capacity_exogenous/exog_cap_upv_reference.csv) +--- + + - [exog_cap_wind-ons_limited.csv](/inputs/capacity_exogenous/exog_cap_wind-ons_limited.csv) +--- + + - [exog_cap_wind-ons_open.csv](/inputs/capacity_exogenous/exog_cap_wind-ons_open.csv) +--- + + - [exog_cap_wind-ons_reference.csv](/inputs/capacity_exogenous/exog_cap_wind-ons_reference.csv) +--- + + - [interconnection_queues.csv](/inputs/capacity_exogenous/interconnection_queues.csv) +--- + + - [prescribed_builds_wind-ofs_meshed_limited.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_limited.csv) +--- + + - [prescribed_builds_wind-ofs_meshed_open.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_open.csv) +--- + + - [prescribed_builds_wind-ofs_meshed_reference.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_reference.csv) +--- + + - [prescribed_builds_wind-ofs_radial_limited.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_limited.csv) +--- + + - [prescribed_builds_wind-ofs_radial_open.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_open.csv) +--- + + - [prescribed_builds_wind-ofs_radial_reference.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_reference.csv) +--- + + - [prescribed_builds_wind-ons_limited.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ons_limited.csv) +--- + + - [prescribed_builds_wind-ons_open.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ons_open.csv) +--- + + - [prescribed_builds_wind-ons_reference.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ons_reference.csv) +--- + + - [ReEDS_generator_database_final_EIA-NEMS.csv](/inputs/capacity_exogenous/ReEDS_generator_database_final_EIA-NEMS.csv) + - **File Type:** Input + - **Description:** EIA-NEMS database of existing generators +--- + + + +#### inputs/climate + + - [climate_heuristics_finalyear.csv](/inputs/climate/climate_heuristics_finalyear.csv) +--- + + - [climate_heuristics_yearfrac.csv](/inputs/climate/climate_heuristics_yearfrac.csv) +--- + + + + +##### inputs/climate/GFDL-ESM2M_RCP4p5_WM + + - [HDDCDD.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/HDDCDD.csv) +--- + + - [hydadjann.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjann.csv) + - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario + - **Indices:** r,t + - **Units:** multipliers (unitless) + +--- + + - [hydadjsea.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjsea.csv) + - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario + - **Indices:** r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMult.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMult.csv) + - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMultAnn.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMultAnn.csv) + - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario + - **Indices:** wst,r,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterSeaAnnDistr.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterSeaAnnDistr.csv) + - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the GFDL-ESM2M_RCP4p5_WM climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + + +##### inputs/climate/HadGEM2-ES_RCP2p6 + + - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_RCP2p6/HDDCDD.csv) +--- + + - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMult.csv) + - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMultAnn.csv) + - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario + - **Indices:** wst,r,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterSeaAnnDistr.csv) + - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP2p6 climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + + +##### inputs/climate/HadGEM2-ES_rcp45_AT + + - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/HDDCDD.csv) +--- + + - [hydadjann.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjann.csv) + - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario + - **Indices:** r,t + - **Units:** multipliers (unitless) + +--- + + - [hydadjsea.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjsea.csv) + - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario + - **Indices:** r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMult.csv) + - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMultAnn.csv) + - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario + - **Indices:** wst,r,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterSeaAnnDistr.csv) + - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp45_AT climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + + +##### inputs/climate/HadGEM2-ES_RCP4p5 + + - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_RCP4p5/HDDCDD.csv) +--- + + - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMult.csv) + - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMultAnn.csv) + - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario + - **Indices:** wst,r,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterSeaAnnDistr.csv) + - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP4p5 climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + + +##### inputs/climate/HadGEM2-ES_rcp85_AT + + - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/HDDCDD.csv) +--- + + - [hydadjann.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjann.csv) + - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario + - **Indices:** r,t + - **Units:** multipliers (unitless) + +--- + + - [hydadjsea.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjsea.csv) + - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario + - **Indices:** r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMult.csv) + - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMultAnn.csv) + - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario + - **Indices:** wst,r,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterSeaAnnDistr.csv) + - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp85_AT climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + + +##### inputs/climate/HadGEM2-ES_RCP8p5 + + - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_RCP8p5/HDDCDD.csv) +--- + + - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMult.csv) + - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMultAnn.csv) + - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario + - **Indices:** wst,r,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterSeaAnnDistr.csv) + - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP8p5 climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + + +##### inputs/climate/IPSL-CM5A-LR_RCP8p5_WM + + - [HDDCDD.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/HDDCDD.csv) +--- + + - [hydadjann.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjann.csv) + - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario + - **Indices:** r,t + - **Units:** multipliers (unitless) + +--- + + - [hydadjsea.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjsea.csv) + - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario + - **Indices:** r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMult.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMult.csv) + - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterMultAnn.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMultAnn.csv) + - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario + - **Indices:** wst,r,t + - **Units:** multipliers (unitless) + +--- + + - [UnappWaterSeaAnnDistr.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterSeaAnnDistr.csv) + - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the IPSL-CM5A-LR_RCP8p5_WM climate scenario + - **Indices:** wst,r,month,t + - **Units:** multipliers (unitless) + +--- + + + +#### inputs/consume + + - [consume_char_low.csv](/inputs/consume/consume_char_low.csv) + - **File Type:** Inputs + - **Description:** Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Conservative assumptions. + - **Indices:** i,t + - **Dollar year:** Units vary based on the parameter - see commented text in b_inputs.gms. + - **Citation:** N/A +--- + + - [consume_char_ref.csv](/inputs/consume/consume_char_ref.csv) + - **File Type:** Inputs + - **Description:** Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Reference assumptions. + - **Indices:** i,t + - **Dollar year:** Units vary based on the parameter - see commented text in b_inputs.gms. + - **Citation:** N/A +--- + + - [dac_elec_BVRE_2021_high.csv](/inputs/consume/dac_elec_BVRE_2021_high.csv) + - **File Type:** Inputs + - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions. + - **Indices:** i,t + - **Dollar year:** As specified in inputs/consume/dollaryear + - **Citation:** [https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a](https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a) +--- + + - [dac_elec_BVRE_2021_low.csv](/inputs/consume/dac_elec_BVRE_2021_low.csv) + - **File Type:** Inputs + - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions. + - **Indices:** i,t + - **Dollar year:** As specified in inputs/consume/dollaryear + - **Citation:** [https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a](https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a) +--- + + - [dac_elec_BVRE_2021_mid.csv](/inputs/consume/dac_elec_BVRE_2021_mid.csv) + - **File Type:** Inputs + - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions. + - **Indices:** i,t + - **Dollar year:** As specified in inputs/consume/dollaryear + - **Citation:** [https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a](https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a) +--- + + - [dac_gas_BVRE_2021_high.csv](/inputs/consume/dac_gas_BVRE_2021_high.csv) + - **File Type:** Inputs + - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions. + - **Indices:** i,t + - **Dollar year:** As specified in inputs/consume/dollaryear + - **Citation:** [https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987](https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987) +--- + + - [dac_gas_BVRE_2021_low.csv](/inputs/consume/dac_gas_BVRE_2021_low.csv) + - **File Type:** Inputs + - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions. + - **Indices:** i,t + - **Dollar year:** As specified in inputs/consume/dollaryear + - **Citation:** [https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987](https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987) +--- + + - [dac_gas_BVRE_2021_mid.csv](/inputs/consume/dac_gas_BVRE_2021_mid.csv) + - **File Type:** Inputs + - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions. + - **Indices:** i,t + - **Dollar year:** As specified in inputs/consume/dollaryear + - **Citation:** [https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987](https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987) +--- + + - [dollaryear.csv](/inputs/consume/dollaryear.csv) + - **File Type:** Inputs + - **Description:** Dollar year for various Beyond VRE scenarios. + - **Indices:** N/A + - **Dollar year:** Stated in document. + - **Citation:** N/A +--- + + - [h2_demand_county_share.csv](/inputs/consume/h2_demand_county_share.csv) + - **File Type:** Inputs + - **Description:** The fraction of national hydrogen demand in that year that corresponds to each county. Demand estimates come from https://data.openei.org/submissions/5655. 2021 demand shares correspond to the "Reference" scenario with light-duty vehicles / biofuels / methanol demand removed and 2050 shares correspond to the "Low Cost Electrolysis" scenario. + - **Indices:** r,t + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [h2_exogenous_demand.csv](/inputs/consume/h2_exogenous_demand.csv) + - **File Type:** Inputs + - **Description:** Exogenous hydrogen demand by industries other than the power sector per year + - **Indices:** t + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [h2_transport_and_storage_costs.csv](/inputs/consume/h2_transport_and_storage_costs.csv) + - **File Type:** Inputs + - **Description:** Transport and storage costs of hydrogen per year + - **Indices:** t + - **Dollar year:** 2004 + - **Citation:** N/A +--- + + + +#### inputs/ctus + + - [co2_site_char.csv](/inputs/ctus/co2_site_char.csv) + - **Dollar year:** 2018 +--- + + - [cs.csv](/inputs/ctus/cs.csv) +--- + + + +#### inputs/degradation + + - [degradation_annual_default.csv](/inputs/degradation/degradation_annual_default.csv) +--- + + + +#### inputs/demand_response + + - [dr_shed_avail_scalar.csv](/inputs/demand_response/dr_shed_avail_scalar.csv) +--- + + - [dr_shed_capacity_scalar_demo_data_IEF_January_2025.csv](/inputs/demand_response/dr_shed_capacity_scalar_demo_data_IEF_January_2025.csv) +--- + + - [dr_shed_hourly.h5](/inputs/demand_response/dr_shed_hourly.h5) +--- + + - [ev_load_Baseline.h5](/inputs/demand_response/ev_load_Baseline.h5) + - **File Type:** inputs + - **Description:** Baseline electricity load from EV charging by timeslice h and year t + - **Units:** MW + +--- + + - [evmc_rsc_Baseline.csv](/inputs/demand_response/evmc_rsc_Baseline.csv) +--- + + - [evmc_shape_decrease_profile_Baseline.h5](/inputs/demand_response/evmc_shape_decrease_profile_Baseline.h5) +--- + + - [evmc_shape_increase_profile_Baseline.h5](/inputs/demand_response/evmc_shape_increase_profile_Baseline.h5) +--- + + - [evmc_storage_decrease_profile_Baseline.h5](/inputs/demand_response/evmc_storage_decrease_profile_Baseline.h5) +--- + + - [evmc_storage_increase_profile_Baseline.h5](/inputs/demand_response/evmc_storage_increase_profile_Baseline.h5) +--- + + - [evmc_storage_profile_energy_Baseline.h5](/inputs/demand_response/evmc_storage_profile_energy_Baseline.h5) +--- + + + +#### inputs/dgen_model_inputs + + + +##### inputs/dgen_model_inputs/stscen2023_electrification + + - [distpvcap_stscen2023_electrification.csv](/inputs/dgen_model_inputs/stscen2023_electrification/distpvcap_stscen2023_electrification.csv) +--- + + + +##### inputs/dgen_model_inputs/stscen2023_highng + + - [distpvcap_stscen2023_highng.csv](/inputs/dgen_model_inputs/stscen2023_highng/distpvcap_stscen2023_highng.csv) + - **File Type:** distribution PV inputs + - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with high NG (including distpv) costs +--- + + + +##### inputs/dgen_model_inputs/stscen2023_highre + + - [distpvcap_stscen2023_highre.csv](/inputs/dgen_model_inputs/stscen2023_highre/distpvcap_stscen2023_highre.csv) + - **File Type:** distribution PV inputs + - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with high RE (including distpv) costs +--- + + + +##### inputs/dgen_model_inputs/stscen2023_lowng + + - [distpvcap_stscen2023_lowng.csv](/inputs/dgen_model_inputs/stscen2023_lowng/distpvcap_stscen2023_lowng.csv) + - **File Type:** distribution PV inputs + - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with low NG (including distpv) costs +--- + + + +##### inputs/dgen_model_inputs/stscen2023_lowre + + - [distpvcap_stscen2023_lowre.csv](/inputs/dgen_model_inputs/stscen2023_lowre/distpvcap_stscen2023_lowre.csv) + - **File Type:** distribution PV inputs + - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with low RE (including distpv) costs +--- + + + +##### inputs/dgen_model_inputs/stscen2023_mid_case + + - [distpvcap_stscen2023_mid_case.csv](/inputs/dgen_model_inputs/stscen2023_mid_case/distpvcap_stscen2023_mid_case.csv) + - **File Type:** distribution PV inputs +--- + + + +##### inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035 + + - [distpvcap_stscen2023_mid_case_95_by_2035.csv](/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035/distpvcap_stscen2023_mid_case_95_by_2035.csv) + - **File Type:** distribution PV inputs +--- + + + +##### inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050 + + - [distpvcap_stscen2023_mid_case_95_by_2050.csv](/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050/distpvcap_stscen2023_mid_case_95_by_2050.csv) + - **File Type:** distribution PV inputs +--- + + + +##### inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050 + + - [distpvcap_stscen2023_taxcredit_extended2050.csv](/inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050/distpvcap_stscen2023_taxcredit_extended2050.csv) + - **File Type:** distribution PV inputs +--- + + + +#### inputs/disaggregation + + - [county_population.csv](/inputs/disaggregation/county_population.csv) + - **Description:** The population of each county, relative values are used as multipliers for downselecting data. Data come from the U.S. Census Bureau 2021 county population estimates (https://www.census.gov/data/tables/time-series/demo/popest/2020s-counties-total.html). + - **Indices:** FIPS +--- + + - [county_state_lpf.csv](/inputs/disaggregation/county_state_lpf.csv) +--- + + - [disagg_hydroexist.csv](/inputs/disaggregation/disagg_hydroexist.csv) + - **Description:** The hydropower capacity fraction of each county within a given ReEDS BA, used as multipliers for downselecting data + - **Indices:** r +--- + + + +#### inputs/emission_constraints + + - [ccs_link.csv](/inputs/emission_constraints/ccs_link.csv) +--- + + - [ccs_link_water.csv](/inputs/emission_constraints/ccs_link_water.csv) +--- + + - [co2_cap.csv](/inputs/emission_constraints/co2_cap.csv) + - **Description:** Annual nationwide carbon cap +--- + + - [co2_tax.csv](/inputs/emission_constraints/co2_tax.csv) + - **Description:** Annual co2 tax +--- + + - [county_co2_share_egrid_2022.csv](/inputs/emission_constraints/county_co2_share_egrid_2022.csv) +--- + + - [csapr_group1_ex.csv](/inputs/emission_constraints/csapr_group1_ex.csv) +--- + + - [csapr_group2_ex.csv](/inputs/emission_constraints/csapr_group2_ex.csv) +--- + + - [csapr_ozone_season.csv](/inputs/emission_constraints/csapr_ozone_season.csv) +--- + + - [emitrate.csv](/inputs/emission_constraints/emitrate.csv) + - **Description:** Emission rates for thermal generators with values from Table 5 of https://docs.nrel.gov/docs/fy25osti/93005.pdf + - **Indices:** i,e +--- + + - [gwp.csv](/inputs/emission_constraints/gwp.csv) +--- + + - [h2_leakage_rate.csv](/inputs/emission_constraints/h2_leakage_rate.csv) +--- + + - [methane_leakage_rate.csv](/inputs/emission_constraints/methane_leakage_rate.csv) +--- + + - [ng_crf_penalty.csv](/inputs/emission_constraints/ng_crf_penalty.csv) + - **File Type:** Inputs + - **Description:** Cost adjustment for NG techs in scenarios with national decarbonization targets + - **Indices:** allt + - **Dollar year:** N/A + - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220](https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220) + - **Units:** rate (unitless) + +--- + + - [rggi_states.csv](/inputs/emission_constraints/rggi_states.csv) + - **Description:** Participating RGGI states + - **Citation:** [https://www.rggi.org/program-overview-and-design/elements](https://www.rggi.org/program-overview-and-design/elements) +--- + + - [rggicon.csv](/inputs/emission_constraints/rggicon.csv) + - **Description:** CO2 caps for RGGI states in metric tons + - **Citation:** [https://www.rggi.org/allowance-tracking/allowance-distribution](https://www.rggi.org/allowance-tracking/allowance-distribution) +--- + + - [state_cap.csv](/inputs/emission_constraints/state_cap.csv) +--- + + + +#### inputs/financials + + - [cap_penalty.csv](/inputs/financials/cap_penalty.csv) +--- + + - [construction_schedules_default.csv](/inputs/financials/construction_schedules_default.csv) +--- + + - [construction_times_default.csv](/inputs/financials/construction_times_default.csv) +--- + + - [currency_incentives.csv](/inputs/financials/currency_incentives.csv) +--- + + - [deflator.csv](/inputs/financials/deflator.csv) + - **Description:** Dollar year deflator to convert values to 2004$ +--- + + - [depreciation_schedules_default.csv](/inputs/financials/depreciation_schedules_default.csv) +--- + + - [energy_communities.csv](/inputs/financials/energy_communities.csv) +--- + + - [financials_hydrogen.csv](/inputs/financials/financials_hydrogen.csv) +--- + + - [financials_sys_ATB2023.csv](/inputs/financials/financials_sys_ATB2023.csv) +--- + + - [financials_sys_ATB2024.csv](/inputs/financials/financials_sys_ATB2024.csv) +--- + + - [financials_tech_ATB2023.csv](/inputs/financials/financials_tech_ATB2023.csv) +--- + + - [financials_tech_ATB2023_CRP20.csv](/inputs/financials/financials_tech_ATB2023_CRP20.csv) +--- + + - [financials_tech_ATB2024.csv](/inputs/financials/financials_tech_ATB2024.csv) +--- + + - [financials_transmission_30ITC_0pen_2022_2031.csv](/inputs/financials/financials_transmission_30ITC_0pen_2022_2031.csv) +--- + + - [financials_transmission_default.csv](/inputs/financials/financials_transmission_default.csv) +--- + + - [incentives_annual.csv](/inputs/financials/incentives_annual.csv) +--- + + - [incentives_biennial.csv](/inputs/financials/incentives_biennial.csv) +--- + + - [incentives_ira.csv](/inputs/financials/incentives_ira.csv) +--- + + - [incentives_ira_45q_45v_extension.csv](/inputs/financials/incentives_ira_45q_45v_extension.csv) +--- + + - [incentives_noira.csv](/inputs/financials/incentives_noira.csv) +--- + + - [incentives_none.csv](/inputs/financials/incentives_none.csv) +--- + + - [incentives_obbba.csv](/inputs/financials/incentives_obbba.csv) +--- + + - [incentives_obbba_conservative.csv](/inputs/financials/incentives_obbba_conservative.csv) +--- + + - [inflation_default.csv](/inputs/financials/inflation_default.csv) + - **Description:** Annual inflation factors from 1914 through 2200; historical values use the avg-avg values from https://www.usinflationcalculator.com/inflation/consumer-price-index-and-annual-percent-changes-from-1913-to-2008/ + - **Indices:** t +--- + + - [nuclear_energy_communities.csv](/inputs/financials/nuclear_energy_communities.csv) + - **Description:** "Counties belonging to metropolitan statistical areas (MSAs) for which at least 0.17 percent of direct employment has been related to nuclear power at any point since 2010. These are determined partly by following the process described in Section 2.6 of https://home.treasury.gov/system/files/8861/EnergyCommunities_Data_Documentation.pdf and substituting in the NAICS code for nuclear electric power generation (221113) and partly by determining counties that belong to MSAs where the number of people employed by national labs engaged in nuclear research and development (PNNL, INL, ORNL, SNL, LLNL, Argonne, and LANL) has been at least 0.17 percent of the MSA's total employment at any point since 2010." +--- + + - [reg_cap_cost_diff_default.csv](/inputs/financials/reg_cap_cost_diff_default.csv) + - **File Type:** parameter + - **Description:** region-specific differences for capital cost of all resources. Add to 1 to produce a multiplier + - **Indices:** i,r +--- + + - [retire_penalty.csv](/inputs/financials/retire_penalty.csv) +--- + + - [supply_chain_adjust.csv](/inputs/financials/supply_chain_adjust.csv) +--- + + - [tc_phaseout_schedule_ira2022.csv](/inputs/financials/tc_phaseout_schedule_ira2022.csv) +--- + + + +#### inputs/fuelprices + + - [alpha_AEO_2023_HOG.csv](/inputs/fuelprices/alpha_AEO_2023_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves + - **Indices:** allt,cendiv + - **Dollar year:** 2004 + - **Citation:** AEO 2023 +--- + + - [alpha_AEO_2023_LOG.csv](/inputs/fuelprices/alpha_AEO_2023_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves + - **Indices:** allt,cendiv + - **Dollar year:** 2004 + - **Citation:** AEO 2023 +--- + + - [alpha_AEO_2023_reference.csv](/inputs/fuelprices/alpha_AEO_2023_reference.csv) + - **File Type:** Input + - **Description:** reference census division alpha values, used in the calculation of natural gas demand curves + - **Indices:** allt,cendiv + - **Dollar year:** 2004 + - **Citation:** AEO 2023 +--- + + - [alpha_AEO_2025_HOG.csv](/inputs/fuelprices/alpha_AEO_2025_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves + - **Indices:** allt,cendiv + - **Dollar year:** 2004 + - **Citation:** AEO 2025 +--- + + - [alpha_AEO_2025_LOG.csv](/inputs/fuelprices/alpha_AEO_2025_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves + - **Indices:** allt,cendiv + - **Dollar year:** 2004 + - **Citation:** AEO 2025 +--- + + - [alpha_AEO_2025_reference.csv](/inputs/fuelprices/alpha_AEO_2025_reference.csv) + - **File Type:** Input + - **Description:** reference census division alpha values, used in the calculation of natural gas demand curves + - **Indices:** allt,cendiv + - **Dollar year:** 2004 + - **Citation:** AEO 2025 +--- + + - [cd_beta0.csv](/inputs/fuelprices/cd_beta0.csv) + - **File Type:** Input + - **Description:** reference census division beta levels electric sector + - **Indices:** cendiv + - **Dollar year:** 2004 +--- + + - [cd_beta0_allsector.csv](/inputs/fuelprices/cd_beta0_allsector.csv) + - **File Type:** Input + - **Description:** reference census division beta levels all sectors + - **Indices:** cendiv + - **Dollar year:** 2004 +--- + + - [cendivweights.csv](/inputs/fuelprices/cendivweights.csv) + - **Description:** weights to smooth gas prices between census regions to avoid abrupt price changes at the cendiv borders + - **Indices:** r,cendiv +--- + + - [coal_AEO_2023_reference.csv](/inputs/fuelprices/coal_AEO_2023_reference.csv) + - **Description:** reference case census division fuel price of coal + - **Indices:** t,cendiv + - **Dollar year:** 2022 +--- + + - [coal_AEO_2025_reference.csv](/inputs/fuelprices/coal_AEO_2025_reference.csv) + - **Description:** reference case census division fuel price of coal with missing values forward-filled from earlier years + - **Indices:** t,cendiv + - **Dollar year:** 2024 +--- + + - [dollaryear.csv](/inputs/fuelprices/dollaryear.csv) + - **Description:** Dollar year mapping for each fuel price scenario +--- + + - [h2-combustion_10.csv](/inputs/fuelprices/h2-combustion_10.csv) + - **Description:** price of hydrogen for combustion technologies (h2-ct and cc) at $10/MMBtu for all years +--- + + - [h2-combustion_30.csv](/inputs/fuelprices/h2-combustion_30.csv) + - **Description:** price of hydrogen for combustion technologies (h2-ct and cc) at $30/MMBtu for all years +--- + + - [h2-combustion_reference.csv](/inputs/fuelprices/h2-combustion_reference.csv) + - **Description:** price of hydrogen for combustion technologies (h2-ct and cc) at $20/MMBtu for all years +--- + + - [ng_AEO_2023_HOG.csv](/inputs/fuelprices/ng_AEO_2023_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology scenario census division fuel price of natural gas + - **Indices:** cendiv,t + - **Dollar year:** 2004 + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** 2004$/MMBtu + +--- + + - [ng_AEO_2023_LOG.csv](/inputs/fuelprices/ng_AEO_2023_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas + - **Indices:** cendiv,t + - **Dollar year:** 2004 + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** 2004$/MMBtu + +--- + + - [ng_AEO_2023_reference.csv](/inputs/fuelprices/ng_AEO_2023_reference.csv) + - **File Type:** Input + - **Description:** Reference scenario census division fuel price of natural gas + - **Indices:** cendiv,t + - **Dollar year:** 2004 + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** 2004$/MMBtu + +--- + + - [ng_AEO_2025_HOG.csv](/inputs/fuelprices/ng_AEO_2025_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology scenario census division fuel price of natural gas + - **Indices:** cendiv,t + - **Dollar year:** 2004 + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** 2004$/MMBtu + +--- + + - [ng_AEO_2025_LOG.csv](/inputs/fuelprices/ng_AEO_2025_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas + - **Indices:** cendiv,t + - **Dollar year:** 2004 + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** 2004$/MMBtu + +--- + + - [ng_AEO_2025_reference.csv](/inputs/fuelprices/ng_AEO_2025_reference.csv) + - **File Type:** Input + - **Description:** Reference scenario census division fuel price of natural gas + - **Indices:** cendiv,t + - **Dollar year:** 2004 + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** 2004$/MMBtu + +--- + + - [ng_demand_AEO_2023_HOG.csv](/inputs/fuelprices/ng_demand_AEO_2023_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_demand_AEO_2023_LOG.csv](/inputs/fuelprices/ng_demand_AEO_2023_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_demand_AEO_2023_reference.csv](/inputs/fuelprices/ng_demand_AEO_2023_reference.csv) + - **File Type:** Input + - **Description:** Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_demand_AEO_2025_HOG.csv](/inputs/fuelprices/ng_demand_AEO_2025_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_demand_AEO_2025_LOG.csv](/inputs/fuelprices/ng_demand_AEO_2025_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_demand_AEO_2025_reference.csv](/inputs/fuelprices/ng_demand_AEO_2025_reference.csv) + - **File Type:** Input + - **Description:** Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_tot_demand_AEO_2023_HOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2023_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_tot_demand_AEO_2023_LOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2023_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_tot_demand_AEO_2023_reference.csv](/inputs/fuelprices/ng_tot_demand_AEO_2023_reference.csv) + - **File Type:** Input + - **Description:** Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_tot_demand_AEO_2025_HOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2025_HOG.csv) + - **File Type:** Input + - **Description:** High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_tot_demand_AEO_2025_LOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2025_LOG.csv) + - **File Type:** Input + - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [ng_tot_demand_AEO_2025_reference.csv](/inputs/fuelprices/ng_tot_demand_AEO_2025_reference.csv) + - **File Type:** Input + - **Description:** Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves + - **Indices:** cendiv,t + - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ + - **Units:** Quads + +--- + + - [uranium_AEO_2023_reference.csv](/inputs/fuelprices/uranium_AEO_2023_reference.csv) +--- + + - [uranium_AEO_2025_reference.csv](/inputs/fuelprices/uranium_AEO_2025_reference.csv) +--- + + + +#### inputs/geothermal + + - [geo_discovery_BAU.csv](/inputs/geothermal/geo_discovery_BAU.csv) +--- + + - [geo_discovery_factor_ATB_2023.csv](/inputs/geothermal/geo_discovery_factor_ATB_2023.csv) +--- + + - [geo_discovery_factor_reV.csv](/inputs/geothermal/geo_discovery_factor_reV.csv) +--- + + - [geo_discovery_TI.csv](/inputs/geothermal/geo_discovery_TI.csv) +--- + + - [geo_rsc_ATB_2023.csv](/inputs/geothermal/geo_rsc_ATB_2023.csv) +--- + + + +#### inputs/growth_constraints + + - [gbin_min.csv](/inputs/growth_constraints/gbin_min.csv) +--- + + - [growth_bin_size_mult.csv](/inputs/growth_constraints/growth_bin_size_mult.csv) +--- + + - [growth_limit_absolute.csv](/inputs/growth_constraints/growth_limit_absolute.csv) + - **Description:** Maximum expected annual builds for wind, batteries, and UPV from 2024-2026 using observed record builds. + - **Units:** MW/year + +--- + + - [growth_penalty.csv](/inputs/growth_constraints/growth_penalty.csv) +--- + + + +#### inputs/hydro + + - [cap_existing_hydro.h5](/inputs/hydro/cap_existing_hydro.h5) + - **File Type:** Input + - **Description:** Annual capacities for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset. + - **Indices:** t + - **Units:** MW + +--- + + - [hyd_fom.csv](/inputs/hydro/hyd_fom.csv) + - **Description:** Regional FOM costs for hydro +--- + + - [hydcf_fixed.h5](/inputs/hydro/hydcf_fixed.h5) + - **File Type:** Input + - **Description:** Fixed monthly zonal hydro capacity factor data partially created by ORNL and partially derived from ORNL's Existing Hydropower Assets dataset. + - **Indices:** i,month + - **Units:** unitless + +--- + + - [hydro_mingen.csv](/inputs/hydro/hydro_mingen.csv) +--- + + - [net_gen_existing_hydro.h5](/inputs/hydro/net_gen_existing_hydro.h5) + - **File Type:** Input + - **Description:** Monthly net generation values for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset. + - **Indices:** t,month + - **Units:** MWh + +--- + + - [SeaCapAdj_hy.csv](/inputs/hydro/SeaCapAdj_hy.csv) +--- + + + +#### inputs/load + + - [cangrowth.csv](/inputs/load/cangrowth.csv) + - **Description:** Canada load growth multiplier +--- + + - [demand_AEO_2023_high.csv](/inputs/load/demand_AEO_2023_high.csv) + - **Description:** Load growth projection from the AEO2023 High Economic Growth scenario + - **Units:** unitless + +--- + + - [demand_AEO_2023_low.csv](/inputs/load/demand_AEO_2023_low.csv) + - **Description:** Load growth projection from the AEO2023 Low Economic Growth scenario + - **Units:** unitless + +--- + + - [demand_AEO_2023_reference.csv](/inputs/load/demand_AEO_2023_reference.csv) + - **Description:** Load growth projection from the AEO2023 Reference scenario + - **Units:** unitless + +--- + + - [demand_AEO_2025_high.csv](/inputs/load/demand_AEO_2025_high.csv) + - **Description:** Load growth projection from the AEO2025 High Economic Growth scenario + - **Units:** unitless + +--- + + - [demand_AEO_2025_low.csv](/inputs/load/demand_AEO_2025_low.csv) + - **Description:** Load growth projection from the AEO2025 Low Economic Growth scenario + - **Units:** unitless + +--- + + - [demand_AEO_2025_reference.csv](/inputs/load/demand_AEO_2025_reference.csv) + - **Description:** Load growth projection from the AEO2025 Reference scenario + - **Units:** unitless + +--- + + - [EIA_loadbystate.csv](/inputs/load/EIA_loadbystate.csv) +--- + + - [loadsite_country_test.csv](/inputs/load/loadsite_country_test.csv) +--- + + - [mex_growth_rate.csv](/inputs/load/mex_growth_rate.csv) + - **Description:** Mexico load growth multiplier +--- + + + +#### inputs/national_generation + + - [gen_mandate_tech_list.csv](/inputs/national_generation/gen_mandate_tech_list.csv) +--- + + - [gen_mandate_trajectory.csv](/inputs/national_generation/gen_mandate_trajectory.csv) +--- + + - [national_rps_frac_allScen.csv](/inputs/national_generation/national_rps_frac_allScen.csv) +--- + + + +#### inputs/outages + + - [temperature_celsius-st.h5](/inputs/outages/temperature_celsius-st.h5) +--- + + + +#### inputs/plant_characteristics + + - [battery_ATB_2024_advanced.csv](/inputs/plant_characteristics/battery_ATB_2024_advanced.csv) + - **Dollar year:** 2021 +--- + + - [battery_ATB_2024_conservative.csv](/inputs/plant_characteristics/battery_ATB_2024_conservative.csv) + - **Dollar year:** 2021 +--- + + - [battery_ATB_2024_moderate.csv](/inputs/plant_characteristics/battery_ATB_2024_moderate.csv) + - **Dollar year:** 2021 +--- + + - [beccs_BVRE_2021_high.csv](/inputs/plant_characteristics/beccs_BVRE_2021_high.csv) +--- + + - [beccs_BVRE_2021_low.csv](/inputs/plant_characteristics/beccs_BVRE_2021_low.csv) +--- + + - [beccs_BVRE_2021_mid.csv](/inputs/plant_characteristics/beccs_BVRE_2021_mid.csv) +--- + + - [beccs_lowcost.csv](/inputs/plant_characteristics/beccs_lowcost.csv) +--- + + - [beccs_reference.csv](/inputs/plant_characteristics/beccs_reference.csv) +--- + + - [biopower_ATB_2024_moderate.csv](/inputs/plant_characteristics/biopower_ATB_2024_moderate.csv) +--- + + - [ccsflex_ATB_2020_cost.csv](/inputs/plant_characteristics/ccsflex_ATB_2020_cost.csv) +--- + + - [ccsflex_ATB_2020_perf.csv](/inputs/plant_characteristics/ccsflex_ATB_2020_perf.csv) +--- + + - [coal-ccs_ATB_2024_advanced.csv](/inputs/plant_characteristics/coal-ccs_ATB_2024_advanced.csv) +--- + + - [coal-ccs_ATB_2024_conservative.csv](/inputs/plant_characteristics/coal-ccs_ATB_2024_conservative.csv) +--- + + - [coal-ccs_ATB_2024_moderate.csv](/inputs/plant_characteristics/coal-ccs_ATB_2024_moderate.csv) +--- + + - [coal_ATB_2024_moderate.csv](/inputs/plant_characteristics/coal_ATB_2024_moderate.csv) +--- + + - [cost_opres_default.csv](/inputs/plant_characteristics/cost_opres_default.csv) +--- + + - [cost_opres_market.csv](/inputs/plant_characteristics/cost_opres_market.csv) +--- + + - [csp_ATB_2023_advanced.csv](/inputs/plant_characteristics/csp_ATB_2023_advanced.csv) +--- + + - [csp_ATB_2023_conservative.csv](/inputs/plant_characteristics/csp_ATB_2023_conservative.csv) +--- + + - [csp_ATB_2023_moderate.csv](/inputs/plant_characteristics/csp_ATB_2023_moderate.csv) +--- + + - [csp_ATB_2024_advanced.csv](/inputs/plant_characteristics/csp_ATB_2024_advanced.csv) +--- + + - [csp_ATB_2024_conservative.csv](/inputs/plant_characteristics/csp_ATB_2024_conservative.csv) +--- + + - [csp_ATB_2024_moderate.csv](/inputs/plant_characteristics/csp_ATB_2024_moderate.csv) +--- + + - [csp_SunShot2030.csv](/inputs/plant_characteristics/csp_SunShot2030.csv) + - **Description:** Csp costs from the SunShot2030 cost scenario +--- + + - [dollaryear.csv](/inputs/plant_characteristics/dollaryear.csv) + - **Description:** Dollar year mapping for each plant cost scenario +--- + + - [dr_shed_capcost_demo_data_IEF_January_2025.csv](/inputs/plant_characteristics/dr_shed_capcost_demo_data_IEF_January_2025.csv) +--- + + - [dr_shed_fom.csv](/inputs/plant_characteristics/dr_shed_fom.csv) +--- + + - [dr_shed_vom.csv](/inputs/plant_characteristics/dr_shed_vom.csv) +--- + + - [evmc_shape_Baseline.csv](/inputs/plant_characteristics/evmc_shape_Baseline.csv) +--- + + - [evmc_storage_Baseline.csv](/inputs/plant_characteristics/evmc_storage_Baseline.csv) +--- + + - [gas-ccs_ATB_2024_advanced.csv](/inputs/plant_characteristics/gas-ccs_ATB_2024_advanced.csv) +--- + + - [gas-ccs_ATB_2024_conservative.csv](/inputs/plant_characteristics/gas-ccs_ATB_2024_conservative.csv) +--- + + - [gas-ccs_ATB_2024_moderate.csv](/inputs/plant_characteristics/gas-ccs_ATB_2024_moderate.csv) +--- + + - [gas_ATB_2024_moderate.csv](/inputs/plant_characteristics/gas_ATB_2024_moderate.csv) +--- + + - [geo_ATB_2023_advanced.csv](/inputs/plant_characteristics/geo_ATB_2023_advanced.csv) +--- + + - [geo_ATB_2023_conservative.csv](/inputs/plant_characteristics/geo_ATB_2023_conservative.csv) +--- + + - [geo_ATB_2023_moderate.csv](/inputs/plant_characteristics/geo_ATB_2023_moderate.csv) +--- + + - [geo_ATB_2024_advanced.csv](/inputs/plant_characteristics/geo_ATB_2024_advanced.csv) +--- + + - [geo_ATB_2024_conservative.csv](/inputs/plant_characteristics/geo_ATB_2024_conservative.csv) +--- + + - [geo_ATB_2024_moderate.csv](/inputs/plant_characteristics/geo_ATB_2024_moderate.csv) +--- + + - [h2-combustion_ATB_2023.csv](/inputs/plant_characteristics/h2-combustion_ATB_2023.csv) +--- + + - [h2-combustion_ATB_2024.csv](/inputs/plant_characteristics/h2-combustion_ATB_2024.csv) + - **Description:** Hydrogen CT and CC plant costs generated in preprocessing from moderate case NREL ATB 2024 data +--- + + - [heat_rate_adj.csv](/inputs/plant_characteristics/heat_rate_adj.csv) + - **Description:** Heat rate adjustment multiplier by technology +--- + + - [heat_rate_penalty_spin.csv](/inputs/plant_characteristics/heat_rate_penalty_spin.csv) +--- + + - [hydro_ATB_2019_constant.csv](/inputs/plant_characteristics/hydro_ATB_2019_constant.csv) + - **Description:** Hydro costs from the 2019 ATB constant cost scenario +--- + + - [hydro_ATB_2019_low.csv](/inputs/plant_characteristics/hydro_ATB_2019_low.csv) + - **Description:** Hydro costs from the 2019 ATB low cost scenario +--- + + - [hydro_ATB_2019_mid.csv](/inputs/plant_characteristics/hydro_ATB_2019_mid.csv) + - **Description:** Hydro costs from the 2019 ATB mid cost scenario +--- + + - [maxage.csv](/inputs/plant_characteristics/maxage.csv) + - **Description:** Maximum age allowed for each technology +--- + + - [maxdailycf.csv](/inputs/plant_characteristics/maxdailycf.csv) + - **Description:** maximum daily capacity factor--dr_shed input supply curves are based on one 4-hour event per day +--- + + - [min_retire_age.csv](/inputs/plant_characteristics/min_retire_age.csv) + - **Description:** Minimum retirement age for given technology +--- + + - [minCF.csv](/inputs/plant_characteristics/minCF.csv) + - **Description:** minimum annual capacity factor for each tech fleet - applied to i-rto +--- + + - [mingen_fixed.csv](/inputs/plant_characteristics/mingen_fixed.csv) +--- + + - [minloadfrac0.csv](/inputs/plant_characteristics/minloadfrac0.csv) + - **Description:** characteristics/minloadfrac0 database of minloadbed generator cs +--- + + - [mttr.csv](/inputs/plant_characteristics/mttr.csv) +--- + + - [nuclear-smr_ATB_2024_advanced.csv](/inputs/plant_characteristics/nuclear-smr_ATB_2024_advanced.csv) +--- + + - [nuclear-smr_ATB_2024_conservative.csv](/inputs/plant_characteristics/nuclear-smr_ATB_2024_conservative.csv) +--- + + - [nuclear-smr_ATB_2024_moderate.csv](/inputs/plant_characteristics/nuclear-smr_ATB_2024_moderate.csv) +--- + + - [nuclear_ATB_2024_advanced.csv](/inputs/plant_characteristics/nuclear_ATB_2024_advanced.csv) +--- + + - [nuclear_ATB_2024_conservative.csv](/inputs/plant_characteristics/nuclear_ATB_2024_conservative.csv) +--- + + - [nuclear_ATB_2024_moderate.csv](/inputs/plant_characteristics/nuclear_ATB_2024_moderate.csv) +--- + + - [ofs-wind_ATB_2023_advanced.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_advanced.csv) + - **File Type:** Inputs file + - **Description:** 2023 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year + - **Dollar year:** 2004 +--- + + - [ofs-wind_ATB_2023_conservative.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_conservative.csv) + - **File Type:** Inputs file + - **Description:** 2023 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year + - **Dollar year:** 2004 +--- + + - [ofs-wind_ATB_2023_moderate.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate.csv) + - **File Type:** Inputs file + - **Description:** 2023 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year + - **Dollar year:** 2004 +--- + + - [ofs-wind_ATB_2023_moderate_noFloating.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate_noFloating.csv) +--- + + - [ofs-wind_ATB_2024_advanced.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_advanced.csv) + - **File Type:** Inputs file + - **Description:** 2024 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year + - **Dollar year:** 2022 +--- + + - [ofs-wind_ATB_2024_conservative.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_conservative.csv) + - **File Type:** Inputs file + - **Description:** 2024 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year + - **Dollar year:** 2022 +--- + + - [ofs-wind_ATB_2024_moderate.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate.csv) + - **File Type:** Inputs file + - **Description:** 2024 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year + - **Dollar year:** 2022 +--- + + - [ofs-wind_ATB_2024_moderate_noFloating.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate_noFloating.csv) + - **File Type:** Inputs file + - **Description:** 2024 moderate_noFloating ofs-wind capital (5x floating capital cost), fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year + - **Dollar year:** 2022 +--- + + - [ons-wind_ATB_2023_advanced.csv](/inputs/plant_characteristics/ons-wind_ATB_2023_advanced.csv) +--- + + - [ons-wind_ATB_2023_conservative.csv](/inputs/plant_characteristics/ons-wind_ATB_2023_conservative.csv) +--- + + - [ons-wind_ATB_2023_moderate.csv](/inputs/plant_characteristics/ons-wind_ATB_2023_moderate.csv) +--- + + - [ons-wind_ATB_2024_advanced.csv](/inputs/plant_characteristics/ons-wind_ATB_2024_advanced.csv) + - **File Type:** Inputs file + - **Description:** Advanced cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind + - **Dollar year:** 2022 +--- + + - [ons-wind_ATB_2024_conservative.csv](/inputs/plant_characteristics/ons-wind_ATB_2024_conservative.csv) + - **File Type:** Inputs file + - **Description:** Conservative cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind + - **Dollar year:** 2022 +--- + + - [ons-wind_ATB_2024_moderate.csv](/inputs/plant_characteristics/ons-wind_ATB_2024_moderate.csv) + - **File Type:** Inputs file + - **Description:** Moderate cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind + - **Dollar year:** 2022 +--- + + - [other_plantchar.csv](/inputs/plant_characteristics/other_plantchar.csv) +--- + + - [outage_forced_static.csv](/inputs/plant_characteristics/outage_forced_static.csv) + - **File Type:** Inputs file + - **Description:** Forced outage rates by technology +--- + + - [outage_forced_temperature_murphy2019.csv](/inputs/plant_characteristics/outage_forced_temperature_murphy2019.csv) +--- + + - [outage_scheduled_monthly.csv](/inputs/plant_characteristics/outage_scheduled_monthly.csv) +--- + + - [outage_scheduled_static.csv](/inputs/plant_characteristics/outage_scheduled_static.csv) + - **Description:** Scheduled outage rate by technology +--- + + - [pvb_benchmark2020.csv](/inputs/plant_characteristics/pvb_benchmark2020.csv) +--- + + - [ramprate.csv](/inputs/plant_characteristics/ramprate.csv) + - **Description:** Generator ramp rates by technology +--- + + - [startcost.csv](/inputs/plant_characteristics/startcost.csv) +--- + + - [unitsize_atb.csv](/inputs/plant_characteristics/unitsize_atb.csv) +--- + + - [upv_ATB_2023_advanced.csv](/inputs/plant_characteristics/upv_ATB_2023_advanced.csv) + - **File Type:** Inputs file + - **Description:** 2023 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year + - **Dollar year:** 2004 +--- + + - [upv_ATB_2023_conservative.csv](/inputs/plant_characteristics/upv_ATB_2023_conservative.csv) + - **File Type:** Inputs file + - **Description:** 2023 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year + - **Dollar year:** 2004 +--- + + - [upv_ATB_2023_moderate.csv](/inputs/plant_characteristics/upv_ATB_2023_moderate.csv) + - **File Type:** Inputs file + - **Description:** 2023 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year + - **Dollar year:** 2004 +--- + + - [upv_ATB_2024_advanced.csv](/inputs/plant_characteristics/upv_ATB_2024_advanced.csv) + - **File Type:** Inputs file + - **Description:** 2024 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year + - **Dollar year:** 2004 +--- + + - [upv_ATB_2024_conservative.csv](/inputs/plant_characteristics/upv_ATB_2024_conservative.csv) + - **File Type:** Inputs file + - **Description:** 2024 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year + - **Dollar year:** 2004 +--- + + - [upv_ATB_2024_moderate.csv](/inputs/plant_characteristics/upv_ATB_2024_moderate.csv) + - **File Type:** Inputs file + - **Description:** 2024 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year + - **Dollar year:** 2004 +--- + + - [years_until_endogenous.csv](/inputs/plant_characteristics/years_until_endogenous.csv) +--- + + + +#### inputs/profiles_cf + + - [cf_distpv_county.h5](/inputs/profiles_cf/cf_distpv_county.h5) +--- + + - [cf_upv_limited_ba.h5](/inputs/profiles_cf/cf_upv_limited_ba.h5) +--- + + - [cf_upv_limited_county.h5](/inputs/profiles_cf/cf_upv_limited_county.h5) +--- + + - [cf_upv_open_ba.h5](/inputs/profiles_cf/cf_upv_open_ba.h5) +--- + + - [cf_upv_open_county.h5](/inputs/profiles_cf/cf_upv_open_county.h5) +--- + + - [cf_upv_reference_ba.h5](/inputs/profiles_cf/cf_upv_reference_ba.h5) +--- + + - [cf_upv_reference_county.h5](/inputs/profiles_cf/cf_upv_reference_county.h5) +--- + + - [cf_wind-ofs_meshed_limited_ba.h5](/inputs/profiles_cf/cf_wind-ofs_meshed_limited_ba.h5) +--- + + - [cf_wind-ofs_meshed_open_ba.h5](/inputs/profiles_cf/cf_wind-ofs_meshed_open_ba.h5) +--- + + - [cf_wind-ofs_meshed_reference_ba.h5](/inputs/profiles_cf/cf_wind-ofs_meshed_reference_ba.h5) +--- + + - [cf_wind-ofs_radial_limited_ba.h5](/inputs/profiles_cf/cf_wind-ofs_radial_limited_ba.h5) +--- + + - [cf_wind-ofs_radial_limited_county.h5](/inputs/profiles_cf/cf_wind-ofs_radial_limited_county.h5) +--- + + - [cf_wind-ofs_radial_open_ba.h5](/inputs/profiles_cf/cf_wind-ofs_radial_open_ba.h5) +--- + + - [cf_wind-ofs_radial_open_county.h5](/inputs/profiles_cf/cf_wind-ofs_radial_open_county.h5) +--- + + - [cf_wind-ofs_radial_reference_ba.h5](/inputs/profiles_cf/cf_wind-ofs_radial_reference_ba.h5) +--- + + - [cf_wind-ofs_radial_reference_county.h5](/inputs/profiles_cf/cf_wind-ofs_radial_reference_county.h5) +--- + + - [cf_wind-ons_limited_ba.h5](/inputs/profiles_cf/cf_wind-ons_limited_ba.h5) +--- + + - [cf_wind-ons_limited_county.h5](/inputs/profiles_cf/cf_wind-ons_limited_county.h5) +--- + + - [cf_wind-ons_open_ba.h5](/inputs/profiles_cf/cf_wind-ons_open_ba.h5) +--- + + - [cf_wind-ons_open_county.h5](/inputs/profiles_cf/cf_wind-ons_open_county.h5) +--- + + - [cf_wind-ons_reference_ba.h5](/inputs/profiles_cf/cf_wind-ons_reference_ba.h5) +--- + + - [cf_wind-ons_reference_county.h5](/inputs/profiles_cf/cf_wind-ons_reference_county.h5) +--- + + + +#### inputs/profiles_demand + + - [demand_EER2023_100by2050.h5](/inputs/profiles_demand/demand_EER2023_100by2050.h5) +--- + + - [demand_EER2023_Baseline_AEO2022.h5](/inputs/profiles_demand/demand_EER2023_Baseline_AEO2022.h5) +--- + + - [demand_EER2023_IRAlow.h5](/inputs/profiles_demand/demand_EER2023_IRAlow.h5) +--- + + - [demand_EER2023_IRAmoderate.h5](/inputs/profiles_demand/demand_EER2023_IRAmoderate.h5) +--- + + - [demand_EER2025_100by2050.h5](/inputs/profiles_demand/demand_EER2025_100by2050.h5) +--- + + - [demand_EER2025_Baseline_AEO2023.h5](/inputs/profiles_demand/demand_EER2025_Baseline_AEO2023.h5) +--- + + - [demand_EER2025_IRAlow.h5](/inputs/profiles_demand/demand_EER2025_IRAlow.h5) +--- + + - [demand_EFS_Baseline.h5](/inputs/profiles_demand/demand_EFS_Baseline.h5) +--- + + - [demand_EFS_Clean2035.h5](/inputs/profiles_demand/demand_EFS_Clean2035.h5) +--- + + - [demand_EFS_Clean2035_LTS.h5](/inputs/profiles_demand/demand_EFS_Clean2035_LTS.h5) +--- + + - [demand_EFS_Clean2035clip1pct.h5](/inputs/profiles_demand/demand_EFS_Clean2035clip1pct.h5) +--- + + - [demand_EFS_HIGH.h5](/inputs/profiles_demand/demand_EFS_HIGH.h5) +--- + + - [demand_EFS_MEDIUM.h5](/inputs/profiles_demand/demand_EFS_MEDIUM.h5) +--- + + - [demand_EFS_MEDIUMStretch2040.h5](/inputs/profiles_demand/demand_EFS_MEDIUMStretch2040.h5) +--- + + - [demand_EFS_MEDIUMStretch2046.h5](/inputs/profiles_demand/demand_EFS_MEDIUMStretch2046.h5) +--- + + - [demand_EFS_REFERENCE.h5](/inputs/profiles_demand/demand_EFS_REFERENCE.h5) +--- + + - [demand_historic.h5](/inputs/profiles_demand/demand_historic.h5) +--- + + + +#### inputs/remote + + - [cf_distpv_county_18421977.h5](/inputs/remote/cf_distpv_county_18421977.h5) +--- + + - [cf_upv_limited_ba_18407660.h5](/inputs/remote/cf_upv_limited_ba_18407660.h5) +--- + + - [cf_upv_limited_county_18407660.h5](/inputs/remote/cf_upv_limited_county_18407660.h5) +--- + + - [cf_upv_open_ba_18407660.h5](/inputs/remote/cf_upv_open_ba_18407660.h5) +--- + + - [cf_upv_open_county_18407660.h5](/inputs/remote/cf_upv_open_county_18407660.h5) +--- + + - [cf_upv_reference_ba_18407660.h5](/inputs/remote/cf_upv_reference_ba_18407660.h5) +--- + + - [cf_upv_reference_county_18407660.h5](/inputs/remote/cf_upv_reference_county_18407660.h5) +--- + + - [cf_wind-ofs_meshed_limited_ba_18423723.h5](/inputs/remote/cf_wind-ofs_meshed_limited_ba_18423723.h5) +--- + + - [cf_wind-ofs_meshed_open_ba_18423723.h5](/inputs/remote/cf_wind-ofs_meshed_open_ba_18423723.h5) +--- + + - [cf_wind-ofs_meshed_reference_ba_18423723.h5](/inputs/remote/cf_wind-ofs_meshed_reference_ba_18423723.h5) +--- + + - [cf_wind-ofs_radial_limited_ba_18423723.h5](/inputs/remote/cf_wind-ofs_radial_limited_ba_18423723.h5) +--- + + - [cf_wind-ofs_radial_limited_county_18423723.h5](/inputs/remote/cf_wind-ofs_radial_limited_county_18423723.h5) +--- + + - [cf_wind-ofs_radial_open_ba_18423723.h5](/inputs/remote/cf_wind-ofs_radial_open_ba_18423723.h5) +--- + + - [cf_wind-ofs_radial_open_county_18423723.h5](/inputs/remote/cf_wind-ofs_radial_open_county_18423723.h5) +--- + + - [cf_wind-ofs_radial_reference_ba_18423723.h5](/inputs/remote/cf_wind-ofs_radial_reference_ba_18423723.h5) +--- + + - [cf_wind-ofs_radial_reference_county_18423723.h5](/inputs/remote/cf_wind-ofs_radial_reference_county_18423723.h5) +--- + + - [cf_wind-ons_limited_ba_18422200.h5](/inputs/remote/cf_wind-ons_limited_ba_18422200.h5) +--- + + - [cf_wind-ons_limited_county_18422200.h5](/inputs/remote/cf_wind-ons_limited_county_18422200.h5) +--- + + - [cf_wind-ons_open_ba_18422200.h5](/inputs/remote/cf_wind-ons_open_ba_18422200.h5) +--- + + - [cf_wind-ons_open_county_18422200.h5](/inputs/remote/cf_wind-ons_open_county_18422200.h5) +--- + + - [cf_wind-ons_reference_ba_18422200.h5](/inputs/remote/cf_wind-ons_reference_ba_18422200.h5) +--- + + - [cf_wind-ons_reference_county_18422200.h5](/inputs/remote/cf_wind-ons_reference_county_18422200.h5) +--- + + - [demand_EER2023_100by2050_18423998.h5](/inputs/remote/demand_EER2023_100by2050_18423998.h5) +--- + + - [demand_EER2023_Baseline_AEO2022_18423998.h5](/inputs/remote/demand_EER2023_Baseline_AEO2022_18423998.h5) +--- + + - [demand_EER2023_IRAlow_18423998.h5](/inputs/remote/demand_EER2023_IRAlow_18423998.h5) +--- + + - [demand_EER2023_IRAmoderate_18423998.h5](/inputs/remote/demand_EER2023_IRAmoderate_18423998.h5) +--- + + - [demand_EER2025_100by2050_18435264.h5](/inputs/remote/demand_EER2025_100by2050_18435264.h5) +--- + + - [demand_EER2025_Baseline_AEO2023_18435264.h5](/inputs/remote/demand_EER2025_Baseline_AEO2023_18435264.h5) +--- + + - [demand_EER2025_IRAlow_18435264.h5](/inputs/remote/demand_EER2025_IRAlow_18435264.h5) +--- + + - [demand_EFS_Baseline_18461543.h5](/inputs/remote/demand_EFS_Baseline_18461543.h5) +--- + + - [demand_EFS_Clean2035_18461543.h5](/inputs/remote/demand_EFS_Clean2035_18461543.h5) +--- + + - [demand_EFS_Clean2035_LTS_18461543.h5](/inputs/remote/demand_EFS_Clean2035_LTS_18461543.h5) +--- + + - [demand_EFS_Clean2035clip1pct_18461543.h5](/inputs/remote/demand_EFS_Clean2035clip1pct_18461543.h5) +--- + + - [demand_EFS_HIGH_18461543.h5](/inputs/remote/demand_EFS_HIGH_18461543.h5) +--- + + - [demand_EFS_MEDIUM_18461543.h5](/inputs/remote/demand_EFS_MEDIUM_18461543.h5) +--- + + - [demand_EFS_MEDIUMStretch2040_18461543.h5](/inputs/remote/demand_EFS_MEDIUMStretch2040_18461543.h5) +--- + + - [demand_EFS_MEDIUMStretch2046_18461543.h5](/inputs/remote/demand_EFS_MEDIUMStretch2046_18461543.h5) +--- + + - [demand_EFS_REFERENCE_18461543.h5](/inputs/remote/demand_EFS_REFERENCE_18461543.h5) +--- + + - [demand_historic_18462671.h5](/inputs/remote/demand_historic_18462671.h5) +--- + + + +#### inputs/reserves + + - [ccseason_dates.csv](/inputs/reserves/ccseason_dates.csv) +--- + + - [opres_periods.csv](/inputs/reserves/opres_periods.csv) +--- + + - [orperc.csv](/inputs/reserves/orperc.csv) +--- + + - [peak_net_imports.csv](/inputs/reserves/peak_net_imports.csv) +--- + + - [prm_annual.csv](/inputs/reserves/prm_annual.csv) + - **Description:** Annual planning reserve margin by NERC region +--- + + - [ramptime.csv](/inputs/reserves/ramptime.csv) +--- + + + +#### inputs/sets + + - [aclike.csv](/inputs/sets/aclike.csv) + - **File Type:** GAMS set + - **Description:** set of AC transmission capacity types +--- + + - [allt.csv](/inputs/sets/allt.csv) + - **File Type:** GAMS set + - **Description:** set of all potential years +--- + + - [bioclass.csv](/inputs/sets/bioclass.csv) + - **File Type:** GAMS set + - **Description:** set of bio tech classes +--- + + - [ccsflex_cat.csv](/inputs/sets/ccsflex_cat.csv) + - **File Type:** GAMS set + - **Description:** set of flexible ccs performance parameter categories +--- + + - [climate_param.csv](/inputs/sets/climate_param.csv) + - **File Type:** GAMS set + - **Description:** set of parameters defined in climate_heuristics_finalyear +--- + + - [consumecat.csv](/inputs/sets/consumecat.csv) + - **File Type:** GAMS set + - **Description:** set of categories for consuming facility characteristics +--- + + - [csapr_cat.csv](/inputs/sets/csapr_cat.csv) + - **File Type:** GAMS set + - **Description:** set of CSAPR regulation categories +--- + + - [csapr_group.csv](/inputs/sets/csapr_group.csv) + - **File Type:** GAMS set + - **Description:** set of CSAPR trading groups +--- + + - [ctt.csv](/inputs/sets/ctt.csv) + - **File Type:** GAMS set + - **Description:** set of cooling technology types +--- + + - [e.csv](/inputs/sets/e.csv) + - **File Type:** GAMS set + - **Description:** set of emission categories used in model +--- + + - [eall.csv](/inputs/sets/eall.csv) + - **File Type:** GAMS set + - **Description:** set of emission categories used in reporting +--- + + - [etype.csv](/inputs/sets/etype.csv) +--- + + - [f.csv](/inputs/sets/f.csv) + - **File Type:** GAMS set + - **Description:** set of fuel types +--- + + - [flex_type.csv](/inputs/sets/flex_type.csv) + - **File Type:** GAMS set + - **Description:** set of demand flexibility types +--- + + - [fuel2tech.csv](/inputs/sets/fuel2tech.csv) + - **File Type:** GAMS set + - **Description:** mapping between fuel types and generations +--- + + - [fuelbin.csv](/inputs/sets/fuelbin.csv) + - **File Type:** GAMS set + - **Description:** set of gas usage brackets +--- + + - [gb.csv](/inputs/sets/gb.csv) + - **File Type:** GAMS set + - **Description:** set of gas price bins +--- + + - [gbin.csv](/inputs/sets/gbin.csv) + - **File Type:** GAMS set + - **Description:** set of growth bins +--- + + - [geotech.csv](/inputs/sets/geotech.csv) + - **File Type:** GAMS set + - **Description:** set of geothermal technology categories +--- + + - [h2_st.csv](/inputs/sets/h2_st.csv) + - **File Type:** GAMS set + - **Description:** defines investments needed to store and transport H2 +--- + + - [h2_stor.csv](/inputs/sets/h2_stor.csv) + - **File Type:** GAMS set + - **Description:** set of H2 storage options +--- + + - [hintage_char.csv](/inputs/sets/hintage_char.csv) + - **File Type:** GAMS set + - **Description:** set of characteristics available in hintage_data +--- + + - [i.csv](/inputs/sets/i.csv) + - **File Type:** GAMS set + - **Description:** set of technologies +--- + + - [i_geotech.csv](/inputs/sets/i_geotech.csv) + - **File Type:** GAMS set + - **Description:** crosswalk between an individual geothermal technology and its category +--- + + - [i_h2_ptc_gen.csv](/inputs/sets/i_h2_ptc_gen.csv) + - **File Type:** GAMS set + - **Description:** set of technologies which can produce energy for electrolyzers claiming the hydrogen production tax credit due to their low lifecycle carbon emissions +--- + + - [i_p.csv](/inputs/sets/i_p.csv) + - **File Type:** GAMS set + - **Description:** mapping from technologies to the products they produce +--- + + - [i_subtech.csv](/inputs/sets/i_subtech.csv) + - **File Type:** GAMS set + - **Description:** set of categories for subtechs +--- + + - [i_water_nocooling.csv](/inputs/sets/i_water_nocooling.csv) + - **File Type:** GAMS set + - **Description:** set of technologies that use water, but are not differentiated by cooling tech and water source +--- + + - [lcclike.csv](/inputs/sets/lcclike.csv) + - **File Type:** GAMS set + - **Description:** set of transmission capacity types where lines are bundled with AC/DC converters +--- + + - [month.csv](/inputs/sets/month.csv) + - **File Type:** GAMS set +--- + + - [noretire.csv](/inputs/sets/noretire.csv) + - **File Type:** GAMS set + - **Description:** set of technologies that will never be retired +--- + + - [notvsc.csv](/inputs/sets/notvsc.csv) + - **File Type:** GAMS set + - **Description:** set of transmission capacity types that are not VSC +--- + + - [ofstype.csv](/inputs/sets/ofstype.csv) + - **File Type:** GAMS set + - **Description:** set of offshore types used in offshore requirement constraint (eq_RPS_OFSWind) +--- + + - [ofstype_i.csv](/inputs/sets/ofstype_i.csv) + - **File Type:** GAMS set + - **Description:** crosswalk between ofstype and i +--- + + - [orcat.csv](/inputs/sets/orcat.csv) + - **File Type:** GAMS set + - **Description:** set of operating reserve categories +--- + + - [ortype.csv](/inputs/sets/ortype.csv) + - **File Type:** GAMS set + - **Description:** set of types of operating reserve constraints +--- + + - [p.csv](/inputs/sets/p.csv) + - **File Type:** GAMS set + - **Description:** set of products produced +--- + + - [pcat.csv](/inputs/sets/pcat.csv) + - **File Type:** GAMS set + - **Description:** set of prescribed technology categories +--- + + - [plantcat.csv](/inputs/sets/plantcat.csv) + - **File Type:** GAMS set + - **Description:** set of categories for plant characteristics +--- + + - [prepost.csv](/inputs/sets/prepost.csv) + - **File Type:** GAMS set +--- + + - [prescriptivelink0.csv](/inputs/sets/prescriptivelink0.csv) + - **File Type:** GAMS set + - **Description:** initial set of prescribed categories and their technologies - used in assigning prescribed builds +--- + + - [pvb_agg.csv](/inputs/sets/pvb_agg.csv) + - **File Type:** GAMS set + - **Description:** crosswalk between hybrid pv+battery configurations and technology options +--- + + - [pvb_config.csv](/inputs/sets/pvb_config.csv) + - **File Type:** GAMS set + - **Description:** set of hybrid pv+battery configurations +--- + + - [quarter.csv](/inputs/sets/quarter.csv) + - **File Type:** GAMS set +--- + + - [resourceclass.csv](/inputs/sets/resourceclass.csv) + - **File Type:** GAMS set + - **Description:** set of renewable resource classes +--- + + - [RPSCat.csv](/inputs/sets/RPSCat.csv) + - **File Type:** GAMS set + - **Description:** set of RPS constraint categories, including clean energy standards +--- + + - [sc_cat.csv](/inputs/sets/sc_cat.csv) + - **File Type:** GAMS set + - **Description:** set of supply curve categories (capacity and cost) +--- + + - [sdbin.csv](/inputs/sets/sdbin.csv) + - **File Type:** GAMS set + - **Description:** set of storage durage bins +--- + + - [sw.csv](/inputs/sets/sw.csv) + - **File Type:** GAMS set + - **Description:** set of surface water types where access is based on consumption not withdrawal +--- + + - [tg.csv](/inputs/sets/tg.csv) + - **File Type:** GAMS set + - **Description:** set of technology groups +--- + + - [tg_rsc_cspagg.csv](/inputs/sets/tg_rsc_cspagg.csv) + - **File Type:** GAMS set + - **Description:** set of csp technologies that belong to the same class +--- + + - [tg_rsc_upvagg.csv](/inputs/sets/tg_rsc_upvagg.csv) + - **File Type:** GAMS set + - **Description:** set of pv and pvb technologies that belong to the same class +--- + + - [trancap_fut_cat.csv](/inputs/sets/trancap_fut_cat.csv) + - **File Type:** GAMS set + - **Description:** set of categories of near-term transmission projects that describe the likelihood of being completed +--- + + - [trtype.csv](/inputs/sets/trtype.csv) + - **File Type:** GAMS set + - **Description:** set of transmission capacity types +--- + + - [unitspec_upgrades.csv](/inputs/sets/unitspec_upgrades.csv) + - **File Type:** GAMS set + - **Description:** set of upgraded technologies that get unit-specific characteristics +--- + + - [upgrade_hintage_char.csv](/inputs/sets/upgrade_hintage_char.csv) + - **File Type:** GAMS set + - **Description:** set to operate over in extension of hintage_data characteristics when sw_upgrades = 1 +--- + + - [w.csv](/inputs/sets/w.csv) + - **File Type:** GAMS set + - **Description:** set of water withdrawal or consumption options for water techs +--- + + - [wst.csv](/inputs/sets/wst.csv) + - **File Type:** GAMS set + - **Description:** set of water source types +--- + + - [wst_climate.csv](/inputs/sets/wst_climate.csv) + - **File Type:** GAMS set + - **Description:** set of water sources affected by climate change +--- + + - [yearafter.csv](/inputs/sets/yearafter.csv) + - **File Type:** GAMS set + - **Description:** set to loop over for the final year calculation +--- + + + +#### inputs/shapefiles + + - [state_fips_codes.csv](/inputs/shapefiles/state_fips_codes.csv) + - **Description:** Mapping of states to FIPS codes and postcal code abbreviations +--- + + + +#### inputs/state_policies + + - [acp_disallowed.csv](/inputs/state_policies/acp_disallowed.csv) + - **Description:** List of states which do not allow alternative compliance payments in place of meeting RPS or CES requirements +--- + + - [acp_prices.csv](/inputs/state_policies/acp_prices.csv) +--- + + - [ces_fraction.csv](/inputs/state_policies/ces_fraction.csv) + - **Description:** Annual compliance for states with a CES policy +--- + + - [forced_retirements.csv](/inputs/state_policies/forced_retirements.csv) + - **Description:** List of regions with mandatory retirement policies for certain technologies +--- + + - [hydrofrac_policy.csv](/inputs/state_policies/hydrofrac_policy.csv) +--- + + - [ng_crf_penalty_st.csv](/inputs/state_policies/ng_crf_penalty_st.csv) + - **File Type:** Inputs + - **Description:** Cost adjustment for NG techs in states where all NG techs must be retired by a certain year + - **Indices:** allt,st + - **Dollar year:** N/A + - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220](https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220) + - **Units:** rate (unitless) + +--- + + - [nuclear_subsidies.csv](/inputs/state_policies/nuclear_subsidies.csv) +--- + + - [offshore_req_default.csv](/inputs/state_policies/offshore_req_default.csv) + - **File Type:** Inputs + - **Description:** default state mandates of offshore wind capacity, updated in November 2025 + - **Indices:** st,allt + - **Units:** MW + +--- + + - [oosfrac.csv](/inputs/state_policies/oosfrac.csv) + - **Description:** Defines the fraction of renewable and clean energy credits can be purchased from out of state (oos). Applied for RPS and CES +--- + + - [recstyle.csv](/inputs/state_policies/recstyle.csv) + - **Description:** Indication for how to apply state requirement (0 = end-use sales, 1 = bus-bar sales, 2 = generation). Default is 0. +--- + + - [rectable.csv](/inputs/state_policies/rectable.csv) + - **Description:** Table defining which states are allowed to trade RECs +--- + + - [rps_fraction.csv](/inputs/state_policies/rps_fraction.csv) + - **Description:** Indicates what fraction of sales or generation (based on recstyle.csv) must be from renewable energy +--- + + - [storage_mandates.csv](/inputs/state_policies/storage_mandates.csv) + - **Description:** Energy storage mandates by region +--- + + - [techs_banned_ces.csv](/inputs/state_policies/techs_banned_ces.csv) + - **Description:** Indicates which technolgies are not eligible to contribute to CES +--- + + - [techs_banned_imports_rps.csv](/inputs/state_policies/techs_banned_imports_rps.csv) +--- + + - [techs_banned_rps.csv](/inputs/state_policies/techs_banned_rps.csv) + - **Description:** Indicates which technolgies are not eligible to contribute to RPS +--- + + - [unbundled_limit_ces.csv](/inputs/state_policies/unbundled_limit_ces.csv) + - **Description:** Limit on fraction of credits towards CES which can be purchased unbundled from other states +--- + + - [unbundled_limit_rps.csv](/inputs/state_policies/unbundled_limit_rps.csv) + - **Description:** Limit on fraction of credits towards RPS which can be purchased unbundled from other states +--- + + + +#### inputs/storage + + - [cap_existing_psh.csv](/inputs/storage/cap_existing_psh.csv) + - **Description:** County-wide PSH operational capacity, pump capacity, and max energy, based on plant-level data from https://www.hydropower.org/hydropower-pumped-storage-tool + - **Units:** MW/MWh +--- + + - [PSH_supply_curves_durations.csv](/inputs/storage/PSH_supply_curves_durations.csv) +--- + + - [storinmaxfrac.csv](/inputs/storage/storinmaxfrac.csv) +--- + + + +#### inputs/supply_curve + + - [bio_supplycurve.csv](/inputs/supply_curve/bio_supplycurve.csv) + - **Description:** Regional biomass supply and costs by resource class + - **Dollar year:** 2015 +--- + + - [dollaryear.csv](/inputs/supply_curve/dollaryear.csv) +--- + + - [dr_shed_cap.csv](/inputs/supply_curve/dr_shed_cap.csv) +--- + + - [dr_shed_cost.csv](/inputs/supply_curve/dr_shed_cost.csv) +--- + + - [hyd_add_upg_cap.csv](/inputs/supply_curve/hyd_add_upg_cap.csv) +--- + + - [hydcap.csv](/inputs/supply_curve/hydcap.csv) +--- + + - [hydcost.csv](/inputs/supply_curve/hydcost.csv) +--- + + - [interconnection_land.h5](/inputs/supply_curve/interconnection_land.h5) +--- + + - [interconnection_offshore.h5](/inputs/supply_curve/interconnection_offshore.h5) +--- + + - [PSH_supply_curves_capacity_10hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_10hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_10hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_10hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEphemeral_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_10hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_10hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_12hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_12hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_12hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_12hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEphemeral_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_12hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_12hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_8hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_8hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_8hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_8hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEphemeral_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_8hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_8hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024.csv) + - **Description:** PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_10hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_apr2025.csv) +--- + + - [PSH_supply_curves_cost_10hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_mar2024.csv) + - **Description:** PSH supply curve cost assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_10hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_cost_10hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEphemeral_mar2024.csv) + - **Description:** PSH supply curve cost assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_10hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_apr2025.csv) +--- + + - [PSH_supply_curves_cost_10hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_mar2024.csv) + - **Description:** PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_10hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_cost_10hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_mar2024.csv) + - **Description:** PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_12hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_apr2025.csv) +--- + + - [PSH_supply_curves_cost_12hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_mar2024.csv) + - **Description:** PSH supply curve cost assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_12hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_cost_12hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEphemeral_mar2024.csv) + - **Description:** PSH supply curve cost assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_12hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_apr2025.csv) +--- + + - [PSH_supply_curves_cost_12hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_mar2024.csv) + - **Description:** PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_12hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_cost_12hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_mar2024.csv) + - **Description:** PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_8hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_apr2025.csv) +--- + + - [PSH_supply_curves_cost_8hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_mar2024.csv) + - **Description:** PSH supply curve cost assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_8hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_cost_8hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEphemeral_mar2024.csv) + - **Description:** PSH supply curve cost assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_8hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_apr2025.csv) +--- + + - [PSH_supply_curves_cost_8hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_mar2024.csv) + - **Description:** PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [PSH_supply_curves_cost_8hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_apr2025.csv) +--- + + - [PSH_supply_curves_cost_8hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_mar2024.csv) + - **Description:** PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline + - **Dollar year:** 2004 + - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) +--- + + - [rev_paths.csv](/inputs/supply_curve/rev_paths.csv) +--- + + - [sc_point_gid_old2new.csv](/inputs/supply_curve/sc_point_gid_old2new.csv) +--- + + - [sitemap.h5](/inputs/supply_curve/sitemap.h5) +--- + + - [supplycurve_egs-reference.csv](/inputs/supply_curve/supplycurve_egs-reference.csv) +--- + + - [supplycurve_upv-limited.csv](/inputs/supply_curve/supplycurve_upv-limited.csv) + - **Description:** UPV supply curve from reV for the limited siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) + - **Units:** capacity numbers are in MW_DC and cost numbers are in $/MW_AC + +--- + + - [supplycurve_upv-open.csv](/inputs/supply_curve/supplycurve_upv-open.csv) + - **Description:** UPV supply curve from reV for the open siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) + - **Units:** capacity numbers are in MW_DC and cost numbers are in $/MW_AC + +--- + + - [supplycurve_upv-reference.csv](/inputs/supply_curve/supplycurve_upv-reference.csv) + - **Description:** UPV supply curve from reV for the reference siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) + - **Units:** capacity numbers are in MW_DC and cost numbers are in $/MW_AC + +--- + + - [supplycurve_wind-ofs-limited.csv](/inputs/supply_curve/supplycurve_wind-ofs-limited.csv) + - **Description:** Offshore sind supply curve from reV for the limited siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) +--- + + - [supplycurve_wind-ofs-open.csv](/inputs/supply_curve/supplycurve_wind-ofs-open.csv) + - **Description:** Offshore wind supply curve from reV for the open siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) +--- + + - [supplycurve_wind-ofs-reference.csv](/inputs/supply_curve/supplycurve_wind-ofs-reference.csv) + - **Description:** Offshore wind supply curve from reV for the reference siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) +--- + + - [supplycurve_wind-ons-limited.csv](/inputs/supply_curve/supplycurve_wind-ons-limited.csv) + - **Description:** Land-based wind supply curve from reV for the limited siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) +--- + + - [supplycurve_wind-ons-open.csv](/inputs/supply_curve/supplycurve_wind-ons-open.csv) + - **Description:** Land-based wind supply curve from reV for the open siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) +--- + + - [supplycurve_wind-ons-reference.csv](/inputs/supply_curve/supplycurve_wind-ons-reference.csv) + - **Description:** Land-based wind supply curve from reV for the reference siting scenario + - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv + - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) +--- + + - [trans_intra_cost_adder.csv](/inputs/supply_curve/trans_intra_cost_adder.csv) +--- + + + +#### inputs/techs + + - [tech_resourceclass.csv](/inputs/techs/tech_resourceclass.csv) +--- + + - [techs_default.csv](/inputs/techs/techs_default.csv) + - **Description:** List of technologies to be used in the model +--- + + - [techs_subsetForTesting.csv](/inputs/techs/techs_subsetForTesting.csv) + - **Description:** Short list of technologies for testing +--- + + + +#### inputs/temporal + + - [month2quarter.csv](/inputs/temporal/month2quarter.csv) +--- + + - [period_szn_user.csv](/inputs/temporal/period_szn_user.csv) +--- + + - [reeds_region_tz_map.csv](/inputs/temporal/reeds_region_tz_map.csv) +--- + + - [stressperiods_user.csv](/inputs/temporal/stressperiods_user.csv) +--- + + + +#### inputs/transmission + + - [cost_hurdle_country.csv](/inputs/transmission/cost_hurdle_country.csv) + - **File Type:** GAMS set + - **Description:** Cost for transmission hurdle rate by country + - **Indices:** country + - **Dollar year:** 2004 +--- + + - [cost_hurdle_intra.csv](/inputs/transmission/cost_hurdle_intra.csv) +--- + + - [rev_transmission_basecost.csv](/inputs/transmission/rev_transmission_basecost.csv) + - **File Type:** inputs + - **Description:** Unweighted average base cost across the four regions for which we have transmission cost data. + - **Indices:** Transreg + - **Dollar year:** 2004 +--- + + - [transmission_capacity_future_ba_baseline.csv](/inputs/transmission/transmission_capacity_future_ba_baseline.csv) + - **File Type:** inputs + - **Description:** Future transmission capacity additions for the baseline case at BA resolution + - **Indices:** r,rr +--- + + - [transmission_capacity_future_ba_default.csv](/inputs/transmission/transmission_capacity_future_ba_default.csv) + - **File Type:** inputs + - **Description:** Future transmission capacity additions for the default case at BA resolution + - **Indices:** r,rr +--- + + - [transmission_capacity_future_ba_LCC_all.csv](/inputs/transmission/transmission_capacity_future_ba_LCC_all.csv) + - **File Type:** inputs + - **Description:** Future transmission capacity additions for the LCC_all case at BA resolution + - **Indices:** r,rr +--- + + - [transmission_capacity_future_ba_VSC_all.csv](/inputs/transmission/transmission_capacity_future_ba_VSC_all.csv) + - **File Type:** inputs + - **Description:** Future transmission capacity additions for the VSC_all_case at BA resolution + - **Indices:** r,rr +--- + + - [transmission_capacity_future_county_baseline.csv](/inputs/transmission/transmission_capacity_future_county_baseline.csv) + - **File Type:** inputs + - **Description:** Future transmission capacity additions for the baseline case at county resolution + - **Indices:** r,rr +--- + + - [transmission_capacity_future_county_default.csv](/inputs/transmission/transmission_capacity_future_county_default.csv) + - **File Type:** inputs + - **Description:** Future transmission capacity additions for the default case at county resolution + - **Indices:** r,rr +--- + + - [transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629.csv](/inputs/transmission/transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629.csv) + - **File Type:** inputs + - **Description:** Future transmission capacity additions for the LCC_1000miles_demand1_wind1_subferc_20230629 case at BA resolution + - **Indices:** r,rr +--- + + - [transmission_capacity_init_AC_ba_NARIS2024.csv](/inputs/transmission/transmission_capacity_init_AC_ba_NARIS2024.csv) + - **Description:** Initial AC transmission capacity from the NARIS 2024 system at the BA resolution - 'NARIS2024' is a better starting point for future-oriented studies, but it becomes increasingly inaccurate for years earlier than 2024 +--- + + - [transmission_capacity_init_AC_ba_REFS2009.csv](/inputs/transmission/transmission_capacity_init_AC_ba_REFS2009.csv) + - **Description:** Initial AC transmission capacity from the 2009 transmission system for ReEDS at the BA resolution - 'REFS2009' does not include direction-dependent capacities or differentiated capacities for energy and PRM trading but it better represents historical additions between 2010-2024 +--- + + - [transmission_capacity_init_AC_county_NARIS2024.csv](/inputs/transmission/transmission_capacity_init_AC_county_NARIS2024.csv) + - **Description:** Initial AC transmission capacity modified from the NARIS 2024 file to eliminate most supply (with county transmission) demand mismatches for the 2024 solve year +--- + + - [transmission_capacity_init_AC_county_NARIS2024_base.csv](/inputs/transmission/transmission_capacity_init_AC_county_NARIS2024_base.csv) + - **Description:** Initial AC transmission capacity from the NARIS 2024 system at the county resolution +--- + + - [transmission_capacity_init_AC_transgrp_NARIS2024.csv](/inputs/transmission/transmission_capacity_init_AC_transgrp_NARIS2024.csv) + - **Description:** Initial AC transmission capacity from the NARIS 2024 system at the transgrp resolution +--- + + - [transmission_capacity_init_nonAC_ba.csv](/inputs/transmission/transmission_capacity_init_nonAC_ba.csv) + - **Description:** Initial non-AC transmission capacity at the BA resolution +--- + + - [transmission_capacity_init_nonAC_county.csv](/inputs/transmission/transmission_capacity_init_nonAC_county.csv) + - **Description:** Initial non-AC transmission capacity at the county resolution +--- + + - [transmission_cost_ac_500kv_ba.h5](/inputs/transmission/transmission_cost_ac_500kv_ba.h5) + - **Description:** Transmission costs for new 500 kV AC at BA resolution +--- + + - [transmission_cost_ac_500kv_county.h5](/inputs/transmission/transmission_cost_ac_500kv_county.h5) + - **Description:** Transmission costs for new 500 kV AC at county resolution +--- + + - [transmission_cost_dc_ba.csv](/inputs/transmission/transmission_cost_dc_ba.csv) + - **Description:** Transmission costs for new 500 kV DC at BA resolution +--- + + - [transmission_cost_dc_county.csv](/inputs/transmission/transmission_cost_dc_county.csv) + - **Description:** Transmission costs for new 500 kV DC at county resolution +--- + + - [transmission_distance_ba.h5](/inputs/transmission/transmission_distance_ba.h5) + - **Description:** Length of least-cost transmission paths between zones at BA resolution +--- + + - [transmission_distance_county.h5](/inputs/transmission/transmission_distance_county.h5) + - **Description:** Length of least-cost transmission paths between zones at county resolution +--- + + + +#### inputs/upgrades + + - [i_coolingtech_watersource_upgrades.csv](/inputs/upgrades/i_coolingtech_watersource_upgrades.csv) + - **File Type:** Inputs + - **Description:** List of cooling technologies for water sources that can be upgraded. + - **Indices:** i + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [i_coolingtech_watersource_upgrades_link.csv](/inputs/upgrades/i_coolingtech_watersource_upgrades_link.csv) + - **File Type:** Inputs + - **Description:** List of cooling technologies for water sources that can be upgraded + their to, from, ctt (cooling technology type) and wst (water source type) + - **Indices:** i, ctt, wst + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [upgrade_costs_ccs_coal.csv](/inputs/upgrades/upgrade_costs_ccs_coal.csv) +--- + + - [upgrade_costs_ccs_gas.csv](/inputs/upgrades/upgrade_costs_ccs_gas.csv) +--- + + - [upgrade_link.csv](/inputs/upgrades/upgrade_link.csv) + - **File Type:** Inputs + - **Description:** Techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta. + - **Indices:** i + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [upgrade_mult_atb23_ccs_adv.csv](/inputs/upgrades/upgrade_mult_atb23_ccs_adv.csv) + - **File Type:** Inputs + - **Description:** Cost adjustment (advanced) over various years for upgrade technologies + - **Indices:** i,t + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [upgrade_mult_atb23_ccs_con.csv](/inputs/upgrades/upgrade_mult_atb23_ccs_con.csv) + - **File Type:** Inputs + - **Description:** Cost adjustment (conservative) over various years for upgrade technologies + - **Indices:** i,t + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [upgrade_mult_atb23_ccs_mid.csv](/inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv) + - **File Type:** Inputs + - **Description:** Cost adjustment (Mid) over various years for upgrade technologies + - **Indices:** i,t + - **Dollar year:** N/A + - **Citation:** N/A +--- + + - [upgradelink_water.csv](/inputs/upgrades/upgradelink_water.csv) + - **File Type:** Inputs + - **Description:** Water techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta + - **Indices:** i + - **Dollar year:** N/A + - **Citation:** N/A +--- + + + +#### inputs/userinput + + - [futurefiles.csv](/inputs/userinput/futurefiles.csv) +--- + + - [ivt_default.csv](/inputs/userinput/ivt_default.csv) +--- + + - [ivt_small.csv](/inputs/userinput/ivt_small.csv) +--- + + - [ivt_step.csv](/inputs/userinput/ivt_step.csv) + - **Description:** ivt steps for endyears beyond 2050 +--- + + - [modeled_regions.csv](/inputs/userinput/modeled_regions.csv) + - **Description:** Sets of BA regions that a user can model in a run. Each column is a different region option and can be specified in cases using GSw_Region. +--- + + - [windows_2100.csv](/inputs/userinput/windows_2100.csv) + - **Description:** Window size for using window solve method to 2100 +--- + + - [windows_default.csv](/inputs/userinput/windows_default.csv) + - **Description:** Window size for using window solve method +--- + + - [windows_step10.csv](/inputs/userinput/windows_step10.csv) + - **Description:** Window size for beyond2050step10 +--- + + - [windows_step5.csv](/inputs/userinput/windows_step5.csv) + - **Description:** Window size for beyond2050step5 +--- + + + +#### inputs/valuestreams + + - [var_map.csv](/inputs/valuestreams/var_map.csv) +--- + + + +#### inputs/waterclimate + + - [cost_cap_mult.csv](/inputs/waterclimate/cost_cap_mult.csv) +--- + + - [cost_vom_mult.csv](/inputs/waterclimate/cost_vom_mult.csv) +--- + + - [heat_rate_mult.csv](/inputs/waterclimate/heat_rate_mult.csv) +--- + + - [i_coolingtech_watersource.csv](/inputs/waterclimate/i_coolingtech_watersource.csv) +--- + + - [i_coolingtech_watersource_link.csv](/inputs/waterclimate/i_coolingtech_watersource_link.csv) +--- + + - [tg_rsc_cspagg_tmp.csv](/inputs/waterclimate/tg_rsc_cspagg_tmp.csv) +--- + + - [unapp_water_sea_distr.csv](/inputs/waterclimate/unapp_water_sea_distr.csv) +--- + + - [wat_access_cap_cost.csv](/inputs/waterclimate/wat_access_cap_cost.csv) +--- + + - [water_req_psh_10h_1_51.csv](/inputs/waterclimate/water_req_psh_10h_1_51.csv) +--- + + - [water_with_cons_rate.csv](/inputs/waterclimate/water_with_cons_rate.csv) +--- + + + +### postprocessing + + - [example.csv](/postprocessing/example.csv) +--- + + + +#### postprocessing/air_quality + + - [scenarios.csv](/postprocessing/air_quality/scenarios.csv) +--- + + + +##### postprocessing/air_quality/rcm_data + + - [counties_ACS_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/counties_ACS_high_stack_2017.csv) +--- + + - [counties_H6C_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/counties_H6C_high_stack_2017.csv) +--- + + - [states_ACS_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/states_ACS_high_stack_2017.csv) +--- + + - [states_H6C_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/states_H6C_high_stack_2017.csv) +--- + + + +#### postprocessing/bokehpivot + + - [reeds_scenarios.csv](/postprocessing/bokehpivot/reeds_scenarios.csv) + - **Description:** Example data for ReEDS scenarios, each scenario with a custom style +--- + + + +##### postprocessing/bokehpivot/in + + - [example_custom_styles.csv](/postprocessing/bokehpivot/in/example_custom_styles.csv) + - **Description:** Examples of custom styles used for bokehpivot +--- + + - [example_data_US_electric_power_generation.csv](/postprocessing/bokehpivot/in/example_data_US_electric_power_generation.csv) + - **Description:** Example data for US electric power generation +--- + + - [gis_centroid_rb.csv](/postprocessing/bokehpivot/in/gis_centroid_rb.csv) +--- + + - [gis_nercr.csv](/postprocessing/bokehpivot/in/gis_nercr.csv) +--- + + - [gis_nercr_new.csv](/postprocessing/bokehpivot/in/gis_nercr_new.csv) +--- + + - [gis_rb.csv](/postprocessing/bokehpivot/in/gis_rb.csv) +--- + + - [gis_rs.csv](/postprocessing/bokehpivot/in/gis_rs.csv) +--- + + - [gis_rto.csv](/postprocessing/bokehpivot/in/gis_rto.csv) +--- + + - [gis_st.csv](/postprocessing/bokehpivot/in/gis_st.csv) +--- + + - [state_code.csv](/postprocessing/bokehpivot/in/state_code.csv) + - **Description:** Abbreviation and code for each state +--- + + + +###### postprocessing/bokehpivot/in/reeds2 + + - [class_map.csv](/postprocessing/bokehpivot/in/reeds2/class_map.csv) + - **Description:** Class mapping for bokehpivot postprocessing +--- + + - [class_style.csv](/postprocessing/bokehpivot/in/reeds2/class_style.csv) + - **Description:** Custom styles for classes in bokehpivot +--- + + - [con_adj_map.csv](/postprocessing/bokehpivot/in/reeds2/con_adj_map.csv) +--- + + - [con_adj_style.csv](/postprocessing/bokehpivot/in/reeds2/con_adj_style.csv) +--- + + - [cost_cat_map.csv](/postprocessing/bokehpivot/in/reeds2/cost_cat_map.csv) +--- + + - [cost_cat_style.csv](/postprocessing/bokehpivot/in/reeds2/cost_cat_style.csv) +--- + + - [ctt_map.csv](/postprocessing/bokehpivot/in/reeds2/ctt_map.csv) +--- + + - [ctt_style.csv](/postprocessing/bokehpivot/in/reeds2/ctt_style.csv) +--- + + - [hours.csv](/postprocessing/bokehpivot/in/reeds2/hours.csv) + - **Description:** Hours for each of the 17 timeslices +--- + + - [m_bar_width.csv](/postprocessing/bokehpivot/in/reeds2/m_bar_width.csv) +--- + + - [m_map.csv](/postprocessing/bokehpivot/in/reeds2/m_map.csv) +--- + + - [m_style.csv](/postprocessing/bokehpivot/in/reeds2/m_style.csv) +--- + + - [process_style.csv](/postprocessing/bokehpivot/in/reeds2/process_style.csv) +--- + + - [tech_ctt_wst.csv](/postprocessing/bokehpivot/in/reeds2/tech_ctt_wst.csv) +--- + + - [tech_map.csv](/postprocessing/bokehpivot/in/reeds2/tech_map.csv) +--- + + - [tech_style.csv](/postprocessing/bokehpivot/in/reeds2/tech_style.csv) + - **Description:** Custom colors for each technology used by bokehpivot +--- + + - [trtype_map.csv](/postprocessing/bokehpivot/in/reeds2/trtype_map.csv) +--- + + - [trtype_style.csv](/postprocessing/bokehpivot/in/reeds2/trtype_style.csv) +--- + + - [wst_map.csv](/postprocessing/bokehpivot/in/reeds2/wst_map.csv) +--- + + - [wst_style.csv](/postprocessing/bokehpivot/in/reeds2/wst_style.csv) +--- + + + +#### postprocessing/combine_runs + + - [combinefiles.csv](/postprocessing/combine_runs/combinefiles.csv) +--- + + - [runlist.csv](/postprocessing/combine_runs/runlist.csv) +--- + + + +#### postprocessing/land_use + + + +##### postprocessing/land_use/inputs + + - [federal_land_categories.csv](/postprocessing/land_use/inputs/federal_land_categories.csv) +--- + + - [field_definitions.csv](/postprocessing/land_use/inputs/field_definitions.csv) +--- + + - [nlcd_categories.csv](/postprocessing/land_use/inputs/nlcd_categories.csv) +--- + + - [nlcd_combined_categories.csv](/postprocessing/land_use/inputs/nlcd_combined_categories.csv) +--- + + - [usgs_categories.csv](/postprocessing/land_use/inputs/usgs_categories.csv) +--- + + - [usgs_combined_categories.csv](/postprocessing/land_use/inputs/usgs_combined_categories.csv) +--- + + + +#### postprocessing/plots + + - [scghg_annual.csv](/postprocessing/plots/scghg_annual.csv) +--- + + - [transmission-interface-coords.csv](/postprocessing/plots/transmission-interface-coords.csv) +--- + + + +#### postprocessing/retail_rate_module + + - [capital_financing_assumptions.csv](/postprocessing/retail_rate_module/capital_financing_assumptions.csv) +--- + + - [df_f861_contiguous.csv](/postprocessing/retail_rate_module/df_f861_contiguous.csv) +--- + + - [df_f861_state.csv](/postprocessing/retail_rate_module/df_f861_state.csv) +--- + + - [inputs.csv](/postprocessing/retail_rate_module/inputs.csv) +--- + + - [inputs_default.csv](/postprocessing/retail_rate_module/inputs_default.csv) +--- + + - [load_by_state_eia.csv](/postprocessing/retail_rate_module/load_by_state_eia.csv) + - **Description:** End use load by state since 1960 +--- + + - [map_i_to_tech.csv](/postprocessing/retail_rate_module/map_i_to_tech.csv) + - **Description:** Maps i to tech with custom coloring for each +--- + + + +##### postprocessing/retail_rate_module/calc_historical_capex + + - [existing_transmission_cost_bystate_USD2024.csv](/postprocessing/retail_rate_module/calc_historical_capex/existing_transmission_cost_bystate_USD2024.csv) +--- + + + +##### postprocessing/retail_rate_module/inputs + + - [Electric O & M Expenses-IOU-1993-2019.csv](/postprocessing/retail_rate_module/inputs/Electric%20O%20&%20M%20Expenses-IOU-1993-2019.csv) +--- + + - [Electric Operating Revenues-IOU-1993-2019.csv](/postprocessing/retail_rate_module/inputs/Electric%20Operating%20Revenues-IOU-1993-2019.csv) +--- + + - [Electric Plant in Service-IOU-1993-2019.csv](/postprocessing/retail_rate_module/inputs/Electric%20Plant%20in%20Service-IOU-1993-2019.csv) +--- + + - [f861_cust_counts.csv](/postprocessing/retail_rate_module/inputs/f861_cust_counts.csv) +--- + + - [overwrite-utility-energy_sales.csv](/postprocessing/retail_rate_module/inputs/overwrite-utility-energy_sales.csv) +--- + + - [state-meanbiaserror_rate-aggregation.csv](/postprocessing/retail_rate_module/inputs/state-meanbiaserror_rate-aggregation.csv) +--- + + - [Table_9.8_Average_Retail_Prices_of_Electricity.xlsx](/postprocessing/retail_rate_module/inputs/Table_9.8_Average_Retail_Prices_of_Electricity.xlsx) + - **Description:** Historical EIA861 rates (annual and monthly) +--- + + + +#### postprocessing/reValue + + - [scenarios.csv](/postprocessing/reValue/scenarios.csv) +--- + + + +#### postprocessing/tableau + + - [tables_to_aggregate.csv](/postprocessing/tableau/tables_to_aggregate.csv) +--- + + + +### preprocessing + + + +#### preprocessing/atb_updates_processing + + + +##### preprocessing/atb_updates_processing/input_files + + - [batt_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/batt_plant_char_format.csv) +--- + + - [conv_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/conv_plant_char_format.csv) +--- + + - [csp_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/csp_plant_char_format.csv) +--- + + - [geo_fom_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/geo_fom_plant_char_format.csv) +--- + + - [h2-combustion_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/h2-combustion_plant_char_format.csv) + - **Description:** Plant characteristics for which the H2-CC and CT ATB estimates are made using Gas-CC and CT data in preprocessing +--- + + - [ofs-wind_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/ofs-wind_plant_char_format.csv) +--- + + - [ofs-wind_rsc_mult_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/ofs-wind_rsc_mult_plant_char_format.csv) +--- + + - [ons-wind_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/ons-wind_plant_char_format.csv) +--- + + - [upv_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/upv_plant_char_format.csv) +--- + + + +### reeds2pras + + + +#### reeds2pras/test + + + +##### reeds2pras/test/reeds_cases + + + +###### reeds2pras/test/reeds_cases/test + + - [cases_reeds2pras.csv](/reeds2pras/test/reeds_cases/test/cases_reeds2pras.csv) +--- + + - [meta.csv](/reeds2pras/test/reeds_cases/test/meta.csv) +--- + + + +###### reeds2pras/test/reeds_cases/test/inputs_case + + - [hydcapadj.csv](/reeds2pras/test/reeds_cases/test/inputs_case/hydcapadj.csv) +--- + + - [hydcf.csv](/reeds2pras/test/reeds_cases/test/inputs_case/hydcf.csv) +--- + + - [mttr.csv](/reeds2pras/test/reeds_cases/test/inputs_case/mttr.csv) +--- + + - [outage_forced_hourly.h5](/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_hourly.h5) +--- + + - [outage_forced_static.csv](/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_static.csv) +--- + + - [outage_scheduled_hourly.h5](/reeds2pras/test/reeds_cases/test/inputs_case/outage_scheduled_hourly.h5) +--- + + - [resources.csv](/reeds2pras/test/reeds_cases/test/inputs_case/resources.csv) +--- + + - [tech-subset-table.csv](/reeds2pras/test/reeds_cases/test/inputs_case/tech-subset-table.csv) +--- + + - [unitdata.csv](/reeds2pras/test/reeds_cases/test/inputs_case/unitdata.csv) +--- + + - [unitsize.csv](/reeds2pras/test/reeds_cases/test/inputs_case/unitsize.csv) +--- + + + +###### reeds2pras/test/reeds_cases/test/ReEDS_Augur + + + +###### reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data + + - [cap_converter_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/cap_converter_2035.csv) +--- + + - [charge_eff_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/charge_eff_2035.csv) +--- + + - [discharge_eff_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/discharge_eff_2035.csv) +--- + + - [energy_cap_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/energy_cap_2035.csv) +--- + + - [max_cap_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_cap_2035.csv) +--- + + - [max_unitsize_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_unitsize_2035.csv) +--- + + - [pras_load_2035.h5](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_load_2035.h5) +--- + + - [pras_vre_gen_2035.h5](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_vre_gen_2035.h5) +--- + + - [tran_cap_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/tran_cap_2035.csv) +--- + + + +### ReEDS_Augur + + - [augur_switches.csv](/ReEDS_Augur/augur_switches.csv) +--- + + + +### tests + + + +#### tests/data + + + +##### tests/data/county + + - [csp.h5](/tests/data/county/csp.h5) + - **Description:** Subset of county-level data for the github runner county test +--- + + - [distpv.h5](/tests/data/county/distpv.h5) + - **Description:** Subset of county-level data for the github runner county test +--- + + - [upv.h5](/tests/data/county/upv.h5) + - **Description:** Subset of county-level data for the github runner county test +--- + + - [wind-ofs.h5](/tests/data/county/wind-ofs.h5) + - **Description:** Subset of county-level data for the github runner county test +--- + + - [wind-ons.h5](/tests/data/county/wind-ons.h5) + - **Description:** Subset of county-level data for the github runner county test +--- + + +## Files + +- [cases.csv](/cases.csv) + - **File Type:** Switches file + - **Description:** Contains the configuration settings for the ReEDS run(s). + - **Dollar year:** 2004 + - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv](https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv) +--- + +- [cases_examples.csv](/cases_examples.csv) +--- + +- [cases_small.csv](/cases_small.csv) + - **Description:** Contains settings to run ReEDS at a smaller scale to test operability of the ReEDS model. Turns off several technologies and reduces the model size to significantly improve solve times. +--- + +- [cases_standardscenarios.csv](/cases_standardscenarios.csv) + - **File Type:** StdScen Cases file + - **Description:** Contains the configuration settings for the Standard Scenarios ReEDS runs. +--- + +- [cases_test.csv](/cases_test.csv) + - **Description:** Contains the configuration settings for doing test runs including the default Pacific census division test case. +--- + +- [e_report_params.csv](/e_report_params.csv) + - **Description:** Contains a parameter list used in the model along with descriptions of what they are and units used. +--- + +- [runfiles.csv](/runfiles.csv) + - **Description:** Contains the locations of input data that is copied from the repository into the runs folder for each respective case. +--- + +- [sources.csv](/sources.csv) + - **Description:** CSV file containing a list of all input files (csv, h5, csv.gz) +--- diff --git a/helpers/interim_report.py b/helpers/interim_report.py new file mode 100644 index 00000000..d4bce155 --- /dev/null +++ b/helpers/interim_report.py @@ -0,0 +1,60 @@ +#%%### Imports +import os +import sys +import subprocess +import argparse +import pandas as pd +from glob import glob + + +#%%### Arugment inputs +parser = argparse.ArgumentParser( + description='Run reporting scripts on latest completed year', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, +) +parser.add_argument( + 'casepath', type=str, nargs='?', default='', + help='path to ReEDS case', +) +parser.add_argument( + '--only', '-o', type=str, default='', + help=',-delimited list of strings; only run reports that contain provided strings' +) +args = parser.parse_args() +casepath = args.casepath +only = [i for i in args.only.split(',') if len(i)] +if not len(casepath): + ### If the user provides a case path, switch to it; otherwise assume we're already there + casepath = os.path.dirname(os.path.abspath(__file__)) + + +#%%### Procedure +### Move to casepath +os.chdir(casepath) + +### Get model execution file +files = glob('*') +callfile = [c for c in files if os.path.basename(c).startswith('call')][0] +### Get final year_iteration that the model plans to run +final_year = int(pd.read_csv(os.path.join('inputs_case','modeledyears.csv')).columns[-1]) +final_year_iteration = f'{final_year}i0' + +#%% Get the lines to run +commands = [] +start_copying = 0 +with open(callfile, 'r') as f: + for line in f: + if ('# Output processing' in line) or start_copying: + start_copying = 1 + if (line.strip() != '') and not (line.strip().startswith('#')): + commands.append(line.strip()) + +### If "only" strings are specified, only include them (and cd lines) +if len(only): + commands = [cmd for cmd in commands if any([s in cmd for s in only+['cd ']])] + +#%% Run it +result = subprocess.run( + '\n'.join(commands), shell=True, + stderr=sys.stderr, stdout=sys.stdout, +) diff --git a/helpers/interim_report_batch.py b/helpers/interim_report_batch.py new file mode 100644 index 00000000..4845e67b --- /dev/null +++ b/helpers/interim_report_batch.py @@ -0,0 +1,70 @@ +''' +This script allows for batch re-running of just the reporting +functions (report.gms and report_dump.py) for a set of jobs +with a common batch prefix, specified as a command line argument. +It will make new copies of report.gms/report_dump.py to +each run folder, and can thus be useful to re-run reporting +for a large set of existing runs using new report scripts. + +It calls `interim_report.py` so can also be useful for reporting +results from runs that have not completed all years. + +author: bsergi +''' + +import os +import subprocess +import shutil +from glob import glob +import argparse +import sys + +parser = argparse.ArgumentParser() +parser.add_argument('--batch_name', '-b', type=str, default='', help='Prefix for batch of runs') +args = parser.parse_args() +# check if on hpc +hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False + +# get list of cases with matching batch name +case_list = glob(os.path.join('runs', '*')) +case_list = [c for c in case_list if args.batch_name in os.path.basename(c)] +if len(case_list) == 0: + sys.exit(f"No cases found with {args.batch_name} prefix.") +# list of new report files to copy +report_files = ["report.gms", "report_dump.py"] + +# loop over cases to copy files and run 'interim_report.py' for each one +for case in case_list: + # copy new report scripts into run folder + for f in report_files: + shutil.copy(f, os.path.join(case, f)) + # call runs + case_name = os.path.basename(case) + print(f"Running interim_report.py for {case_name}") + interim_report = os.path.join(case, "interim_report.py") + if os.name=='posix': + if hpc: + shutil.copy("srun_template.sh", os.path.join(case, "interim_report.sh")) + with open(os.path.join(case, "interim_report.sh"), 'a') as SPATH: + #add the name for easy tracking of the case + SPATH.writelines("\n#SBATCH --job-name=" + case_name + "_interim_report" + "\n\n") + + # load environments + SPATH.writelines("\nmodule purge\n") + SPATH.writelines("module load conda\n") + SPATH.writelines("conda activate reeds2\n") + SPATH.writelines("module load gams\n\n\n") + + #add the call to the python file to run the report + SPATH.writelines("python " + os.path.join(case, "interim_report.py")) + #close the file + SPATH.close() + #call that file + batchcom = "sbatch " + os.path.join(case, "interim_report.sh") + subprocess.Popen(batchcom.split()) + else: + os.chmod(interim_report, 0o777) + shellscript = subprocess.Popen(["python", interim_report]) + shellscript.wait() + else: + os.system('start /wait cmd /c ' + interim_report) diff --git a/helpers/restart_runs.py b/helpers/restart_runs.py new file mode 100644 index 00000000..37174055 --- /dev/null +++ b/helpers/restart_runs.py @@ -0,0 +1,180 @@ +#%% Imports +import os +import shutil +import subprocess +import argparse +import pandas as pd +from glob import glob +from runbatch import submit_slurm_parallel_jobs +from runstatus import get_run_status + +#%% Argument inputs +parser = argparse.ArgumentParser(description='Restart failed runs on the HPC') +parser.add_argument('batch_name', type=str, help='batch name (case prefix) to search for') +parser.add_argument('--copy_cplex', '-c', type=int, default=0, + help='Which cplex.opt file to copy (or 0 for none)') +parser.add_argument('--copy_srun_template', '-s', action='store_true', + help='Copy current srun_template.sh to sbatch file') +parser.add_argument('--force', '-f', action='store_true', + help='Proceed without double-checking') +parser.add_argument('--more_copyfiles', '-m', type=str, default='', + help=',-delimited list of additional files to copy from reeds_path') +parser.add_argument('--include_finished', '-i', action='store_true', + help='Also restart finished runs (e.g. to redo postprocessing)') + +args = parser.parse_args() +batch_name = args.batch_name +copy_cplex = args.copy_cplex +copy_srun_template = args.copy_srun_template +force = args.force +more_copyfiles = [i for i in args.more_copyfiles.split(',') if len(i)] +include_finished = args.include_finished + +# #%% Inputs for debugging +# batch_name = 'v20231113_yamM0' +# copy_cplex = 1 +# copy_srun_template = True +# force = True +# more_copyfiles = ['report.gms'] +# include_finished = False + +###### Procedure +#%% Shared parameters +reeds_path = os.path.dirname(os.path.abspath(__file__)) +#%% Get all runs +dictruns = get_run_status(reeds_path, batch_name) + +runs_unfinished = dictruns['running'] + dictruns['failed'] +runs_failed = dictruns['failed'] +runs_running = dictruns['running'] + +### Take a look +print('unfinished:', len(runs_unfinished)) +print('running:', len(runs_running)) +print('failed:', len(runs_failed)) + +#%% Double check +if not force: + for i in runs_failed: + print(os.path.basename(i)) + print(f'Restarting the {len(runs_failed)} runs listed above.') + confirm_local = str(input('Proceed? [y]/n: ') or 'y') + if confirm_local not in ['y','Y','yes','Yes','YES']: + quit() + + +#%% Get the cplex file to copy +if copy_cplex: + if copy_cplex == 1: + cplex_file = os.path.join(reeds_path,'cplex.opt') + else: + cplex_file = os.path.join(reeds_path,f'cplex.op{copy_cplex}') +else: + cplex_file = None + +#%% Copy the header from the srun_template.sh file if desired +if copy_srun_template: + srun_template = os.path.join(reeds_path,'srun_template.sh') + writelines_srun = list() + with open(srun_template, 'r') as f: + for line in f: + writelines_srun.append(line.strip()) +else: + writelines_srun = list() + +#%%### Loop through runs, figure out when they failed, and restart +for case in runs_failed: + casename = os.path.basename(case) + + #%% Copy the cplex file if desired + if copy_cplex: + shutil.copy(cplex_file, os.path.join(case,'')) + + #%% Copy additional files if desired + for f in more_copyfiles: + shutil.copy(os.path.join(reeds_path,f), os.path.join(case,f)) + + #%% Make a backup copy of the original bash and sbatch scripts + callfile = os.path.join(case,f'call_{casename}.sh') + shutil.copy(callfile, os.path.join(case,f'ORIGINAL_call_{casename}.sh')) + + sbatchfile = os.path.join(case,f'{casename}.sh') + shutil.copy(sbatchfile, os.path.join(case,f'ORIGINAL_{casename}.sh')) + + #%% Get last .lst file and restart from there + lstfiles = sorted(glob(os.path.join(case,'lstfiles','*.lst'))) + if any([os.path.basename(i).startswith('report') for i in lstfiles]): + restart_tag = '# Output processing' + elif len(lstfiles) < 2: + # If there is only 1 lst file, then it is an environment.csv so the run failed during inputs processing + restart_tag = '# Input processing' + elif len(lstfiles) == 2: + # If there are only 2 lst files, then one of them will be environment.csv and the other will be 1_inputs.lst so the run failed during the model compilation + restart_tag = '# Compile model' + else: + # Drop environment and inputs .lst files + lstfiles = [l for l in lstfiles if ("environment.csv" not in l) and ('1_Inputs.lst' not in l)] + lastfile = lstfiles[-1] + restart_year = int(os.path.splitext(lastfile)[0].split('_')[-1].split('i')[0]) + restart_tag = f'# Year: {restart_year}' + + #%% Comment out the unnecessary lines + writelines = [] + with open(callfile, 'r') as f: + comment = 0 + for line in f: + ## Start commenting at input processing + if '# Input processing' in line: + comment = 1 + ## Stop commenting at restart_tag + if line.startswith(restart_tag): + comment = 0 + ## Record it + writelines.append(('# ' if comment else '') + line.strip()) + + ### Write it + with open(callfile, 'w') as f: + for line in writelines: + f.writelines(line + '\n') + +# Check if we are going to run this in parallel or not +hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False +if hpc and len(runs_failed) > 1: + # On HPC with multiple cases + cases_per_node = int(input('Number of simultaneous runs per node [integer]: ')) +else: + cases_per_node = 1 + +if hpc and (cases_per_node > 1): + # Write the slurm scripts for parallel runs and + # submit them to the HPC + casenames = [os.path.basename(p).split(batch_name + "_", 1)[-1] for p in runs_failed] + submit_slurm_parallel_jobs( + reeds_path=reeds_path, + BatchName=batch_name, + casenames=casenames, + cases_per_node=cases_per_node, + ) + +else: + # Run each case individually + for case in runs_failed: + casename = os.path.basename(case) + callfile = os.path.join(case, f'call_{casename}.sh') + sbatchfile = os.path.join(case, f'{casename}.sh') + # It is a single case or we are not on HPC + if copy_srun_template: + writelines_srun_case = writelines_srun.copy() + writelines_srun_case.append(f"\n#SBATCH --job-name={casename}\n") + writelines_srun_case.append(f"sh {callfile}") + with open(sbatchfile, 'w') as f: + for line in writelines_srun_case: + f.writelines(line + '\n') + + #%% Run it + sbatch = f'sbatch {sbatchfile}' + sbatchout = subprocess.run(sbatch, capture_output=True, shell=True) + + if len(sbatchout.stderr): + print(sbatchout.stderr.decode()) + print(f"{casename}: {sbatchout.stdout.decode()}") diff --git a/helpers/runstatus.py b/helpers/runstatus.py new file mode 100644 index 00000000..b749aae3 --- /dev/null +++ b/helpers/runstatus.py @@ -0,0 +1,187 @@ +#%% Imports +import os +import re +import datetime +import subprocess +import argparse +from glob import glob + +# #%% Inputs for debugging +# batch_name = 'v20250812_mcK0' +# include_finished = False +# verbose = 0 + +#%%### Functions +def parse_multiple_runs_per_node(runs_running): + expanded_runs = [] + for i in runs_running: + ## Matches the form used for multiple runs per node: foo_(bar,baz[,etc]) + if re.match('^\w+_\(\w+,\w+(,\w+)*\)$', i): + batch_ = i.split('(')[0] + constituents = i.split('(')[1].strip(')').split(',') + expanded_runs.extend([batch_+c for c in constituents]) + ## Otherwise it's a normal run + else: + expanded_runs.append(i) + return sorted(expanded_runs) + + +def print_log_if_verbose(fullcase, verbose=0): + if verbose: + gamslog = os.path.join(fullcase, 'gamslog.txt') + print('vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv') + subprocess.run(f'tail {gamslog} -n {verbose}', shell=True) + print('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n') + +def get_run_status(reeds_path, batch_name): + #%% Get active runs + sq = f'squeue -u {os.environ["USER"]} -o "%.200j"' + sqout = subprocess.run(sq, capture_output=True, shell=True) + runs_running_all = [os.path.splitext(i.decode())[0] for i in sqout.stdout.split()] + + #%% If no batch_name is provided, use the pre-underscore text from the last run + if not len(batch_name): + batch_name = sorted(runs_running_all)[-1].split('_')[0] + print(f'Runs with batch_name = {batch_name}:') + + #%% Get all runs + runs_all = sorted(glob(os.path.join(reeds_path,'runs',batch_name+'*'))) + ### Identify finished runs + runs_finished = [ + i for i in runs_all + if os.path.exists(os.path.join(i, 'outputs', 'reeds-report', 'report.xlsx')) + ] + ### Keep unfinished runs + runs_unfinished = [i for i in runs_all if i not in runs_finished] + + ### Get failed runs by identifying and excluding active runs + runs_running_unparsed = [i for i in runs_running_all if i.startswith(batch_name)] + runs_running = parse_multiple_runs_per_node(runs_running_unparsed) + + # If a run is finished but on a shared node with another run still going, + # drop it from the running list + runs_running = [i for i in runs_running if os.path.join(reeds_path,'runs',i) not in runs_finished] + runs_failed = [i for i in runs_unfinished if os.path.basename(i) not in runs_running] + + ### Store the runs + dictruns = { + 'finished': [os.path.join(reeds_path,'runs',os.path.basename(i)) for i in runs_finished], + 'running': [os.path.join(reeds_path,'runs',os.path.basename(i)) for i in runs_running], + 'failed': [os.path.join(reeds_path,'runs',os.path.basename(i)) for i in runs_failed], + } + ## Only keep running runs in the current repo + dictruns['running'] = [i for i in dictruns['running'] if os.path.isdir(i)] + + return dictruns + +#%%### Procedure +if __name__ == '__main__': + + #%% Argument inputs + parser = argparse.ArgumentParser(description='Print status of runs on the HPC') + parser.add_argument('batch_name', type=str, nargs='?', default='', + help='batch name (case prefix) to search for') + parser.add_argument('--include_finished', '-f', action='store_true', + help='Include finished runs in response') + parser.add_argument('--verbose', '-v', action='count', default=0, + help='How many tail lines to print from gamslog.txt') + + args = parser.parse_args() + batch_name = args.batch_name + include_finished = args.include_finished + verbose = args.verbose + + #%% Shared parameters + reeds_path = os.path.dirname(os.path.abspath(__file__)) + dictruns = get_run_status(reeds_path, batch_name) + + #%%### Loop through categories and runs and report their status + for key, runs in dictruns.items(): + text = f'{key}: {len(runs)}' + print(f"\n{text}\n{'-'*len(text)}") + ### Loop through runs + try: + longest = max([len(os.path.basename(i)) for i in runs]) + except ValueError: + longest = 0 + for fullcase in runs: + case = os.path.basename(fullcase) + if (key == 'finished'): + if include_finished: + import pandas as pd + duration = pd.read_csv( + os.path.join(fullcase,'meta.csv'), skiprows=3).processtime.sum() + print(f"{case:<{longest}}: {datetime.timedelta(seconds=int(duration))}") + else: + ### Get last .lst file + lstfiles = sorted(glob(os.path.join(fullcase,'lstfiles','*'))) + if any([os.path.basename(i).startswith('report') for i in lstfiles]): + last_lst = 'report.gms' + penultimatefile = None + else: + if len(lstfiles) > 1: + # Drop environment file + lstfiles = [ + line for line in lstfiles if ( + ('environment.csv' not in line) + and ('mcs_group_weights.csv' not in line) + ) + ] + try: + lastfile = lstfiles[-1] + except IndexError: + print(f"{case:<{longest}}: failed in input processing") + print_log_if_verbose(fullcase, verbose) + continue + try: + # Get time since previous lst file was modified + penultimatefile = lstfiles[-2] + penultimateyear = os.path.splitext(penultimatefile)[0].split('_')[-1] + lasttime = os.path.getmtime(penultimatefile) + nowtime = datetime.datetime.now().timestamp() + duration = datetime.timedelta(seconds=int((nowtime - lasttime))) + except IndexError: + penultimatefile = None + last_lst = os.path.splitext(lastfile)[0].split('_')[-1] + + if (key == 'running'): + + # check if PRAS is stalled + logfile = os.path.join(fullcase,'gamslog.txt') + with open(logfile, "r") as file: + gamslog = file.readlines() + # only look at last 5 lines in case there was a restart + gamslog = ''.join(gamslog[-5:]) + if "signal (6): Aborted" in gamslog: + errortext = "(WARNING: PRAS may be stalled, check gamslog)" + else: + errortext = "" + if penultimatefile: + print( + f"{case:<{longest}}: running {last_lst} " + f"({duration} since {penultimateyear} finished) " + f"{errortext}" + ) + else: + print(f"{case:<{longest}}: running {last_lst} {errortext}") + elif (key == 'failed'): + # add some details on the runs that failed by reading the slurm file + slurmfile = sorted(glob(os.path.join(fullcase,'slurm*.out')))[-1] + with open(slurmfile, "r") as file: + slurm = file.read() + # check if timed out + if "CANCELLED AT" in slurm and "DUE TO TIME LIMIT" in slurm: + errortext = "(timed out)" + # check if dual objective limit was reached + elif "dual objective limit exceeded" in slurm: + errortext = "(hit dual obj. limit)" + # check if infeasible + elif "d_solveoneyear.gms failed with return code 3" in slurm: + errortext = "(infeasible)" + else: + errortext = "" + print(f"{case:<{longest}}: failed in {last_lst} {errortext}") + else: + print(f"Unrecognized key for {case}: {key}") + + print_log_if_verbose(fullcase, verbose) diff --git a/hourlize/.pylintrc b/hourlize/.pylintrc new file mode 100644 index 00000000..7dc59014 --- /dev/null +++ b/hourlize/.pylintrc @@ -0,0 +1,473 @@ +# This file was created based on https://github.com/NatLabRockies/reVXOrdinances/blob/main/.pylintrc, +# and then modified for the use with ReEDS-2.0 +[MAIN] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CSV, config, data + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-paths=^.*hourlize/(?!(?:reeds_to_rev)|(?:test)).*?py$, + ^.*hourlize/tests/data/.*$ + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable= + import-error, + unspecified-encoding, + fixme, + too-many-locals, + import-error, + too-many-instance-attributes, + logging-fstring-interpolation, + no-else-break + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[BASIC] + +# Naming style matching correct argument names +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style +#argument-rgx= + +# Naming style matching correct attribute names +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style +#class-attribute-rgx= + +# Naming style matching correct class names +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming-style +#class-rgx= + +# Naming style matching correct constant names +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma +good-names=reVXOrdinances, + e, + i, + j, + s, + o, + df, + n, + c, + fh, + h, + sw, + yr, + r, + f + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=yes + +# Naming style matching correct inline iteration names +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style +#inlinevar-rgx= + +# Naming style matching correct method names +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style +#method-rgx= + +# Naming style matching correct module names +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=50000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=yes + +# Minimum lines number of a similarity. +min-similarity-lines=15 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=10 + +# Maximum number of attributes for a class (see R0902). +max-attributes=10 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=builtins.Exception diff --git a/hourlize/README.md b/hourlize/README.md index faf38431..80d2565c 100644 --- a/hourlize/README.md +++ b/hourlize/README.md @@ -185,7 +185,7 @@ The `resource.py` script follows the following logic (in order of execution): * Existing and planned sites from a generator database (`existing_sites`) are assigned to supply curve points for exogenous and prescribed capacity outputs respectively. * If we have minimum capacity thresholds for the supply curve points, these are applied to further filter the supply curve. 1. `add_classes()` - * A 'class' column is added to the supply curve and filled with the associated class. Classes can be based on statically defined conditions for columns in the supply curve (`class_path`). Otherwise (or layered on top of static class definitions), dynamic classes can be assigned (`class_bin`=true) using a binning method (`class_bin_method`, e.g. "kmeans"), a number of bins (`class_bin_num`), and the supply curve column to bin (`class_bin_col`). The binning logic itself is in `reeds.inputs.get_bin()`. The current default classes for onshore wind and utility-scale PV are based on national k-means clustering of average annual capacity factor (where higher class number corresponds with higher annual CF). Offshore wind, by contrast, uses statically defined classes from `hourlize/inputs/resource/wind-ofs_resource_classes.csv`. + * A 'class' column is added to the supply curve and filled with the associated class. Classes can be based on statically defined conditions for columns in the supply curve (`class_path`). Otherwise (or layered on top of static class definitions), dynamic classes can be assigned (`class_bin`=true) using a binning method (`class_bin_method`, e.g. "kmeans"), a number of bins (`class_bin_num`), and the supply curve column to bin (`class_bin_col`). The binning logic itself is in `reeds.parse.get_bin()`. The current default classes for onshore wind and utility-scale PV are based on national k-means clustering of average annual capacity factor (where higher class number corresponds with higher annual CF). Offshore wind, by contrast, uses statically defined classes from `hourlize/inputs/resource/wind-ofs_resource_classes.csv`. 1. `add_cost()` * A column of overall supply curve costs is added to the supply curve (`supply_curve_cost_per_mw`), as well as certain components of that cost (e.g. `trans_adder_per_mw` and `capital_adder_per_mw`). Logic for these costs depends on `tech`, and the value of `cost_out` in config (e.g. `combined_eos_trans` for onshore wind). * A column of overall supply curve costs is added to the supply curve (`supply_curve_cost_per_mw`), as well as certain components of that cost (e.g. `trans_adder_per_mw` and `capital_adder_per_mw`). Logic for these costs depends on `tech`, and the value of `cost_out` in config (e.g. `combined_eos_trans` for onshore wind). diff --git a/hourlize/pyproject.toml b/hourlize/pyproject.toml new file mode 100644 index 00000000..debef993 --- /dev/null +++ b/hourlize/pyproject.toml @@ -0,0 +1,20 @@ +[tool.black] +include = ''' +( + hourlize/tests/.*\.py?$ + | hourlize/reeds_to_rev\.py?$ +) +''' + +[project] +name = "ReEDS" +version = "2024.0.0" + +[project.optional-dependencies] +docs = [ + "sphinx", + "myst-parser", +] + + + diff --git a/hourlize/requirements_dev.txt b/hourlize/requirements_dev.txt new file mode 100644 index 00000000..21614b6e --- /dev/null +++ b/hourlize/requirements_dev.txt @@ -0,0 +1,3 @@ +pre-commit>=3.1.1,<3.2 +pylint>=2.17.0,<2.18 +pytest>=7.2.2,<7.3 diff --git a/hourlize/resource.py b/hourlize/resource.py index 1566e5f5..62064b96 100644 --- a/hourlize/resource.py +++ b/hourlize/resource.py @@ -541,7 +541,7 @@ def add_classes(df_sc, class_path, class_bin, class_bin_col, class_bin_method, c df_sc .groupby(['class_orig'], sort=False) .apply( - reeds.inputs.get_bin, + reeds.parse.get_bin, bin_col=class_bin_col, bin_out_col='class_bin', weight_col='capacity', diff --git a/inputs/national_generation/README.md b/inputs/national_generation/README.md index 566df849..5a058f10 100644 --- a/inputs/national_generation/README.md +++ b/inputs/national_generation/README.md @@ -57,7 +57,7 @@ For existing coal plants, this is the code implementation: This is the emissions rate (metric tons CO2 per MWh) equivalent to average emissions from a new coal-CCS plant, assuming 90% capture rate. The emissions rate from a new coal-CCS plant in ReEDS is 0.051956 metric tons CO2 per MWh (see `emit_rate` parameter) which assumes 95% capture. For 90% capture, the emissions rate is double that or 0.1039 metric tons CO2 per MWh, which we use as the standard. -2. `input_processing/WriteHintage.py` +2. `reeds/inputs/WriteHintage.py` - Coal plants are binned at the unit level if `GSw_Clean_Air_Act=1` so that each coal unit can independently choose to retire or upgrade. - Coal plants maintain their exogenous retirement assumption, except after 2032, when the Clean Air Act regulations begin and coal can retire endogenously. For example, if the NEMS data states that a plant will retire in 2029, we maintain that assumption and that plant will retire in 2029. diff --git a/inputs/transmission/README.md b/inputs/transmission/README.md index e349a7d7..8f3454af 100644 --- a/inputs/transmission/README.md +++ b/inputs/transmission/README.md @@ -33,7 +33,7 @@ Calculated using the [TSC](https://github.nrel.gov/pbrown/TSC) model as describe import reeds ## GSw_ZoneSet can be any of the supported zone resolutions listed in the `GSw_ZoneSet` row of `cases.csv` GSw_ZoneSet = 'z134' - reeds.inputs.get_itls(GSw_ZoneSet=GSw_ZoneSet) + reeds.parse.get_itls(GSw_ZoneSet=GSw_ZoneSet) ``` - `rev_transmission_basecost.csv`: Base transmission costs (before terrain multipliers) used in reV. diff --git a/inputs/zones/README.md b/inputs/zones/README.md index 54159636..904afa7d 100644 --- a/inputs/zones/README.md +++ b/inputs/zones/README.md @@ -56,7 +56,7 @@ Once you're happy with your zone and hierarchy level definitions, run the follow - Creates the `zonehash.csv` file - Rewrites the `itl_NARIS.csv` file (existing data in the file are preserved, so you should only see new rows added to the bottom of the file) 1. Add the new zone definition to the choices for the `GSw_ZoneSet` switch in `cases.csv` -1. To make sure it worked (or just to read the ITLs in general), you can run `import reeds` and then `reeds.inputs.get_itls(GSw_ZoneSet='your new zoneset name')` in Python with the `reeds2` conda environment activated. +1. To make sure it worked (or just to read the ITLs in general), you can run `import reeds` and then `reeds.parse.get_itls(GSw_ZoneSet='your new zoneset name')` in Python with the `reeds2` conda environment activated. 1. Try a ReEDS run. - The following checks will be performed; if any of them fail, the run will stop. - `b2b.csv`, `county2zone.csv`, `hierarchy.csv`, `zonehash.csv`, `interfaces_r.csv`, and `interfaces_transgrp.csv` should all be preset in the `inputs/zones/{GSw_ZoneSet}` folder diff --git a/postprocessing/.pre-commit-config.yaml b/postprocessing/.pre-commit-config.yaml new file mode 100644 index 00000000..7d082ffd --- /dev/null +++ b/postprocessing/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: +- repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + language_version: python3.9 + files: ^(hourlize/tests/.*\.py?|hourlize/reeds_to_rev.*\.py?)$ +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-json + - id: check-yaml + exclude: ^conda.recipe/ + - id: end-of-file-fixer + exclude_types: [csv] + files: ^(hourlize/tests/.*\.py?|hourlize/reeds_to_rev.*\.py?)$ + - id: trailing-whitespace + - id: check-merge-conflict + - id: check-symlinks + - id: mixed-line-ending + exclude_types: [csv] + files: ^(hourlize/tests/.*\.py?|hourlize/reeds_to_rev.*\.py?)$ + - id: requirements-txt-fixer +- repo: https://github.com/PyCQA/pylint + rev: v2.16.2 + hooks: + - id: pylint + args: [ + "hourlize" + ] \ No newline at end of file diff --git a/postprocessing/air_quality/health_damage_calculations.py b/postprocessing/air_quality/health_damage_calculations.py index 7f4c9a84..9e96e811 100644 --- a/postprocessing/air_quality/health_damage_calculations.py +++ b/postprocessing/air_quality/health_damage_calculations.py @@ -1,6 +1,6 @@ ''' This script can be run in one of two ways: -1. called automatically from runbatch.py as part of a ReEDS run, +1. called automatically from run.py as part of a ReEDS run, in which case a single case folder is passed to the script 2. call directly to post-process a set of completed runs, with a csv file diff --git a/postprocessing/bokehpivot/in/reeds2/process_style.csv b/postprocessing/bokehpivot/in/reeds2/process_style.csv index 1d12ac8f..77320f5c 100644 --- a/postprocessing/bokehpivot/in/reeds2/process_style.csv +++ b/postprocessing/bokehpivot/in/reeds2/process_style.csv @@ -1,21 +1,21 @@ order,color -input_processing/copy_files.py,#393B79 -input_processing/aggregate_regions.py,#5254A3 -input_processing/calc_financial_inputs.py,#6B6ECF -input_processing/fuelcostprep.py,#9C9EDE -input_processing/writecapdat.py,#637939 -input_processing/writesupplycurves.py,#8CA252 -input_processing/plantcostprep.py,#B5CF6B -input_processing/climateprep.py,#CEDB9C -input_processing/hourly_load.py,#8C6D31 -input_processing/recf.py,#BD9E39 -input_processing/forecast.py,#E7BA52 -input_processing/writehintage.py,#E7CB94 -input_processing/transmission.py,#7B4173 -input_processing/outage_rates.py,#A55194 -input_processing/hourly_repperiods.py,#CE6DBD -input_processing/check_inputs.py,#DE9ED6 -createmodel.gms,#31A354 +inputs/copy_files.py,#393B79 +inputs/aggregate_regions.py,#5254A3 +inputs/calc_financial_inputs.py,#6B6ECF +inputs/fuelcostprep.py,#9C9EDE +inputs/writecapdat.py,#637939 +inputs/writesupplycurves.py,#8CA252 +inputs/plantcostprep.py,#B5CF6B +inputs/climateprep.py,#CEDB9C +inputs/hourly_load.py,#8C6D31 +inputs/recf.py,#BD9E39 +inputs/forecast.py,#E7BA52 +inputs/writehintage.py,#E7CB94 +inputs/transmission.py,#7B4173 +inputs/outage_rates.py,#A55194 +inputs/hourly_repperiods.py,#CE6DBD +inputs/check_inputs.py,#DE9ED6 +a_createmodel.gms,#31A354 d_solveoneyear.gms,#843C39 solver/barrier,#AD494A solver/crossover,#D6616B @@ -25,6 +25,6 @@ reeds_augur/stress_periods.py,#6BAED6 reeds_augur/capacity_credit.py,#9ECAE1 reeds2pras,#E6550D pras,#FD8D3C -e_report.gms,#74C476 -e_report_dump.py,#A1D99B +report.gms,#74C476 +report_dump.py,#A1D99B retail_rate_calculations.py,#C7E9C0 diff --git a/postprocessing/cleanup_files.py b/postprocessing/cleanup_files.py index 0e94b9e6..1eb425cc 100644 --- a/postprocessing/cleanup_files.py +++ b/postprocessing/cleanup_files.py @@ -29,7 +29,7 @@ ## All the other outputs/*.csv files are duplicates of data in outputs.h5. os.path.join('outputs', r'^((?!(neue|health|h2_price_month)).)*csv$'), ], - ## Large input files. Would need to rerun input_processing to recreate. + ## Large input files. Would need to rerun input processing to recreate. 2: [ os.path.join('inputs_case', 'inputs.gdx'), os.path.join('inputs_case', 'unitdata.csv'), @@ -52,7 +52,7 @@ 'g00files', 'ReEDS_Augur', ## The following regex matches the rep_{casename}.gdx file written by - ## e_report.gms (which contains the same data as outputs.h5) + ## report.gms (which contains the same data as outputs.h5) os.path.join('outputs', '^rep_.*\.gdx$'), ] } diff --git a/postprocessing/input_plots.py b/postprocessing/input_plots.py index 5507c141..9b3c51a4 100644 --- a/postprocessing/input_plots.py +++ b/postprocessing/input_plots.py @@ -117,7 +117,7 @@ def plot_profile( ## Parse inputs sw = reeds.io.get_switches(case) t = reeds.io.get_years(case)[-1] if year in [0, None, 'last'] else year - rs = reeds.inputs.parse_regions((region if region else case), case) + rs = reeds.parse.parse_regions((region if region else case), case) if weatheryears is None: weatheryears = sw.resource_adequacy_years_list elif isinstance(weatheryears, int): diff --git a/postprocessing/raw_value_streams.py b/postprocessing/raw_value_streams.py new file mode 100644 index 00000000..d2f95961 --- /dev/null +++ b/postprocessing/raw_value_streams.py @@ -0,0 +1,253 @@ +''' +This file can be used to combine a coefficient matrix file (non-pre-solved) and associated marginals +from GAMS gdx solution file to produce value streams for the variables of the model. +''' + +import pandas as pd +import gdxpds +import subprocess +from datetime import datetime +import logging +logger = logging.getLogger('') + +def get_value_streams(solution_file, problem_file, var_list=None, con_list=None, prob_file_type='jacobian'): + ''' + Create dataframe of value streams for each variable of interest based on variable coefficients + in a coefficient matrix file and constraint and variable marginals in the associated GAMS gdx solution file. + Note that all strings are lowercased because GAMS is case insensitive. + Args: + solution_file (string): Full path to GAMS gdx solution file. + problem_file (string): Full path to the file of the model associated with the solution file. + var_list (list of strings): List of lowercased variable names that are of interest. If no list is given, + value streams will be created for all variables. If a list is given, variables not on the list will be + filtered out. + con_list (list of strings): List of lowercased constraint names that are of interest. If no list is given, + value streams will be created for all constraints. If a list is given, constraints not on the list will be + filtered out. + prob_file_type (string): Either 'jacobian' or 'mps'. + Returns: + df (pandas dataframe): Value streams of variables with these columns: + var_name (string): Name of variable. + var_set (string): Period-seperated sets of the variable. + con_name (string): Constraint name or '_obj' for objective coefficients. + con_set (string): Period-seperated sets of the constraint. + coeff (float): Coefficient of the variable in the constraint or objective. + var_level (float): Level of the variable in the solution. + var_marginal (float): Marginal of the variable in the solution. + con_level (float): Level of the constraint in the solution. + con_marginal (float): Marginal of the constraint in the solution or -1 for the objective (costs are negative). + value_per_unit (float): Value per unit of the variable from that constraint (equal to coeff * var_marginal). + value (float): Value that is produced by the variable from the constraint (equal to var_level * value_per_unit). + ''' + if prob_file_type == 'jacobian': + df_prob = get_df_jacobian(problem_file, var_list, con_list) + elif prob_file_type == 'mps': + df_prob = get_df_mps(problem_file, var_list, con_list) + var_list_prob = df_prob['var_name'].unique() + con_list_prob = df_prob['con_name'].unique() + dfs_solution = get_df_solution(solution_file, var_list_prob, con_list_prob) + df = pd.merge(left=df_prob, right=dfs_solution['vars'], how='left', on=['var_name', 'var_set'], sort=False) + df = pd.merge(left=df, right=dfs_solution['cons'], how='left', on=['con_name', 'con_set'], sort=False) + #The objective essentially has a con_marginal of -1 + df.loc[df['con_name']=='_obj','con_marginal'] = -1 + #When variable has a marginal, this marginal must be added as a new row + df_var_marg = df[df['var_marginal'] != 0].copy() + df_var_marg.drop_duplicates(inplace=True, subset=['var_name','var_set']) + df_var_marg['con_name'] = 'var' + df_var_marg['coeff'] = 1 + df_var_marg['con_marginal'] = df_var_marg['var_marginal'] + #concatenate back into original dataframe + df = pd.concat([df, df_var_marg], ignore_index=True) + #Calculate value streams + df['value_per_unit'] = df['coeff']*df['con_marginal'] + df['value'] = df['value_per_unit']*df['var_level'] + return df + +def get_df_mps(mps_file, var_list=None, con_list=None): + ''' + Create dataframe of coefficients for each variable in each constraint and objective function. Note that all + strings are lowercased because GAMS is case insensitive. + Args: + mps_file (string): Full path to the non-presolved mps file of the model associated with the solution file. + var_list (list of strings): List of lowercased variable names that are of interest. If no list is given, + value streams will be created for all variables. If a list is given, variables not on the list will be + filtered out. + con_list (list of strings): List of lowercased constraint names that are of interest. If no list is given, + value streams will be created for all constraints. If a list is given, constraints not on the list will be + filtered out. + Returns: + df (pandas dataframe): Value streams of variables with these columns: + var_name (string): Name of variable. + var_set (string): Period-seperated sets of the variable. + con_name (string): Constraint name or '_obj' for objective coefficients. + con_set (string): Period-seperated sets of the constraint. + coeff (float): Coefficient of the variable in the constraint or objective. + ''' + + #First, gather all rows of mps between 'COLUMNS' and 'RHS' into a dataframe, + #separating the set elements from the variable and constraint names, and separating + #the coefficients into their own column + start = datetime.now() + mps_ls = [] + columns = False + with open(mps_file) as mpsfile: + for line in mpsfile: + if columns: + if line[:3] == 'RHS': + break + if line[:8] == ' MARK': + continue + #split on whitespace + ls = line.split() + if len(ls) > 3: + #This means there was whitespace in one of the set elements. We need to recombine list elements. + i = 0 + while i < len(ls): + if '(' in ls[i] and ')' not in ls[i]: + j = i + while ')' not in ls[j]: + j = j + 1 + ls[i:j+1] = [' '.join(ls[i:j+1])] + i = i + 1 + var_ls = ls[0].split('(') + con_ls = ls[1].split('(') + if (var_list == None or var_ls[0].lower() in var_list) and (con_list == None or con_ls[0].lower() in con_list + ['_obj']): + if len(var_ls) == 1: + var_ls.append('') + else: + var_ls[1] = var_ls[1][:-1].replace('"','').replace("'",'') + if len(con_ls) == 1: + con_ls.append('') + else: + con_ls[1] = con_ls[1][:-1].replace('"','').replace("'",'') + mps_ls.append(var_ls + con_ls + [float(ls[2])]) + elif line[:7] == 'COLUMNS': + columns = True + df_mps = pd.DataFrame(mps_ls) + df_mps.columns = ['var_name','var_set','con_name','con_set', 'coeff'] + for col in ['var_name','var_set','con_name','con_set']: + df_mps[col] = df_mps[col].str.lower() + logger.info('mps read: ' + str(datetime.now() - start)) + return df_mps + +def get_df_jacobian(jacobian_file, var_list=None, con_list=None): + ''' + Create dataframe of coefficients for each variable in each constraint and objective function. Note that all + strings are lowercased because GAMS is case insensitive. + Args: + jacobian_file (string): Full path to the non-presolved jacobian gdx file of the model associated with the solution file. + var_list (list of strings): List of lowercased variable names that are of interest. If no list is given, + value streams will be created for all variables. If a list is given, variables not on the list will be + filtered out. + con_list (list of strings): List of lowercased constraint names that are of interest. If no list is given, + value streams will be created for all constraints. If a list is given, constraints not on the list will be + filtered out. + Returns: + df (pandas dataframe): Value streams of variables with these columns: + var_name (string): Name of variable. + var_set (string): Period-seperated sets of the variable. + con_name (string): Constraint name or '_obj' for objective coefficients. + con_set (string): Period-seperated sets of the constraint. + coeff (float): Coefficient of the variable in the constraint or objective. + ''' + start = datetime.now() + df_A = gdxpds.to_dataframe(jacobian_file, 'A', old_interface=False) + for x in ['j','i']: + #For i (equation) and j (variable) sets, I need to dump to csv to get the Text column, ugh + x_file = jacobian_file.replace('.gdx',f'_{x}.csv') + subprocess.Popen(f'gdxdump "{jacobian_file}" format=csv epsout=0 symb={x} output="{x_file}" CSVSetText').wait() + df_x = pd.read_csv(x_file) + df_x[['name','set']] = df_x['Text'].str.rstrip(')').str.replace(',','.').str.lower().str.split('(', expand=True, n=1) + if x == 'j': + #'j' means variable + name_col = 'var_name' + set_col = 'var_set' + lst = var_list + else: + #'i' means equation + name_col = 'con_name' + set_col = 'con_set' + lst = con_list + df_x = df_x.rename(columns={'Dim1':x, 'name':name_col, 'set':set_col}) + df_x = df_x.drop(columns=['Text']) + if lst != None: + df_x = df_x[df_x[name_col].isin(lst)].copy() + #inner merge with df_A (note that map may be much faster) + df_A = df_A.merge(df_x, on=x, how='inner') + df_A = df_A.rename(columns={'Value':'coeff'}) + df_A = df_A[['var_name','var_set','con_name','con_set','coeff']].copy() + logger.info('jacobian read: ' + str(datetime.now() - start)) + return df_A + +def get_df_solution(solution_file, var_list_prob, con_list_prob): + ''' + Create dataframes of marginals and levels of variables and constraints of interest. + Note that all strings are lowercased because GAMS is case insensitive. + Args: + solution_file (string): Full path to GAMS gdx solution file. + var_list_prob (list of strings): List of lowercased variable names that are of interest. + con_list_prob (list of strings): List of lowercased constraint names that are of interest. + Returns: + dict of two pandas dataframes, one for variables ('vars'), and one for constraints ('cons'). + Columns in the 'vars' dataframe: + var_name (string): Name of variable. + var_set (string): Period-seperated sets of the variable. + var_level (float): Level of the variable in the solution. + var_marginal (float): Marginal of the variable in the solution. + Columns in the 'cons' dataframe + con_name (string): Constraint name. + con_set (string): Period-seperated sets of the constraint. + con_level (float): Level of the constraint in the solution. + con_marginal (float): Marginal of the constraint in the solution. + ''' + start = datetime.now() + dfs = gdxpds.to_dataframes(solution_file) + logger.info('solution read: ' + str(datetime.now() - start)) + start = datetime.now() + dfs = {k.lower(): v for k, v in list(dfs.items())} + df_vars = get_df_symbols(dfs, var_list_prob) + df_vars = df_vars.rename(columns={"Level": "var_level", "Marginal": "var_marginal", 'sym_name':'var_name', 'sym_set': 'var_set'}) + df_cons = get_df_symbols(dfs, con_list_prob) + df_cons = df_cons.rename(columns={"Level": "con_level", "Marginal": "con_marginal", 'sym_name':'con_name', 'sym_set': 'con_set'}) + logger.info('solution reformatted: ' + str(datetime.now() - start)) + return {'vars':df_vars, 'cons':df_cons} + +def get_df_symbols(dfs, symbols): + ''' + Create dataframes of marginals and levels of symbols of interest. + Note that all strings are lowercased because GAMS is case insensitive. + Args: + dfs (dict of pandas dataframes): A result of gdxpds.to_dataframes(), with keys lowercased. + Dataframes of gdxpds.to_dataframes() always have separate columns for each set, + followed by Level, Marginal, Lower, Upper, and Scale columns. + symbols (list of strings): List of lowercased symbol names that are of interest. + Returns: + df_syms (pandas dataframe): dataframe of symbol levels and marginals with these columns: + sym_name (string): Name of symbol, lowercased + sym_set (string): Period-seperated sets of the symbol, lowercased + Level (float): Level of the symbol in the solution. + Marginal (float): Marginal of the symbol in the solution. + ''' + df_syms = [] + for sym_name in symbols: + if sym_name not in dfs: + continue + df_sym = dfs[sym_name] + df_sym['sym_name'] = sym_name + #concatenate all the set columns into one column + level_col = df_sym.columns.get_loc('Level') + df_sym['sym_set'] = '' + for s in range(level_col): + set_col = df_sym.iloc[:,s] + if set_col.str.contains(r'[.\'"()]| ').any(): + logger.info('Warning: Invalid character (dot, quote, parens, double space) found in column #' + str(s) + ' of ' + sym_name) + df_sym['sym_set'] = df_sym['sym_set'] + set_col + if s < level_col - 1: + df_sym['sym_set'] = df_sym['sym_set'] + '.' + #reduce to only the columns of interest + df_sym = df_sym[['sym_name','sym_set','Level','Marginal']] + df_syms.append(df_sym) + df_syms = pd.concat(df_syms).reset_index(drop=True) + for col in ['sym_name','sym_set']: + df_syms[col] = df_syms[col].str.lower() + return df_syms \ No newline at end of file diff --git a/postprocessing/retail_rate_module/ITC-PTC_expenditures.py b/postprocessing/retail_rate_module/ITC-PTC_expenditures.py index 3c3f346d..dfcd6d2e 100644 --- a/postprocessing/retail_rate_module/ITC-PTC_expenditures.py +++ b/postprocessing/retail_rate_module/ITC-PTC_expenditures.py @@ -6,9 +6,9 @@ desired ReEDS-2.0 and {batch}_{case} run directories * Outputs are saved to ReEDS-2.0/runs/{batch}_{case}/outputs/ * To use for outputs generated before PR #527 (2cec69214baddf817b276a89e3f4072ab76cf2c9), - add the following lines to the start of e_report.gms and re-run (otherwise geothermal + add the following lines to the start of report.gms and re-run (otherwise geothermal won't be included) - e_report.gms and e_report_dump.py: + report.gms and report_dump.py: cost_cap_fin_mult_noITC(i,r,t)$geo(i) = cost_cap_fin_mult_noITC("geothermal",r,t) cost_cap_fin_mult_no_credits(i,r,t)$geo(i) = cost_cap_fin_mult_no_credits("geothermal",r,t) * Caveats diff --git a/postprocessing/retail_rate_module/retail_rate_calculations.py b/postprocessing/retail_rate_module/retail_rate_calculations.py index c8365b99..df48bb7b 100644 --- a/postprocessing/retail_rate_module/retail_rate_calculations.py +++ b/postprocessing/retail_rate_module/retail_rate_calculations.py @@ -462,7 +462,7 @@ def main(run_dir, inputpath='inputs.csv', write=True, verbose=0): 'r':'receiving_region', 't':'t', 'Value':'expenditure_flow', 'Dim1':'receiving_region','Dim2':'t','Val':'expenditure_flow'}) ) - ### According to e_report.gms, all international flows are load to/from Canada + ### According to report.gms, all international flows are load to/from Canada # (not capacity, reserves, rps, or Mexico) state_international_flows['price_type'] = 'load' state_international_flows['sending_state'] = 'Canadian Imports' diff --git a/postprocessing/run_pcm.py b/postprocessing/run_pcm.py new file mode 100644 index 00000000..1ad0f93f --- /dev/null +++ b/postprocessing/run_pcm.py @@ -0,0 +1,391 @@ +# %% Imports +import os +import subprocess +import argparse +import json +from glob import glob +import gdxpds +import pandas as pd +from pathlib import Path + +## Local imports +import reeds +from reeds.input_processing import hourly_repperiods +from reeds.input_processing import hourly_writetimeseries + + +# %% Inferred inputs +reeds_path = os.path.dirname(__file__) + +# %% Default inputs +switch_mods_default = { + 'GSw_HourlyClusterAlgorithm': 'hierarchical', + 'GSw_HourlyNumClusters': 365, + 'GSw_HourlyType': 'day', + 'GSw_HourlyChunkLengthRep': 1, + 'GSw_HourlyChunkLengthStress': 1, + 'GSw_HourlyChunkAggMethod': 1, + 'GSw_PRM_CapCredit': 0, +} + + +# %% Functions +def check_slurm(forcelocal=False): + """Check whether to submit slurm jobs (if on HPC) or run locally""" + hpc = ( + False + if forcelocal + else ( + True + if int(os.environ.get('REEDS_USE_SLURM', 0)) and ('NREL_CLUSTER' in os.environ) + else False + ) + ) + return hpc + + +def solvestring_pcm( + batch_case, + sw, + t, + restartfile, + iteration=0, + hpc=0, + stress_year=0, + label='', +): + """ + Typical inputs: + * restartfile: batch_case if first solve year else {batch_case}_{prev_year} + * sw: loaded from {batch_case}/inputs_case/switches.csv + """ + savefile = f"pcm_{label}_{batch_case}_{t}i{iteration}" + _stress_year = f"{t}i{iteration}" if stress_year in ['keep', 'default'] else stress_year + out = ( + "gams d_solvepcm.gms" + + (" license=gamslice.txt" if hpc else '') + + f" o={os.path.join('lstfiles', f'{savefile}.lst')}" + + f" r={os.path.join('g00files', restartfile)}" + + " gdxcompress=1" + + f" xs={os.path.join('g00files', savefile)}" + + ' logOption=4 appendLog=1' + + f" logFile=gamslog_pcm_{label}_{t}.txt" + + f" --case={batch_case}" + + f" --cur_year={t}" + + f" --stress_year={stress_year}" + + f" --temporal_inputs=pcm_{label}" + + ''.join( + [ + f" --{s}={sw[s]}" + for s in [ + 'GSw_SkipAugurYear', + 'GSw_HourlyType', + 'GSw_HourlyWrapLevel', + 'GSw_ClimateWater', + 'GSw_Canada', + 'GSw_ClimateHydro', + 'GSw_HourlyChunkLengthRep', + 'GSw_HourlyChunkLengthStress', + 'GSw_StateCO2ImportLevel', + 'GSw_PVB_Dur', + 'GSw_ValStr', + 'GSw_gopt', + 'solver', + 'debug', + 'startyear', + ] + ] + ) + + '\n' + ) + + return out + + +def pcm_report_string(batch_case, sw, t, iteration=0, hpc=0, label=''): + savefile = f"pcm_{label}_{batch_case}_{t}i{iteration}" + out = ( + f"gams {Path('reeds','core','terminus','report.gms')}" + + (' license=gamslice.txt' if hpc else '') + + f" o={os.path.join('lstfiles', f'report_pcm_{label}_{t}_{batch_case}.lst')}" + + f" r={os.path.join('g00files', savefile)}" + + ' gdxcompress=1' + + ' logOption=4 appendLog=1' + + f" logFile=gamslog_pcm_{label}_{t}.txt" + + f" --fname=pcm_{label}_{t}_{batch_case}" + + " --GSw_calc_powfrac=0 \n" + ) + return out + + +def submit_job(casepath, command_string, jobname='', joblabel='', bigmem=0): + """ + Create a slurm job submission script for `command_string` at `casepath`, + then submit it. + Uses the slurm settings from {reeds_path}/srun_template.sh. + """ + ### Get the SLURM boilerplate + commands_header, commands_sbatch, commands_other = [], [], [] + with open(os.path.join(reeds_path, 'srun_template.sh'), 'r') as f: + for line in f: + if bigmem and ('--mem=' in line): + line = '#SBATCH --mem=500000' + + if line.strip().startswith('#!'): + commands_header.append(line.strip()) + elif line.strip().startswith('#SBATCH'): + commands_sbatch.append(line.strip()) + else: + commands_other.append(line.strip()) + + ### Add the command for this run + slurm = ( + commands_header + + commands_sbatch + + ([f"#SBATCH --job-name={jobname}"] if len(jobname) else []) + + [f"#SBATCH --output={os.path.join(casepath, 'slurm-%j.out')}"] + + commands_other + + [''] + + [command_string] + ) + ### Write the SLURM command + callfile = os.path.join(casepath, f'submit_{joblabel}.sh') + with open(callfile, 'w+') as f: + for line in slurm: + f.writelines(line + '\n') + ### Submit the job + batchcom = f'sbatch {callfile}' + subprocess.Popen(batchcom.split()) + + +# %% +def main(casepath, t, switch_mods=switch_mods_default, label='', overwrite=False): + """ + Args: + kwargs: Passed to hourly_reppreiods.main() + """ + # %% Switch to run folder + os.chdir(casepath) + + # %% Get the run settings + sw = reeds.io.get_switches(casepath) + years = ( + pd.read_csv(os.path.join(casepath, 'inputs_case', 'modeledyears.csv')) + .columns.astype(int) + .values + ) + _t = t if t > 0 else max(years) + + # %% Set up logger + reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(casepath, f'gamslog_pcm_{label}_{_t}.txt'), + ) + + # %% Get and modify the switch settings + sw_pcm = sw.copy() + for key, val in switch_mods.items(): + sw_pcm[key] = val + + # %% Write the inputs for PCM + if (not os.path.isdir(os.path.join(casepath, 'inputs_case', f'pcm_{label}'))) or overwrite: + hourly_repperiods.main( + sw=sw_pcm, + reeds_path=reeds_path, + inputs_case=os.path.join(casepath, 'inputs_case'), + periodtype=f'pcm_{label}', + minimal=1, + ) + hourly_writetimeseries.main( + sw=sw_pcm, + reeds_path=reeds_path, + inputs_case=os.path.join(casepath, 'inputs_case'), + periodtype=f'pcm_{label}', + make_plots=0, + ) + ## Write a set of empty "stress0" inputs to turn off stress periods for PCM + stresspath = os.path.join(casepath, 'inputs_case', 'stress0') + if (not os.path.isdir(stresspath)) or overwrite: + os.makedirs(stresspath, exist_ok=True) + pd.DataFrame(columns=['rep_period', 'year', 'yperiod', 'actual_period']).to_csv( + os.path.join(stresspath, 'period_szn.csv'), + index=False, + ) + hourly_writetimeseries.main( + sw=sw_pcm, + reeds_path=reeds_path, + inputs_case=os.path.join(casepath, 'inputs_case'), + periodtype='stress0', + make_plots=0, + ) + + # %% Get ReEDS LP for specified year + batch_case = os.path.basename(casepath) + ### Get the restartfile and get the last year/iteration if t=0 and iteration='last' + if _t == min(years): + restartfile = batch_case + _iteration = 0 + elif iteration == 'last': + restartfile = sorted(glob(os.path.join(casepath, 'g00files', f"{batch_case}_{_t}i*")))[-1] + _iteration = int(restartfile[: -len('.g00')].split('i')[-1]) + else: + _iteration = iteration + restartfile = os.path.join(casepath, 'g00files', f"{batch_case}_{_t}i{_iteration}.g00") + + cmd_gams = solvestring_pcm( + batch_case=batch_case, + sw=sw_pcm, + t=_t, + restartfile=restartfile, + iteration=_iteration, + hpc=int(sw['hpc']), + label=label, + ) + print(cmd_gams) + + ### Run GAMS LP + result = subprocess.run(cmd_gams, shell=True) + if result.returncode: + raise Exception(f'd_solvepcm.gms failed with return code {result.returncode}') + + # %% Dump results to gdx + cmd_report = pcm_report_string( + batch_case=batch_case, + sw=sw_pcm, + t=_t, + iteration=_iteration, + hpc=int(sw['hpc']), + label=label, + ) + print(cmd_report) + + result = subprocess.run(cmd_report, shell=True) + if result.returncode: + raise Exception(f'report.gms failed with return code {result.returncode}') + + # %% Dump gdx to h5 + ## Get new file names if applicable + dfparams = pd.read_csv( + os.path.join(casepath, "e_report_params.csv"), + comment="#", + index_col="param", + ) + rename = dfparams.loc[~dfparams.output_rename.isnull(), "output_rename"].to_dict() + rename = {k.split("(")[0]: v for k, v in rename.items()} + print(f"renamed parameters: {rename}") + + print("Loading outputs gdx") + dict_out = gdxpds.to_dataframes( + os.path.join(casepath, 'outputs', f"rep_pcm_{label}_{_t}_{batch_case}.gdx") + ) + print("Finished loading outputs gdx") + + outputs_path = os.path.join(casepath, 'outputs', f'pcm_{label}_{_t}') + os.makedirs(outputs_path, exist_ok=True) + reeds.core.terminus.report_dump.write_dfdict( + dfdict=dict_out, + outputs_path=outputs_path, + rename=rename, + ) + + +# %% Procedure +if __name__ == '__main__': + # %% Argument inputs + parser = argparse.ArgumentParser( + description='Run ReEDS in PCM mode', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('casepath', type=str, help='ReEDS-2.0/runs/{case} directory') + parser.add_argument( + '--year', + '-t', + type=int, + default=0, + help='Year to run (must have the corresponding .g00 file), or 0 for last year', + ) + parser.add_argument( + '--iteration', + '-i', + type=str, + default='last', + help="Iteration to run, or 'last' for last iteration", + ) + parser.add_argument( + '--switch_mods', + '-s', + type=json.loads, + default=json.dumps(switch_mods_default), + help=( + 'Dictionary-formated string of switch arguments for PCM. ' + 'Use single quotes outside the dictionary and double quotes for keys, as in:\n' + '`-s \'{"GSw_HourlyChunkLengthRep":4}\'`' + ), + ) + parser.add_argument('--label', '-l', type=str, default='', help='Label for PCM outputs') + parser.add_argument( + '--overwrite', + '-o', + action='store_true', + help="Overwrite input files if they already exist (otherwise don't rewrite them)", + ) + parser.add_argument( + '--forcelocal', + '-f', + action='store_true', + help='Run locally (including on a compute node as part of a job)', + ) + parser.add_argument('--bigmem', '-b', action='store_true', help='Use bigmem node') + + args = parser.parse_args() + casepath = args.casepath + t = args.year + iteration = args.iteration + switch_mods = args.switch_mods + label = args.label + if not len(label): + label = f"{switch_mods['GSw_HourlyType'][0]}{switch_mods['GSw_HourlyChunkLengthRep']}h" + overwrite = args.overwrite + forcelocal = args.forcelocal + bigmem = args.bigmem + + # #%% Inputs for debugging + # casepath = os.path.join(reeds_path, 'runs', 'v20250206_pcmM0_Pacific') + # t = 0 + # iteration = 'last' + # switch_mods = switch_mods_default + # switch_mods = { + # 'GSw_HourlyClusterAlgorithm': 'hierarchical', + # 'GSw_HourlyType': 'day', 'GSw_HourlyNumClusters': 365, + # # 'GSw_HourlyType': 'wek', 'GSw_HourlyNumClusters': 73, + # 'GSw_HourlyChunkLengthRep': 4, + # } + # label = f"{switch_mods['GSw_HourlyType'][0]}{switch_mods['GSw_HourlyChunkLengthRep']}h" + # forcelocal = False + # overwrite = False + # bigmem = True + + # %% Determine whether to submit slurm job + hpc = check_slurm(forcelocal=forcelocal) + + ### Run it + if not hpc: + main(casepath=casepath, t=t, switch_mods=switch_mods, label=label, overwrite=overwrite) + else: + command_string = ( + f"python run_pcm.py {casepath} " + f"--year={t} " + f"--iteration={iteration} " + f"--switch_mods='{json.dumps(switch_mods)}' " + f"--label={label} " + "--forcelocal " + ) + ("--overwrite " if overwrite else "") + joblabel = f"pcm_{label}_{t}" + jobname = f"{os.path.basename(casepath)}-{joblabel}" + submit_job( + casepath=casepath, + command_string=command_string, + jobname=jobname, + joblabel=joblabel, + bigmem=bigmem, + ) diff --git a/postprocessing/run_reeds2pras.py b/postprocessing/run_reeds2pras.py index 129a6fe5..9d3006b8 100644 --- a/postprocessing/run_reeds2pras.py +++ b/postprocessing/run_reeds2pras.py @@ -104,9 +104,7 @@ def main( site.addsitedir(reeds_path) else: site.addsitedir(case) - import Augur import reeds - import ReEDS_Augur.prep_data as prep_data ### Get the switches, overwriting values as necessary sw = reeds.io.get_switches(case) @@ -147,10 +145,10 @@ def main( any([not os.path.isfile(os.path.join(augur_data,f)) for f in files_expected]) or overwrite ): - augur_csv, augur_h5 = prep_data.main(t, case) + augur_csv, augur_h5 = reeds.resource_adequacy.prep_data.main(t, case) ### Run ReEDS2PRAS - Augur.run_pras( + reeds.resource_adequacy.Augur.run_pras( case, t, iteration=iteration, diff --git a/postprocessing/valuestreams.py b/postprocessing/valuestreams.py new file mode 100644 index 00000000..ef246976 --- /dev/null +++ b/postprocessing/valuestreams.py @@ -0,0 +1,85 @@ +import sys +import os +import pandas as pd +import raw_value_streams as rvs +from datetime import datetime +import logging + +sys.stdout = open('gamslog.txt', 'a') +sys.stderr = open('gamslog.txt', 'a') + +this_dir_path = os.path.dirname(os.path.realpath(__file__)) +vs_path = this_dir_path + '/inputs_case' +output_dir = this_dir_path + '/outputs' +solution_file = this_dir_path + '/ReEDSmodel_p.gdx' +problem_file = this_dir_path + '/ReEDSmodel_jacobian.gdx' +# problem_file = this_dir_path + '/ReEDSmodel.mps' + +logger = logging.getLogger('') +logger.setLevel(logging.DEBUG) +sh = logging.StreamHandler(sys.stdout) +sh.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(message)s') +sh.setFormatter(formatter) +logger.addHandler(sh) + +df_var_map = pd.read_csv(vs_path+'/var_map.csv', dtype=object) +var_list = df_var_map['var_name'].values.tolist() + +#common function for outputting to csv +def add_concat_csv(df_in, csv_file): + df_in['t'] = pd.to_numeric(df_in['t']) + if not os.path.exists(csv_file): + df_in.to_csv(csv_file,index=False) + else: + df_csv = pd.read_csv(csv_file) + df_out = pd.concat([df_csv, df_in], ignore_index=True, sort=False) + df_out.to_csv(csv_file,index=False) + +def createValueStreams(): + very_start = datetime.now() + logger.info('Starting valuestreams.py') + df = rvs.get_value_streams(solution_file, problem_file, var_list) + logger.info('Raw value streams completed: ' + str(datetime.now() - very_start)) + + df = pd.merge(left=df, right=df_var_map, on='var_name', how='inner') + + #Chosen plants (with nonzero levels in solution) + start = datetime.now() + df_lev = df[df['var_level'] != 0].copy() + #convert to list of lists for speed + df_lev_ls = df_lev.values.tolist() + cols = df_lev.columns.values.tolist() + ci = {c:i for i,c in enumerate(cols)} + #Use iterrows or itertuples or somthing faster? iterrows is most convenient so if this isn't a bottleneck, use it. + replace_cols = ['i','v','r','t'] + for i, r in enumerate(df_lev_ls): + var_set_ls = r[ci['var_set']].split('.') + for c in replace_cols: + if str(r[ci[c]]).isdigit(): + df_lev_ls[i][ci[c]] = var_set_ls[int(r[ci[c]])] + #convert back to pandas dataframe + df_lev = pd.DataFrame(df_lev_ls) + df_lev.columns = cols + + #Fill missing values with 'none' + out_sets = ['i','v','r','t','var_name','con_name'] + df_lev[out_sets] = df_lev[out_sets].fillna(value='none') + + #Reduce df_lev to columns of interest and groupby sum + out_cols = out_sets + ['value'] + df_lev = df_lev[out_cols] + df_lev = df_lev.groupby(out_sets, sort=False, as_index =False).sum() + + add_concat_csv(df_lev.copy(), output_dir + '/valuestreams_chosen.csv') + logger.info('Levels output: ' + str(datetime.now() - start)) + logger.info('Done with years: ' + str(df_lev['t'].unique().tolist())) + logger.info('Finished valuestreams.py. Total time: ' + str(datetime.now() - very_start)) + +if __name__ == '__main__': + createValueStreams() + x_files = [problem_file.replace('.gdx',f'_{x}.csv') for x in ['i','j']] + files = x_files + [solution_file, problem_file] + for f in files: + if os.path.exists(f): + os.remove(f) \ No newline at end of file diff --git a/preprocessing/casemaker.py b/preprocessing/casemaker.py index c70b8d72..4f3a8db7 100644 --- a/preprocessing/casemaker.py +++ b/preprocessing/casemaker.py @@ -191,9 +191,9 @@ def main(casematrix_path=None, batchname=None): ### Write it dfcases.to_csv(os.path.join(reeds_path,f'cases_{_batchname}.csv')) - ### Make sure the switch names and values pass the checks in runbatch.py + ### Make sure the switch names and values pass the checks in run.py sep = ';' if os.name == 'posix' else '&&' - cmd = f"cd {reeds_path} {sep} python runbatch.py -b test -c {_batchname} -r 4 -p 1 --dryrun" + cmd = f"cd {reeds_path} {sep} python run.py -b test -c {_batchname} -r 4 -p 1 --dryrun" result = subprocess.run(cmd, shell=True, capture_output=True) err = result.stderr.decode() if len(err): diff --git a/reeds/__init__.py b/reeds/__init__.py index e6aaa6e1..5f165aba 100644 --- a/reeds/__init__.py +++ b/reeds/__init__.py @@ -1,12 +1,11 @@ from . import checks as checks from . import financials as financials -from . import inputs as inputs from . import io as io from . import log as log from . import output_calc as output_calc +from . import parse as parse from . import plots as plots from . import prasplots as prasplots -from . import ra as ra from . import reedsplots as reedsplots from . import remote as remote from . import report_utils as report_utils @@ -14,3 +13,7 @@ from . import techs as techs from . import timeseries as timeseries from . import units as units +from . import core +from . import hpc +from . import input_processing +from . import resource_adequacy diff --git a/reeds/core/setup/a_createmodel.gms b/reeds/core/setup/a_createmodel.gms new file mode 100644 index 00000000..9250f30e --- /dev/null +++ b/reeds/core/setup/a_createmodel.gms @@ -0,0 +1,10 @@ +$setglobal ds \ +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +$include reeds%ds%core%ds%setup%ds%b_inputs.gms +$include reeds%ds%core%ds%setup%ds%c_model.gms +$include reeds%ds%core%ds%setup%ds%d_objective.gms +$include reeds%ds%core%ds%setup%ds%d_mga.gms +$include reeds%ds%core%ds%setup%ds%e_solveprep.gms diff --git a/reeds/core/setup/b_inputs.gms b/reeds/core/setup/b_inputs.gms new file mode 100644 index 00000000..2aa9316e --- /dev/null +++ b/reeds/core/setup/b_inputs.gms @@ -0,0 +1,6773 @@ +$title 'ReEDS 2.0' + +* Note - all dollar values are in 2004$ unless otherwise indicated +* It is our intention that there are no hard-coded values in b_inputs.gms +* but you will note that we still have some work to do to make that happen... + +*Setting the default directory separator +$setglobal ds \ + +$eolcom \\ + +*Change the default slash if in UNIX +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +*need to convert the 'unit' numhintage to some large value +$ifthen.unithintage %numhintage%=="unit" +$eval numhintage 300 +$endif.unithintage + +* Under the Clean Air Act, all coal plants are regulated individually. Therefore, we need a large value of hintages to represent these plants +$include inputs_case%ds%max_hintage_number.txt + +$ifthen.unithintage %GSw_Clean_Air_Act%==1 +$eval numhintage max_hintage_number +$endif.unithintage + +*need to convert the 'group' numhintage to some large value +$ifthen.unithintage %numhintage%=="group" +$eval numhintage 300 +$endif.unithintage + +* there are numeraire hintages on either sides of the outer breaks +* when using calcmethod = 1, here adding two for safety +* NB this will not increase model size given conditions +* dictating valcap and valgen for initial classes +$eval numhintage %numhintage% + 2 + +*$ontext +* --- print timing profile --- +option profile = 3 +option profiletol = 0 + +* --- suppress .lst file printing --- +* equations listed per block +option limrow = %debug% ; +* variables listed per block +option limcol = %debug% ; +* solver's solution output printed +option solprint = off ; +* solver's system output printed +option sysout = off ; +*$offtext + +set dummy "set used for initialization of numerical sets" / 0*10000 / ; +alias(dummy,adummy) ; + + +*====================== +* -- Local Switches -- +*====================== + +* Following are scalars used to turn on or off various components of the model. +* For binary switches, [0] is off and [1] is on. +* These switches are generated from the cases file in run.py. +$include inputs_case%ds%gswitches.txt + +* Extra switches that are defined based on other switches +scalar Sw_Prod "Scalar value for whether Sw_H2 or Sw_DAC are enabled" ; +Sw_Prod$[Sw_H2 or Sw_DAC or Sw_DAC_Gas] = 1 ; + +set timetype "Type of time method used in the model" +/ seq, win, int / ; + +parameter Sw_Timetype(timetype) "Switch that specifies the type of time method used in the model" ; + +Sw_Timetype("%timetype%") = 1 ; + +* Sw_PCM is always 0 except when running d_solvepcm.gms, where it's set to 1 +scalar Sw_PCM "Internal switch used when running PCM mode" / 0 / ; +* Sw_MGA is always 0 except when running the optimization a second time for MGA, where it's set to 1 +scalar Sw_MGA "Internal switch used when running MGA mode" / 0 / ; + +*============================ +* --- Scalar Declarations --- +*============================ + +*year-related switches that define retirement and upgrade start dates +scalar retireyear "first year to allow capacity to start retiring" /%GSw_Retireyear%/ + upgradeyear "first year to allow capacity to upgrade" /%GSw_Upgradeyear%/ + climateyear "first year to apply climate impacts" /%GSw_ClimateStartYear%/ ; + +*** Scalars: copied from inputs/scalars.csv to inputs_case/scalars.txt in run.py +$include inputs_case%ds%scalars.txt + +*========================== +* --- Set Declarations --- +*========================== + +*## Spatial sets (define first so case stays consistent) +* written by copy_files.py +$onOrder +set r "regions" +/ +$offlisting +$include inputs_case%ds%val_r.csv +$onlisting +/ ; +$offOrder + +$onempty +set offshore(r) "offshore zones" +/ +$offlisting +$include inputs_case%ds%offshore.csv +$onlisting +/ ; +$offempty + +set land(r) "land-based (not offshore) zones" ; +land(r)$[not offshore(r)] = yes ; + +* written by copy_files.py +$onempty +set cs(*) "carbon storage sites" +/ +$offlisting +$include inputs_case%ds%val_cs.csv +$onlisting +/ ; +$offempty + +* created in and mapped to hierarchy in recf.py +set ccreg "capacity credit regions" +/ +$offlisting +$include inputs_case%ds%ccreg.csv +$onlisting +/ ; + +set eall "emission categories used in reporting" +/ +$offlisting +$include inputs_case%ds%eall.csv +$onlisting +/ ; + +set e(eall) "emission categories used in model" +/ +$offlisting +$include inputs_case%ds%e.csv +$onlisting +/ ; + +set etype "emission types used in model (upstream and process)" +/ +$offlisting +$include inputs_case%ds%etype.csv +$onlisting +/ ; + +Sets +nercr "NERC regions" +* https://www.nerc.com/pa/RAPA/ra/Reliability%20Assessments%20DL/NERC_LTRA_2021.pdf +/ +* written by copy_files.py +$include inputs_case%ds%val_nercr.csv +/ + +transreg "Transmission Planning Regions from FERC order 1000" +* (https://www.ferc.gov/sites/default/files/industries/electric/indus-act/trans-plan/trans-plan-map.pdf) +/ +* written by copy_files.py +$include inputs_case%ds%val_transreg.csv +/, + +transgrp "sub-FERC-1000 regions" +/ +* written by copy_files.py +$include inputs_case%ds%val_transgrp.csv +/, + +itlgrp "ReEDS zones for additional ITL constraints when doing a run that includes county resolution" +/ +* written by copy_files.py +$include inputs_case%ds%val_itlgrp.csv +/, + +cendiv "census divisions" +/ +* written by copy_files.py +$include inputs_case%ds%val_cendiv.csv +/, + +interconnect "interconnection regions" +/ +* written by copy_files.py +$include inputs_case%ds%val_interconnect.csv +/, + +country "country regions" +/ +* written by copy_files.py +$include inputs_case%ds%val_country.csv +/, + +st "US, Mexico, and/or Canadian States/Provinces" +/ +* written by copy_files.py +$include inputs_case%ds%val_st.csv +/, + +* biomass supply curves defined by USDA region +usda_region "Biomass supply curve regions" +/ +* written by copy_files.py +$include inputs_case%ds%val_usda_region.csv +/, + +h2ptcreg "Regions which enforce the H2 production incentive regulations, for the US these are the National Transmission Needs Study regions" +* https://www.energy.gov/sites/default/files/2023-12/National%20Transmission%20Needs%20Study%20Supplemental%20Material%20-%20Final_2023.12.1.pdf +/ +* written by copy_files.py +$include inputs_case%ds%val_h2ptcreg.csv +/ + +* Hurdle rate regions +hurdlereg "Hurdle regions" +/ +$include inputs_case%ds%val_hurdlereg.csv +/ +; + +* Written by copy_files.py +$include b_sets.gms + +sets +*The following two sets: +*ban - will remove the technology from being considered, anywhere +*bannew - will remove the ability to invest in that technology + ban(i) "ban from existing, prescribed, and new generation -- usually indicative of missing data or operational constraints" + / + upv_10 +* csp-ns is "CSP, no storage". There is ~1.3 GW existing capacity but we group it with UPV and +* don't allow new builds of csp-ns. + csp-ns + other + unknown + geothermal + hydro + csp3_1*csp3_12 + csp4_1*csp4_12 + pumped-hydro-flex + hydED_pumped-hydro-flex + CoalOldUns_CoalOldScr + CoalOldUns_CofireOld + CoalOldScr_CofireOld +$ifthene.hydup not %GSw_HydroCapEnerUpgradeType% == 1 + hydUD + hydUND +$endif.hydup +$ifthene.hydup2 %GSw_HydroAddPumpDispUpgSwitch% == 0 + hydEND_hydED + hydED_pumped-hydro +$endif.hydup2 + /, + + bannew(i) "banned from creating new capacity, usually due to lacking data or representation" + / + can-imports + hydro + distpv + geothermal + cofireold + CoalOldScr + CoalOldUns + csp-ns +*you cannot build existing hydro... + HydEND + HydED + /, + +*Technologies with certain combinations of power technology, cooling technology, and +*water source are also banned from new capacity below after defining +*linking sets between i, ctt, and wst. + +*Data is insufficient to characterize new pond cooling systems, regulations effectively +*prohibit new once-through cooling + bannew_ctt(ctt) "banned ctt from creating new non-numeraire techs, usually due to lacking data or representation" + / + o + p + /, + + bannew_wst(wst) "banned wst from creating new non-numeraire techs, usually due to lacking data or representation" + / + fsl + ss + / ; + +alias(i,ii,iii) ; + +set i_water_nocooling(i) "technologies that use water, but are not differentiated by cooling tech and water source" +/ +$offlisting +$ondelim +$include inputs_case%ds%i_water_nocooling.csv +$offdelim +$onlisting +/ ; + +set i_water_cooling(i) "derived technologies from original technologies with cooling technologies other than just none", +*Hereafter numeraire techs in cooling-water context mean original technologies, +*like gas-CC, and non-numeraire techs mean techs that are derived from numeraire techs +*with cooling technology type and water source data appended to them, like gas-CC_r_fsa +*-- it is gas-CC with recirculating cooling and fresh surface appropriated water source. + i_water(i) "set of all technologies that use water for any purpose", + i_ii_ctt_wst(i,ii,ctt,wst) "linking set between non-numeraire techs, numeraire techs, cooling technology types, and water source types", +*linking sets extracted from i_ii_ctt_wst(i,ii,ctt,wst) that allow one-one mapping among dimensions + i_ctt(i,ctt) "linking set between non-numeraire techs and cooling technology types", + i_wst(i,wst) "linking set between non-numeraire techs and water source types", + wst_i_ii(i,ii) "linking set between non-numeraire techs and numeraire techs", + ctt_i_ii(i,ii) "linking set between non-numeraire techs and numeraire techs"; + +*input parameters for non-numeraire techs and linking set only if Sw_WaterMain is ON and start with a blank slate +i_water_cooling(i) = no ; +i_ii_ctt_wst(i,ii,ctt,wst) = no ; + +$ifthen.coolingwatersets %GSw_WaterMain% == 1 +set i_water_cooling_temp(i) +/ +$offlisting +$include inputs_case%ds%i_coolingtech_watersource.csv +$include inputs_case%ds%i_coolingtech_watersource_upgrades.csv +$onlisting +/, + + i_ii_ctt_wst_temp(i,ii,ctt,wst) +/ +$offlisting +$ondelim +$include inputs_case%ds%i_coolingtech_watersource_link.csv +$include inputs_case%ds%i_coolingtech_watersource_upgrades_link.csv +$offdelim +$onlisting +/ ; + +i_water_cooling(i)$i_water_cooling_temp(i) = yes ; +i_ii_ctt_wst(i,ii,ctt,wst)$i_ii_ctt_wst_temp(i,ii,ctt,wst) = yes ; +$endif.coolingwatersets + +i_water(i)$[i_water_cooling(i) or i_water_nocooling(i)] = yes ; + +*linking sets between non-numeraire techs, numeraire techs, cooling tech, and water source +i_ctt(i,ctt)$[sum{(ii,wst)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; +i_wst(i,wst)$[sum{(ii,ctt)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; +*wst_i_ii(i,ii) and ctt_i_ii(i,ii) are identical linking set between non-numeraire and numeraire techs, +*kept both for clarity of use in cooling technology and water source related formulations +wst_i_ii(i,ii)$[sum{(wst,ctt)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; +ctt_i_ii(i,ii)$[sum{(wst,ctt)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; + +set i_numeraire(i) "numeraire techs that need cooling" ; +*i_numeraire(i) will be removed from valcap set as these technologies are ultimately +*expanded to non-numeraire techs. valcap will have non-numeraire techs if Sw_WaterMain=1 +*or will have numeraire techs otherwise. +i_numeraire(ii)$sum{(wst,ctt,i)$i_ii_ctt_wst(i,ii,ctt,wst), 1 } = yes ; + +table ctt_hr_mult(i,ctt) "heatrate multipliers to differentiate cooling technology types" +$offlisting +$ondelim +$include inputs_case%ds%heat_rate_mult.csv +$offdelim +$onlisting +; + +table ctt_cc_mult(i,ctt) "capital cost multipliers to differentiate cooling technology types" +$offlisting +$ondelim +$include inputs_case%ds%cost_cap_mult.csv +$offdelim +$onlisting +; + +table ctt_cost_vom_mult(i,ctt) "VOM cost multipliers to differentiate cooling technology types" +$offlisting +$ondelim +$include inputs_case%ds%cost_vom_mult.csv +$offdelim +$onlisting +; + +set allt "all potential years" +/ +$offlisting +$include inputs_case%ds%allt.csv +$onlisting +/ ; + +set i_geotech(i,geotech) "crosswalk between an individual geothermal technology and its category" +/ +$offlisting +$ondelim +$include inputs_case%ds%i_geotech.csv +$offdelim +$onlisting +/ ; + +set +*technology-specific subsets + battery(i) "battery storage technologies", + beccs(i) "Bio with CCS", + bio(i) "technologies that use only biofuel", + boiler(i) "technologies that use steam boilers" + canada(i) "Canadian imports", + ccs(i) "CCS technologies", + ccs_mod(i) "CCS technologies with moderate capture rate", + ccs_max(i) "CCS technologies with maximum capture rate", + ccsflex_byp(i) "Flexible CCS technologies with bypass", + ccsflex_dac(i) "Flexible CCS technologies with direct air capture", + ccsflex_sto(i) "Flexible CCS technologies with storage", + ccsflex(i) "Flexible CCS technologies", + cf_tech(i) "technologies that have a specified capacity factor" + coal_ccs(i) "technologies that use coal and have CCS", + coal(i) "technologies that use coal", + cofire(i) "cofire technologies", + combined_cycle(i) "combined cycle technologies", + combustion_turbine(i)"combustion turbine technologies", + consume(i) "technologies that consume electricity and add to load", + conv(i) "conventional generation technologies", + csp_storage(i) "csp generation technologies with thermal storage", + csp(i) "csp generation technologies", + csp1(i) "csp-tes generation technologies 1", + csp2(i) "csp-tes generation technologies 2", + csp3(i) "csp-tes generation technologies 3", + csp4(i) "csp-tes generation technologies 4", + dac(i) "direct air capture technologies", + distpv(i) "distpv (i.e., rooftop PV) generation technologies", + demand_flex(i) "demand flexibility technologies (includes DR and EVMC)", + dr_shed(i) "DR shed technologies" + evmc(i) "ev flexibility technologies", + evmc_storage(i) "ev flexibility as direct load control", + evmc_shape(i) "ev flexibility as adoptable change to load from response to pricing", + fossil(i) "fossil technologies" + fuel_cell(i) "fuel cell technologies", + gas_cc_ccs(i) "techs that are gas combined cycle and have CCS", + gas_cc(i) "techs that are gas combined cycle", + gas_ct(i) "techs that are gas combustion turbine", + gas(i) "techs that use gas (but not o-g-s)", + geo(i) "geothermal technologies", + geo_base(i) "geothermal technologies typically considered in model runs", + geo_hydro(i) "geothermal hydrothermal technologies", + geo_egs(i) "geothermal enhanced geothermal systems technologies", + geo_extra(i) "geothermal technologies not typically considered in model runs", + geo_egs_allkm(i) "egs (covering deep egs depths of all km) technologies", + geo_egs_nf(i) "egs (near-field) technologies", + h2_combustion(i) "h2-ct and h2-cc technologies", + h2_cc(i) "h2-cc technologies" + h2_ct(i) "h2-ct technologies", + h2(i) "hydrogen-producing technologies", + hyd_add_pump(i) "hydro techs with an added pump", + hydro_d(i) "dispatchable hydro technologies", + hydro_nd(i) "non-dispatchable hydro technologies", + hydro(i) "hydro technologies", + lfill(i) "land-fill gas technologies", + nondispatch(i) "technologies that are not dispatchable" + nuclear(i) "nuclear technologies", + ofswind(i) "offshore wind technologies", + ogs(i) "oil-gas-steam technologies", + onswind(i) "onshore wind technologies", + psh(i) "pumped hydro storage technologies", + pv(i) "all PV generation technologies", + pvb(i) "hybrid pv+battery technologies", + pvb1(i) "pvb generation technologies 1", + pvb2(i) "pvb generation technologies 2", + pvb3(i) "pvb generation technologies 3", + re(i) "renewable energy technologies", + refurbtech(i) "technologies that can be refurbished", + rsc_i(i) "technologies based on Resource supply curves", + smr(i) "steam methane reforming technologies", + storage_hybrid(i) "hybrid VRE-storage technologies", + storage_standalone(i) "stand alone storage technologies", + storage(i) "storage technologies", + storage_interday(i) "interday storage", + thermal_storage(i) "thermal storage technologies", + upgrade(i) "technologies that are upgrades from other technologies", + upv(i) "upv generation technologies", + vre_distributed(i) "distributed PV technologies", + vre_no_csp(i) "variable renewable energy technologies that are not csp", + vre_utility(i) "utility scale wind and PV technologies", + vre(i) "variable renewable energy technologies", + wind(i) "wind generation technologies", + +t(allt) "full set of years" /%startyear%*%endyear%/, + +* Each generation technology is broken out by class: +* 1. initial capacity: init-1, init-2, ..., init-n +* 2. prescribed capacity: prescribed +* 3. new capacity: new +* This allows us to distinguish between existing, prescribed, and model-chosen builds +* The number of classes is set by numhintage for initial capacity and numclass for new capacity +v "technology class" + / + init-1*init-%numhintage%, + new1*new%numclass% + /, + +initv(v) "initial technologies" /init-1*init-%numhintage%/, + +newv(v) "new tech set" /new1*new%numclass%/ + +; + +* DAC == direct air capture +* H2 == hydrogen +* Note: no longer tracking H2 by color. This means ReEDS internalizes +* emissions for any H2 produced for non-power sector demands +set p "products produced" +/ +$offlisting +$include inputs_case%ds%p.csv +$onlisting +/ ; + + +hyd_add_pump('hydED_pumped-hydro') = yes ; +hyd_add_pump('hydED_pumped-hydro-flex') = yes ; + +* Sets involved with resource supply curve definitions +set sc_cat "supply curve categories (capacity and cost)" +/ +$offlisting +$include inputs_case%ds%sc_cat.csv +$onlisting +/ ; + +set rscbin "Resource supply curves bins" /bin1*bin%numbins%/, + rscfeas(i,r,rscbin) "feasibility set for technologies that have resource supply curves" ; + +alias(r,rr,n,nn) ; +alias(v,vv) ; +alias(t,tt,ttt) ; +alias(st,ast) ; +alias(allt,alltt) ; +alias(cendiv,cendiv2) ; +alias(rscbin,arscbin) ; +alias(nercr,nercrr) ; +alias(transgrp,transgrpp) ; +alias(itlgrp,itlgrpp) ; + +parameter yeart(t) "numeric value for year", + year(allt) "numeric year value for allt" ; + +yeart(t) = t.val ; +year(allt) = allt.val ; + +set att(allt,t) "mapping set between allt and t" ; +att(allt,t)$[year(allt) = yeart(t)] = yes ; + +*the end year is defined dynamically +*if %end_year% < annual data, we are gonna get into trouble... +*if you aint first you're last +set tfirst(t) "first year", + tlast(t) "last year" ; + +*aint first you're last +tfirst(t)$[ord(t) = 1] = yes ; +tlast(t)$[ord(t) = smax(tt,ord(tt))] = yes ; + + +parameter deflator(allt) "Deflator values (for inflation) calculated from http://www.usinflationcalculator.com/inflation/consumer-price-index-and-annual-percent-changes-from-1913-to-2008/ using the Avg-Avg values" +/ +$offlisting +$ondelim +$include inputs_case%ds%deflator.csv +$offdelim +$onlisting +/ ; + +*various parameters needed for Present Value Factor (PVF) calculations before solving +*specifically these are used in the aggregating of the PVF of +*onm and capital when years are skipped +set yearafter "set to loop over for the final year calculation" +/ +$offlisting +$include inputs_case%ds%yearafter.csv +$onlisting +/ ; + +* --- Upgrade link definitions --- +Set upgrade_to(i,ii) "mapping set that allows for i to be upgraded to ii" + upgrade_from(i,ii) "mapping set that allows for i to be upgraded from ii" + upgrade_link(i,ii,iii) "indicates that tech i is upgradeable from ii with a delta base of iii" +/ +$offlisting +$ondelim +$include inputs_case%ds%upgrade_link.csv +$ifthen.ctech %GSw_WaterMain% == 1 +$include inputs_case%ds%upgradelink_water.csv +$endif.ctech +$offdelim +$onlisting +/ ; + +upgrade(i)$[sum{(ii,iii), upgrade_link(i,ii,iii) }] = yes ; +upgrade_to(i,ii)$[sum{iii, upgrade_link(i,iii,ii) }] = yes ; +upgrade_from(i,ii)$[sum{iii, upgrade_link(i,ii,iii) }] = yes ; + +set unitspec_upgrades(i) "upgraded technologies that get unit-specific characteristics" +/ +$offlisting +$ondelim +$include inputs_case%ds%unitspec_upgrades.csv +$offdelim +$onlisting +/ ; + +unitspec_upgrades(i)$[sum{ii$ctt_i_ii(i,ii), unitspec_upgrades(ii) }$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), unitspec_upgrades(ii) } ; + +* Ban upgrades if upgrades are turned off +ban(i)$[upgrade(i)$(not Sw_Upgrades)] = yes ; +bannew(i)$[upgrade(i)$(not Sw_Upgrades)] = yes ; + +* --- Read technology subset lookup table --- +Table i_subsets(i,i_subtech) "technology subset lookup table" +$offlisting +$ondelim +$include inputs_case%ds%tech-subset-table.csv +$offdelim +$onlisting +; + +*approach in cooling water formulation is populating parameters of numeraire tech (e.g. gas-CC) +*for non-numeraire techs (e.g. gas-CC_r_fsa; r = recirculating cooling, fsa=fresh surface appropriated water source) +*e.g. populate i_subsets for non-numeraire techs from numeraire tech using a linking set ctt_i_ii(i,ii) +i_subsets(i,i_subtech)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), i_subsets(ii,i_subtech) } ; + +*assign subtechs to each upgrade tech +*based on what they will be upgraded to +i_subsets(i,i_subtech)$[upgrade(i)$Sw_Upgrades] = + sum{ii$upgrade_to(i,ii), i_subsets(ii,i_subtech) } ; + +** define tech bans so that they are not defined in the technology subsets below ** +* switch based gen tech bans (see cases file for details) +if(Sw_BECCS = 0, + ban('beccs_mod') = yes ; + ban('beccs_max') = yes ; +) ; + +if(Sw_Biopower = 0, + ban('biopower') = yes ; +) ; + +if(Sw_Canada <> 1, + ban('can-imports') = yes ; +) ; + +if(Sw_CCS = 0, + ban(i)$i_subsets(i,'ccs') = yes ; +) ; + +if(Sw_CCSFLEX_BYP = 0, + ban('Gas-CC-CCS-F1') = yes ; + ban('coal-CCS-F1') = yes ; +) ; + +if(Sw_CCSFLEX_STO = 0, + ban('Gas-CC-CCS-F2') = yes ; + ban('coal-CCS-F2') = yes ; +) ; + +if(Sw_CCSFLEX_DAC = 0, + ban('Gas-CC-CCS-F3') = yes ; + ban('coal-CCS-F3') = yes ; +) ; + +if(Sw_CSP = 0, + ban(i)$i_subsets(i,'csp') = yes ; +) ; + +if(Sw_CSP = 1, + ban(i)$i_subsets(i,'csp2') = yes ; +) ; + +if(Sw_CoalIGCC = 0, + ban('Coal-IGCC') = yes ; +) ; + +if(Sw_CoalNew = 0, + ban('coal-new') = yes ; +) ; + +if(Sw_CofireNew = 0, + ban('CofireNew') = yes ; +) ; + +if(Sw_DAC = 0, + ban(i)$i_subsets(i,'dac') = yes ; +) ; + +if(Sw_DAC_Gas = 0, + ban("dac_gas") = yes ; +); + +if(Sw_EVMC = 0, + ban(i)$i_subsets(i,'evmc') = yes ; +) ; + +if(Sw_GasCT_Aero = 0, + ban('Gas-CT_aero') = yes ; +) ; + +if(Sw_GasCC_H_1x1 = 0, + ban('Gas-CC_H_1x1') = yes ; + ban('Gas-CC_H_1x1-CCS_mod') = yes ; + ban('Gas-CC_H_1x1-CCS_max') = yes ; +) ; + +if(Sw_GasCC_H_2x1 = 0, + ban('Gas-CC_H_2x1') = yes ; + ban('Gas-CC_H_2x1-CCS_mod') = yes ; + ban('Gas-CC_H_2x1-CCS_max') = yes ; +) ; + +if(Sw_Geothermal = 0, + ban(i)$i_subsets(i,'geo') = yes ; +) ; + +if(Sw_Geothermal = 1, + ban(i)$i_subsets(i,'geo_extra') = yes ; +) ; + +if(Sw_H2 = 0, + ban(i)$i_subsets(i,'h2') = yes ; +) ; + +if(Sw_H2_SMR = 0, + ban(i)$i_subsets(i,'smr') = yes ; +) ; + +if(Sw_H2Combustion = 0, + ban(i)$i_subsets(i,'h2_combustion') = yes ; +) ; + +if(Sw_H2CombinedCycle = 0, + ban(i)$i_subsets(i,'h2_cc') = yes ; +) ; + +if(Sw_H2Combustionupgrade = 0, + ban(i)$[i_subsets(i,'h2_combustion')$upgrade(i)] = yes ; +) ; + +if(Sw_FuelCell = 0, + ban(i)$i_subsets(i,'fuel_cell') = yes ; +) ; + +if(Sw_LfillGas = 0, + ban('lfill-gas') = yes ; +) ; + +if(Sw_MaxCaptureCCSTechs = 0, + ban(i)$[i_subsets(i,'ccs_max')] = yes ; +) ; + +if(Sw_Nuclear = 0, + bannew(i)$i_subsets(i,'nuclear') = yes ; +) ; + +if(Sw_NuclearSMR = 0, + ban("Nuclear-SMR") = yes ; +) ; + +if(Sw_OfsWind = 0, + ban(i)$i_subsets(i,'ofswind') = yes ; +) ; + +if(Sw_OnsWind6to10 = 0, + bannew('wind-ons_6') = yes ; + bannew('wind-ons_7') = yes ; + bannew('wind-ons_8') = yes ; + bannew('wind-ons_9') = yes ; + bannew('wind-ons_10') = yes ; +) ; + +if(Sw_DRShed = 0, + ban(i)$i_subsets(i,'DR_SHED') = yes ; +) ; + +* always allow PSH to use fresh surface water (fsa, fsu) +* do not allow new PSH to use saline surface water +bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'ss') }] = YES ; +$ifthen.pshwat %GSw_PSHwatertypes% == 0 +* do not allow saline ground water or wastewater effluent +bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'sg') }] = YES ; +bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'ww') }] = YES ; +$elseif.pshwat %GSw_PSHwatertypes% == 1 +* option to also prohibit fresh groundwater +bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'sg') }] = YES ; +bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'ww') }] = YES ; +bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'fg') }] = YES ; +$elseif.pshwat %GSw_PSHwatertypes% == 2 +* option 2 allows fresh/saline ground water and wastewater +$else.pshwat +$endif.pshwat + +*** Ban hybrid storage techs based on Sw_HybridPlant switch +* 0: Ban all storage, including CSP +if(Sw_HybridPlant = 0, + ban(i)$i_subsets(i,'storage_hybrid') = yes ; +) ; +* 1: Allow CSP, ban all other storage +if(Sw_HybridPlant = 1, + ban(i)$[i_subsets(i,'storage_hybrid')$(not sameas(i,'csp_storage'))] = yes ; + ban(i)$i_subsets(i,'csp_storage') = no ; +) ; +* 2: Allow hybrid plants, excluding CSP +if(Sw_HybridPlant = 2, + ban(i)$[i_subsets(i,'storage_hybrid')$(not sameas(i,'csp_storage'))] = no ; + ban(i)$i_subsets(i,'csp_storage') = yes ; +) ; +* 3: Allow CSP and all other hybrid plants (note csp_storage bans are controlled by Sw_CSP) +if(Sw_HybridPlant = 3, + ban(i)$[i_subsets(i,'storage_hybrid')$(not sameas(i,'csp_storage'))] = no ; +) ; + +*ban techs in hybrid PV+battery if the switch calls for it +if(Sw_PVB=0, + ban(i)$i_subsets(i,'pvb') = yes ; + bannew(i)$i_subsets(i,'pvb') = yes ; +) ; + +* Ban PVB_Types that aren't included in the model +$ifthen.pvb123 %GSw_PVB_Types% == '1_2' + ban(i)$i_subsets(i,'pvb3') = yes ; +$endif.pvb123 +$ifthen.pvb12 %GSw_PVB_Types% == '1' + ban(i)$i_subsets(i,'pvb2') = yes ; + ban(i)$i_subsets(i,'pvb3') = yes ; +$endif.pvb12 + +*** Ban storage techs based on Sw_Storage switch +* 0: Ban all storage +if(Sw_Storage = 0, + ban(i)$i_subsets(i,'storage_standalone') = yes ; + Sw_BatteryMandate = 0 ; +) ; +* 1: Keep all storage + +if(Sw_WaterMain = 1, +*By default, ban new builds with bannew_ctt cooling techs for all i, +bannew(i)$[sum{ctt$bannew_ctt(ctt), i_ctt(i,ctt) }] = YES ; + +* ban new builds of Nuclear and coal-CCS with dry cooling techs as cooling requirements +* of nuclear and coal-CCS make dry cooling impractical +bannew(i)$[sum{ctt_i_ii(i,'Nuclear'), i_ctt(i,'d') }] = YES ; +bannew(i)$[sum{ctt_i_ii(i,'coal-CCS_mod'), i_ctt(i,'d') }] = YES ; +bannew(i)$[sum{ctt_i_ii(i,'coal-CCS_max'), i_ctt(i,'d') }] = YES ; +bannew(i)$[sum{ctt_i_ii(i,'Nuclear-SMR'), i_ctt(i,'d') }] = YES ; + +*ban and bannew all non-numeraire techs that are derived from ban numeraire techs +ban(i)$sum{ii$ban(ii), ctt_i_ii(i,ii) } = YES ; +bannew(i)$sum{ii$bannew(ii), ctt_i_ii(i,ii) } = YES ; + +* ban new builds of water sources included in bannew_wst for all i +bannew(i)$[sum{wst$bannew_wst(wst), i_wst(i,wst) }] = YES ; +* end parentheses for Sw_WaterMain = 1 +) ; + +* Turn off canadian imports as an option when running NARIS +$ifthen.naris %GSw_Region% == "naris" + ban(i)$i_subsets(i,'canada') = yes ; +$endif.naris + +* Ban DUPV, CSP, and Geothermal resources that do not remain after aggregation +set resourceclass "renewable resource classes" +/ +$offlisting +$include inputs_case%ds%resourceclass.csv +$onlisting +/ ; +parameter resourceclassnum(resourceclass) "numeric value for resource class" ; +resourceclassnum(resourceclass) = resourceclass.val ; +set tech_resourceclass(i,resourceclass) "map from CSP/DUPV techs to resource classes" +/ +$offlisting +$ondelim +$include inputs_case%ds%tech_resourceclass.csv +$offdelim +$onlisting +/ ; +* There are 12 CSP resource classes by default. If Sw_NumCSPclasses < 12, we ban the +* CSP techs with resource class > Sw_NumCSPclasses +if(Sw_NumCSPclasses < 12, +ban(i)$[i_subsets(i,'csp') + $sum{resourceclass$tech_resourceclass(i,resourceclass), + resourceclassnum(resourceclass)>Sw_NumCSPclasses }] = yes ; +) ; +* If Sw_CSPRemoveLow is turned on, remove the last (worst) CSP class (which will be +* equal to Sw_NumCSPclasses) +if(Sw_CSPRemoveLow = 1, +ban(i)$[i_subsets(i,'csp') + $sum{resourceclass$tech_resourceclass(i,resourceclass), + resourceclassnum(resourceclass)=Sw_NumCSPclasses }] = yes ; +) ; + +*Ban Geothermal resources that do not remain after aggregation +if(Sw_NumGeoclasses < 10, +ban(i)$[i_subsets(i,'geo') + $sum{resourceclass$tech_resourceclass(i,resourceclass), + resourceclassnum(resourceclass)>Sw_NumGeoclasses }] = yes ; +) ; + +*Ingest list of new nuclear restricted BAs ('p' regions), ba list is consistent with NCSL restrictions. +*https://www.ncsl.org/research/environment-and-natural-resources/states-restrictions-on-new-nuclear-power-facility.aspx +$onempty +set nuclear_ba_ban(r) "List of BAs where new nuclear builds are restricted" +/ +$offlisting +$include inputs_case%ds%nuclear_ba_ban_list.csv +$onlisting +/ ; +$offempty + +* techs banned by state (note that this only applies to valinv later) +$onempty +table tech_banned(i,r) "Banned technologies by model region" +$offlisting +$ondelim +$include inputs_case%ds%techs_banned.csv +$offdelim +$onlisting +; +$offempty + +* --- Remove banned technologies from upgrade links --- +upgrade_link(i,ii,iii)$[ban(i) or ban(ii) or ban(iii)] = no ; +upgrade(i)$[not sum{(ii,iii), upgrade_link(i,ii,iii) }] = no ; +upgrade_to(i,ii)$[not sum{iii, upgrade_link(i,iii,ii) }] = no ; +upgrade_from(i,ii)$[not sum{iii, upgrade_link(i,ii,iii) }] = no ; + +* --- define technology subsets --- +battery(i)$(not ban(i)) = yes$i_subsets(i,'battery') ; +beccs(i)$(not ban(i)) = yes$i_subsets(i,'beccs') ; +bio(i)$(not ban(i)) = yes$i_subsets(i,'bio') ; +boiler(i)$(not ban(i)) = yes$i_subsets(i,'boiler') ; +canada(i)$(not ban(i)) = yes$i_subsets(i,'canada') ; +ccs(i)$(not ban(i)) = yes$i_subsets(i,'ccs') ; +ccs_mod(i)$(not ban(i)) = yes$i_subsets(i,'ccs_mod') ; +ccs_max(i)$(not ban(i)) = yes$i_subsets(i,'ccs_max') ; +ccsflex_byp(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex_byp') ; +ccsflex_dac(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex_dac') ; +ccsflex_sto(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex_sto') ; +ccsflex(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex') ; +cf_tech(i)$(not ban(i)) = yes$i_subsets(i,'cf_tech') ; +coal_ccs(i)$(not ban(i)) = yes$i_subsets(i,'coal_ccs') ; +coal(i)$(not ban(i)) = yes$i_subsets(i,'coal') ; +cofire(i)$(not ban(i)) = yes$i_subsets(i,'cofire') ; +combined_cycle(i)$(not ban(i)) = yes$i_subsets(i,'combined_cycle') ; +combustion_turbine(i)$(not ban(i)) = yes$i_subsets(i,'combustion_turbine') ; +consume(i)$(not ban(i)) = yes$i_subsets(i,'consume') ; +conv(i)$(not ban(i)) = yes$i_subsets(i,'conv') ; +csp_storage(i)$(not ban(i)) = yes$i_subsets(i,'csp_storage') ; +csp(i)$(not ban(i)) = yes$i_subsets(i,'csp') ; +csp1(i)$(not ban(i)) = yes$i_subsets(i,'csp1') ; +csp2(i)$(not ban(i)) = yes$i_subsets(i,'csp2') ; +csp3(i)$(not ban(i)) = yes$i_subsets(i,'csp3') ; +csp4(i)$(not ban(i)) = yes$i_subsets(i,'csp4') ; +dac(i)$(not ban(i)) = yes$i_subsets(i,'dac') ; +distpv(i)$(not ban(i)) = yes$i_subsets(i,'distpv') ; +dr_shed(i)$(not ban(i)) = yes$i_subsets(i,'dr_shed') ; +demand_flex(i)$(not ban(i)) = yes$i_subsets(i,'demand_flex') ; +evmc(i)$(not ban(i)) = yes$i_subsets(i,'evmc') ; +evmc_storage(i)$(not ban(i)) = yes$i_subsets(i,'evmc_storage') ; +evmc_shape(i)$(not ban(i)) = yes$i_subsets(i,'evmc_shape') ; +fossil(i)$(not ban(i)) = yes$i_subsets(i,'fossil') ; +fuel_cell(i)$(not ban(i)) = yes$i_subsets(i,'fuel_cell') ; +gas_cc_ccs(i)$(not ban(i)) = yes$i_subsets(i,'gas_cc_ccs') ; +gas_cc(i)$(not ban(i)) = yes$i_subsets(i,'gas_cc') ; +gas_ct(i)$(not ban(i)) = yes$i_subsets(i,'gas_ct') ; +gas(i)$(not ban(i)) = yes$i_subsets(i,'gas') ; +geo(i)$(not ban(i)) = yes$i_subsets(i,'geo') ; +geo_base(i)$(not ban(i)) = yes$i_subsets(i,'geo_base') ; +geo_hydro(i)$(not ban(i)) = yes$i_subsets(i,'geo_hydro') ; +geo_egs(i)$(not ban(i)) = yes$i_subsets(i,'geo_egs') ; +geo_extra(i)$(not ban(i)) = yes$i_subsets(i,'geo_extra') ; +geo_egs_allkm(i)$(not ban(i)) = yes$i_subsets(i,'geo_egs_allkm') ; +geo_egs_nf(i)$(not ban(i)) = yes$i_subsets(i,'geo_egs_nf') ; +h2_combustion(i)$(not ban(i)) = yes$i_subsets(i,'h2_combustion') ; +h2_cc(i)$(not ban(i)) = yes$i_subsets(i,'h2_cc') ; +h2_ct(i)$(not ban(i)) = yes$i_subsets(i,'h2_ct') ; +h2(i)$(not ban(i)) = yes$i_subsets(i,'h2') ; +hydro_d(i)$(not ban(i)) = yes$i_subsets(i,'hydro_d') ; +hydro_nd(i)$(not ban(i)) = yes$i_subsets(i,'hydro_nd') ; +hydro(i)$(not ban(i)) = yes$i_subsets(i,'hydro') ; +lfill(i)$(not ban(i)) = yes$i_subsets(i,'lfill') ; +nondispatch(i)$(not ban(i)) = yes$i_subsets(i,'nondispatch') ; +nuclear(i)$(not ban(i)) = yes$i_subsets(i,'nuclear') ; +ofswind(i)$(not ban(i)) = yes$i_subsets(i,'ofswind') ; +ogs(i)$(not ban(i)) = yes$i_subsets(i,'ogs') ; +onswind(i)$(not ban(i)) = yes$i_subsets(i,'onswind') ; +psh(i)$(not ban(i)) = yes$i_subsets(i,'psh') ; +pv(i)$(not ban(i)) = yes$i_subsets(i,'pv') ; +pvb(i)$(not ban(i)) = yes$i_subsets(i,'pvb') ; +pvb1(i)$(not ban(i)) = yes$i_subsets(i,'pvb1') ; +pvb2(i)$(not ban(i)) = yes$i_subsets(i,'pvb2') ; +pvb3(i)$(not ban(i)) = yes$i_subsets(i,'pvb3') ; +re(i)$(not ban(i)) = yes$i_subsets(i,'re') ; +refurbtech(i)$(not ban(i)) = yes$i_subsets(i,'refurbtech') ; +rsc_i(i)$(not ban(i)) = yes$i_subsets(i,'rsc') ; +smr(i)$(not ban(i)) = yes$i_subsets(i,'smr') ; +storage_hybrid(i)$(not ban(i)) = yes$i_subsets(i,'storage_hybrid') ; +storage_interday(i)$(not ban(i)) = yes$i_subsets(i,'storage_interday') ; +storage_standalone(i)$(not ban(i)) = yes$i_subsets(i,'storage_standalone') ; +storage(i)$(not ban(i)) = yes$i_subsets(i,'storage') ; +thermal_storage(i)$(not ban(i)) = yes$i_subsets(i,'thermal_storage') ; +upv(i)$(not ban(i)) = yes$i_subsets(i,'upv') ; +vre_distributed(i)$(not ban(i)) = yes$i_subsets(i,'vre_distributed') ; +vre_no_csp(i)$(not ban(i)) = yes$i_subsets(i,'vre_no_csp') ; +vre_utility(i)$(not ban(i)) = yes$i_subsets(i,'vre_utility') ; +vre(i)$(not ban(i)) = yes$i_subsets(i,'vre') ; +wind(i)$(not ban(i)) = yes$i_subsets(i,'wind') ; + +set coal_noccs(i) "technologies that use coal and do not have CCS, aka unabated coal" ; +coal_noccs(i)$[coal(i)$(not ccs(i))] = yes ; + +* Create mapping of technology groups to technologies +set tg_i(tg,i) "technologies that belong in tech group tg" ; +tg_i('wind-ons',i)$onswind(i) = yes ; +tg_i('wind-ofs',i)$ofswind(i) = yes ; +tg_i('pv',i)$[(pv(i) or pvb(i))$(not distpv(i))] = yes ; +tg_i('csp',i)$csp(i) = yes ; +tg_i('gas',i)$gas(i) = yes ; +tg_i('coal',i)$coal(i) = yes ; +tg_i('nuclear',i)$nuclear(i) = yes ; +tg_i('battery',i)$battery(i) = yes ; +tg_i('hydro',i)$hydro(i) = yes ; +tg_i('h2',i)$h2_combustion(i) = yes ; +tg_i('geothermal',i)$geo(i) = yes ; +tg_i('biomass',i)$bio(i) = yes ; +tg_i('pumped-hydro',i)$psh(i) = yes ; +tg_i('dr_shed',i)$dr_shed(i) = yes ; + +*Hybrid pv+battery (PVB) configurations are defined by: +* (1) inverter loading ratio (DC/AC) and +* (2) battery capacity ratio (Battery/PV Array) +*Each configuration has ten resource classes +*The PV portion refers to "UPV", but not "DUPV" +*The battery portion refers to "battery_li" +set pvb_config "set of hybrid pv+battery configurations" +/ +$offlisting +$include inputs_case%ds%pvb_config.csv +$onlisting +/ ; + +set pvb_agg(pvb_config,i) "crosswalk between hybrid pv+battery configurations and technology options" +/ +$offlisting +$ondelim +$include inputs_case%ds%pvb_agg.csv +$offdelim +$onlisting +/ ; + +*add non-numeraire CSPs in index i of already defined set tg_i(tg,i) +tg_i("csp",i)$[(csp1(i) or csp2(i) or csp3(i) or csp4(i))$Sw_WaterMain] = yes ; + +*Offhsore wind turbine types +set ofstype "offshore types used in offshore requirement constraint (eq_RPS_OFSWind)" +/ +$offlisting +$include inputs_case%ds%ofstype.csv +$onlisting +/ ; + +set ofstype_i(ofstype,i) "crosswalk between ofstype and i" +/ +$offlisting +$ondelim +$include inputs_case%ds%ofstype_i.csv +$offdelim +$onlisting +/ ; + +storage_interday(i)$(Sw_InterDayLinkage = 0) = no ; + +$onempty +parameter water_with_cons_rate(i,ctt,w) "--gal/MWh-- technology specific-cooling tech based water withdrawal and consumption data" +/ +$offlisting +$ondelim +$include inputs_case%ds%water_with_cons_rate.csv +$offdelim +$onlisting +/ +; +$offempty + +$onempty +* Water requirement if all filling takes place in 1 year and minimum reservoir level is 15% of max volume +table water_req_psh(r,rscbin) "--Mgal/MW/yr-- required water for PSH during construction to fill reservoir" +$offlisting +$ondelim +$include inputs_case%ds%water_req_psh_10h_1_51.csv +$offdelim +$onlisting +; +$offempty + +* Recalculate PSH water requirements based on user input filling time +scalar psh_fillyrs "number of years assumed to fill PSH reservoirs" /%GSw_PSHfillyears%/ ; +water_req_psh(r,rscbin) = round(water_req_psh(r,rscbin) / psh_fillyrs, 6) ; + +*populate the water withdrawal and consumption data to non-numeraire technologies +*based on numeraire techs and cooling technologies types to avoid repetitive +*entry of data in the input data file and provide flexibility +*in populating data if new combinations come along the way +water_with_cons_rate(i,ctt,w)$i_water_cooling(i) = + sum{(ii,wst)$i_ii_ctt_wst(i,ii,ctt,wst), water_with_cons_rate(ii,ctt,w) } ; + +*CSP techs have same water withdrawal and consumption rates; populating all CSP data with the data of csp1_1 +water_with_cons_rate(i,ctt,w)$[i_water_cooling(i)$(csp1(i) or csp2(i) or csp3(i) or csp4(i))] = + sum{(ii,wst)$i_ii_ctt_wst(i,ii,ctt,wst), water_with_cons_rate("csp1_1",ctt,w) } ; + +water_with_cons_rate(ii,ctt,w)$[sum{(wst,i)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = no ; + +parameter water_rate(i,w) "--gal/MWh-- water withdrawal/consumption w rate by technology i" ; +* adding geothermal categories for water accounting +i_water(i)$geo(i) = yes ; +water_with_cons_rate(i,ctt,w)$geo(i) = water_with_cons_rate("geothermal",ctt,w) ; + +* Till this point, i already has non-numeraire techs (e.g., gas-CC_o_fsa, gas-CC_r_fsa, +*and gas-CC_r_fg) instead of numeraire technology (e.g., gas-CC) +* The line below just removes ctt dimension, by summing over ctt. +water_rate(i,w)$i_water(i) = sum{ctt, water_with_cons_rate(i,ctt,w) } ; + +water_rate(i,w)$upgrade(i) = sum{ii$upgrade_to(i,ii), water_rate(ii,w) } ; + +set dispatchtech(i) "technologies that are dispatchable", + noret_upgrade_tech(i) "upgrade techs that do not retire", + retiretech(i,v,r,t) "combinations of i,v,r,t that can be retired", + sccapcosttech(i) "technologies that have their capital costs embedded in supply curves", + inv_cond(i,v,r,t,tt) "allows an investment in tech i of class v to be built in region r in year tt and usable in year t" ; + +noret_upgrade_tech(i)$hyd_add_pump(i) = yes ; +noret_upgrade_tech(i)$[(coal_ccs(i) or gas_cc_ccs(i))$upgrade(i)$Sw_CCS_NoRetire] = yes ; +dispatchtech(i)$[not(vre(i) or hydro_nd(i) or ban(i))] = yes ; +sccapcosttech(i)$[hydro(i) or psh(i) or dr_shed(i)] = yes ; + +*initialize sets to "no" +retiretech(i,v,r,t) = no ; +inv_cond(i,v,r,t,tt) = no ; + +parameter min_retire_age(i) "minimum retirement age by technology" +/ +$offlisting +$ondelim +$include inputs_case%ds%min_retire_age.csv +$offdelim +$onlisting +/ ; + +min_retire_age(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), min_retire_age(ii) } ; +* if GSw_Clean_Air_Act is enabled, there is no minimum retire age for coal plants +min_retire_age(i)$[coal(i)$Sw_Clean_Air_Act] = no ; + +parameter retire_penalty(allt) "--fraction-- penalty for retiring a power plant expressed as a fraction of FOM" +/ +$offlisting +$ondelim +$include inputs_case%ds%retire_penalty.csv +$offdelim +$onlisting + / ; + + +set prescriptivelink0(pcat,ii) "initial set of prescribed categories and their technologies - used in assigning prescribed builds" +/ +$offlisting +$ondelim +$include inputs_case%ds%prescriptivelink0.csv +$offdelim +$onlisting +/ ; + +*include non-numeraire CSPs and then exclude numeraire CSPs in ii dimension of +*prescriptivelink0(pcat,ii) set when Sw_WaterMain is ON +prescriptivelink0("csp-ws",ii)$[(csp1(ii) or csp2(ii) or csp3(ii) or csp4(ii))$Sw_WaterMain] = yes ; +prescriptivelink0("csp-ws",ii)$[csp(ii)$i_numeraire(ii)$Sw_WaterMain] = no ; + +set prescriptivelink(pcat,i) "final set of prescribed categories and their technologies - used in the model" ; + +prescriptivelink(pcat,i)$prescriptivelink0(pcat,i) = yes ; + +alias(pcat,ppcat) ; + +* active prescriptivelink for all techs not included in the table above +* but restrict out csp techs in this calculation - since they +* are indexed by a separate pcat (csp-ws) and have special considerations +prescriptivelink(pcat,i)$[sameas(pcat,i)$(not sum{ppcat, prescriptivelink(ppcat,i) })$(not csp1(i))] = yes ; +*only geo_hydro techs are considered to meet geothermal prescriptions +prescriptivelink(pcat,i)$[geo_extra(i)] = no ; + + +*upgrades have no prescriptions +prescriptivelink(pcat,i)$[upgrade(i)] = no ; + +set rsc_agg(i,ii) "rsc technologies that belong to the same class" ; + +set tg_rsc_cspagg(i,ii) "csp technologies that belong to the same class" +/ +$offlisting +$ondelim +$include inputs_case%ds%tg_rsc_cspagg.csv +$offdelim +$onlisting +/ ; + +set tg_rsc_cspagg_tmp(i,ii) "expanded tg_rsc_cspagg(i,ii) to include new non-numeraire CSP techs" ; + +*input parameters for linking set only when Sw_WaterMain is ON and start with a blank slate +tg_rsc_cspagg_tmp(i,ii) = no ; +$ifthen.coolingwatersets %GSw_WaterMain% == 1 +set tg_rsc_cspagg_tmp_temp(i,ii) +/ +$offlisting +$ondelim +$include inputs_case%ds%tg_rsc_cspagg_tmp.csv +$offdelim +$onlisting +/ ; +tg_rsc_cspagg_tmp(i,ii)$tg_rsc_cspagg_tmp_temp(i,ii) = yes ; +$endif.coolingwatersets + +*include non-numeraire CSPs and then exclude numeraire CSPs in ii dimension +*of tg_rsc_cspagg(i,ii) set when Sw_WaterMain is ON +tg_rsc_cspagg(i,ii)$[tg_rsc_cspagg_tmp(i,ii)$Sw_WaterMain] = yes ; +tg_rsc_cspagg(i,ii)$[csp(ii)$i_numeraire(ii)$Sw_WaterMain] = no ; + +$ontext +Replicating the construct for CSP to link Hybrid PV+battery and UPV for the resoruce supply curve constraints + eq_rsc_invlim(i,bin).. sum{ii$rsc_agg(i,ii), INV_RSC(i,bin) } <= bin_capacity(i,bin) ; + When i = "upv_1", this constraint looks like: + eq_rsc_invlim("upv_1",bin).. INV_RSC("pvb1_1") + INV_RSC("upv_1") <= bin_capacity("upv_1",bin) + Because the first index of rsc_agg is only a UPV technology the above constraint will never be generated when "i" is a pvb(i). +$offtext + +set tg_rsc_upvagg(i,ii) "pv and pvb technologies that belong to the same class" +/ +$offlisting +$ondelim +$include inputs_case%ds%tg_rsc_upvagg.csv +$offdelim +$onlisting +/ ; + +*initialize rsc aggregation set for 'i'='ii' +*rsc_agg(i,ii)$[sameas(i,ii)$(not csp(i))$(not csp(ii))$rsc_i(i)$rsc_i(ii)] = yes ; +rsc_agg(i,ii)$[sameas(i,ii)$rsc_i(i)$rsc_i(ii)] = yes ; +*add csp to rsc aggregation set +rsc_agg(i,ii)$tg_rsc_cspagg(i,ii) = yes ; +*add upv to rsc aggregation set +rsc_agg(i,ii)$tg_rsc_upvagg(i,ii) = yes ; +*All PSH types use the same supply curve +rsc_agg('pumped-hydro',ii)$psh(ii) = yes ; +rsc_agg(i,ii)$[ban(i) or ban(ii)] = no ; + +*============================ +* -- Demand flexibility setup -- +*============================ + +set flex_type "set of demand flexibility types: daily, previous, next, adjacent" +/ +$offlisting +$include inputs_case%ds%flex_type.csv +$onlisting +/ ; + +*====================================== +* --- Begin hierarchy --- +*====================================== + +set hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) "hierarchy of various regional definitions" +/ +$offlisting +$ondelim +$include inputs_case%ds%hierarchy.csv +$offdelim +$onlisting +/ ; + + +* Mappings between r and other region sets +set r_itlgrp(r,itlgrp) + r_nercr(r,nercr) + r_transreg(r,transreg) + r_transgrp(r,transgrp) + r_cendiv(r,cendiv) + r_st(r,st) + r_interconnect(r,interconnect) + r_country(r,country) + r_usda(r,usda_region) + r_h2ptcreg(r,h2ptcreg) + r_hurdlereg(r,hurdlereg) + r_ccreg(r,ccreg) +; + +r_nercr(r,nercr) $sum{( transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_transreg(r,transreg) $sum{(nercr, transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_transgrp(r,transgrp) $sum{(nercr,transreg, cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_cendiv(r,cendiv) $sum{(nercr,transreg,transgrp, st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_st(r,st) $sum{(nercr,transreg,transgrp,cendiv, interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_interconnect(r,interconnect) $sum{(nercr,transreg,transgrp,cendiv,st, country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_country(r,country) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect, usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_usda(r,usda_region) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country, h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_h2ptcreg(r,h2ptcreg) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region, hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_hurdlereg(r,hurdlereg) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg, ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; +r_ccreg(r,ccreg) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg ) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; + +set r_itlgrp(r,itlgrp) "mapping of r to itlgrp" +/ +$offlisting +$ondelim +$include inputs_case%ds%hierarchy_itlgrp.csv +$offdelim +$onlisting +/ ; + +* Region hierarchy level within which to site optimally sited load +alias(%GSw_LoadSiteReg%, loadsitereg) ; +set r_loadsitereg(r,loadsitereg) "Mapping from model zones to loadsite regions" ; +r_loadsitereg(r,%GSw_LoadSiteReg%) = r_%GSw_LoadSiteReg%(r,%GSw_LoadSiteReg%) ; + + +*================================ +*sets that define model boundaries +*================================ +set tmodel(t) "years to include in the model", + tfix(t) "years to fix variables over when summing over previous years", + tprev(t,tt) "previous modeled tt from year t", + stfeas(st) "states to include in the model", + tsolved(t) "years that have solved" ; + +*following parameters get re-defined when the solve years have been declared +parameter mindiff(t) "minimum difference between t and all other tt that are in tmodel(t)" ; + + +tmodel(t) = no ; +tfirst(t) = no ; +tlast(t) = no ; +tfix(t) = no ; +stfeas(st) = no ; +tprev(t,tt) = no ; +tsolved(t) = no ; + + +*============================== +* Year specification +*============================== + +* declared over allt to allow for external data files that extend beyond end_year +set tmodel_new(allt) "years to run the model" +/ +$offlisting +$include inputs_case%ds%modeledyears.csv +$onlisting +/ ; + +tmodel_new(allt)$[year(allt) > %endyear%]= no ; + +*reset the first and last year indices of the model +tfirst(t)$[ord(t) = smin{tt$tmodel_new(tt), ord(tt) }] = yes ; +tlast(t)$[ord(t) = smax{tt$tmodel_new(tt), ord(tt) }] = yes ; + +*now get rid of all non-immediately-previous values (it takes three steps to get there...) +tprev(t,tt)$[tmodel_new(t)$tmodel_new(tt)$(tt.valmindiff(t))] = no ; + +* In order to fill all necessary dimensions of upgrade techs parameters, we require +* Sw_UpgradeYear in ban(i) to be a modeled year and thus we compute as either +* the GSw_UpgradeYear option or the next modeled years after GSw_UpgradeYear + +* reset sw_upgradeyear +Sw_UpgradeYear = 0 ; + +* if the upgradeyear is modeled set it to upgrade year +Sw_UpgradeYear$tmodel_new("%GSw_UpgradeYear%") = %GSw_UpgradeYear% ; + +* if upgrade year is not modeled, set it to the next available upgrade year +Sw_UpgradeYear$[(not Sw_UpgradeYear)] = + smin(tt$[(tt.val>=%GSw_UpgradeYear%)$tmodel_new(tt)],tt.val) ; + + +* if caa_coal_retire_year is not in the set of years being modeled, then set it to the first year that is modeled after caa_coal_retire_year +caa_coal_retire_year$[not sum{tt$[tt.val=caa_coal_retire_year], tmodel_new(tt) }] = + smin(tt$[(tt.val>=caa_coal_retire_year)$tmodel_new(tt)],tt.val) ; + +* if Sw_Clean_Air_Act = 0, then set caa_coal_retire_year to the last solve year +caa_coal_retire_year$[Sw_Clean_Air_Act = 0] = smax(tmodel_new, tmodel_new.val) ; + +*====================================== +* ---------- Bintage Mapping ---------- +*====================================== +*following set is un-assumingly important +*it allows for the investment of bintage 'v' at time 't' + +*table ivtmap(i,t) +* declared over allt to allow for external data files that extend beyond end_year +table ivt_num(i,allt) "number associated with bin for ivt calculation" +$offlisting +$ondelim +$include inputs_case%ds%ivt.csv +$offdelim +$onlisting +; + + +set ivt(i,v,t) "mapping set between i v and t - for new technologies" ; +ivt(i,newv,t)$[ord(newv) = ivt_num(i,t)] = yes ; + +*Expand ivt to water techs +ivt(i,v,t)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), ivt(ii,v,t) } ; + +*Also expand ivt_num to water techs for use in Augur +ivt_num(i,t)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), ivt_num(ii,t) } ; + + +*important assumption here that upgrade technologies +*receive the same binning assumptions as the technologies +*that they are upgraded to - this allows for easier translation +*and mapping of plant characteristics (cost_vom, cost_fom, heat_rate) +ivt(i,newv,t)$[(yeart(t)>=Sw_UpgradeYear)$upgrade(i)] = + sum{ii$upgrade_to(i,ii), ivt(ii,newv,t) } ; + + +parameter countnc(i,newv) "number of years in each newv set" ; + +*add 1 for each t item in the ct_corr set +countnc(i,newv) = sum{t$ivt(i,newv,t),1} ; + +set one_newv(i) "technologies that only have one vintage for new plants" ; + +* Only one vintage is present if there is a new1 for that technology... +one_newv(i)$[sum{t, ivt(i,"new1",t) }] = yes ; +* ...and there are no entries other than new1 for that technology +one_newv(i)$sum{(v,t)$[not sameas(v,"new1")], ivt(i,v,t) } = no ; + +*===================================== +*--- basic parameter declarations --- +*===================================== + +parameter crf(t) "--unitless-- capital recovery factor" +/ +$offlisting +$ondelim +$include inputs_case%ds%crf.csv +$offdelim +$onlisting +/, + crf_co2_incentive(t) "--unitless-- capital recovery factor using a 12-year economic lifetime" +/ +$offlisting +$ondelim +$include inputs_case%ds%crf_co2_incentive.csv +$offdelim +$onlisting +/, + + crf_h2_incentive(t) "--unitless-- capital recovery factor using a 10-year economic lifetime" +/ +$offlisting +$ondelim +$include inputs_case%ds%crf_h2_incentive.csv +$offdelim +$onlisting +/, + +* pvf_capital and pvf_onm here are for intertemporal mode. These parameters +* are overwritten for sequential mode in d_solveprep.gms. + pvf_capital(t) "--unitless-- present value factor for overnight capital costs" +/ +$offlisting +$ondelim +$include inputs_case%ds%pvf_cap.csv +$offdelim +$onlisting +/, + pvf_onm(t)"--unitless-- present value factor of operations and maintenance costs" +/ +$offlisting +$ondelim +$include inputs_case%ds%pvf_onm_int.csv +$offdelim +$onlisting +/, + tc_phaseout_mult(i,v,t) "--unitless-- multiplier that reduces the value of the PTC and ITC after the phaseout trigger has been hit", + tc_phaseout_mult_t(i,t) "--unitless-- a single year's multiplier of tc_phaseout_mult", + tc_phaseout_mult_t_load(i,t) "--unitless-- a single year's multiplier of tc_phaseout_mult", + co2_captured_incentive(i,v,r,allt) "--$/tco2 stored-- incentive on CO2 captured dependent on technology" + co2_captured_incentive_in(i,v,allt) "--$/tco2 stored-- incentive on CO2 captured dependent on technology" +/ +$offlisting +$ondelim +$include inputs_case%ds%co2_capture_incentive.csv +$offdelim +$onlisting +/, + + h2_ptc(i,v,r,allt) "--2004$/kg h2 produced -- incentive on hydrogen production by electrolyzers which purchase Energy Attribute Credits" + h2_ptc_in(i,v,allt) "--2004$/kg h2 produced -- incentive on hydrogen production by electrolyzers which purchase Energy Attribute Credits, this parameter is used to build h2_ptc and is produced in reeds/inputs/calc_financial_inputs.py" +/ +$offlisting +$ondelim +$include inputs_case%ds%h2_ptc.csv +$offdelim +$onlisting +/, + + ptc_value_scaled(i,v,allt) "--$/MWh-- value of the PTC incorporating adjustments for monetization costs, tax grossup benefits, and the difference between ptc duration and reeds evaluation period" +/ +$offlisting +$ondelim +$include inputs_case%ds%ptc_value_scaled.csv +$offdelim +$onlisting +/, + pvf_onm_undisc(t) "--unitless-- undiscounted present value factor of operations and maintenance costs" +; + +ptc_value_scaled(i,v,t)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), ptc_value_scaled(ii,v,t) } ; + +parameter firstyear_v(i,v) "flag for first year that a new new vintage can be built" ; +parameter lastyear_v(i,v) "flag for the last year that a new new vintage can be built" ; + +firstyear_v(i,v) = sum{t$[yeart(t)=smin(tt$ivt(i,v,tt),yeart(tt))], yeart(t) } ; +lastyear_v(i,v) = sum{t$[yeart(t)=smax(tt$ivt(i,v,tt),yeart(tt))], yeart(t) } ; + +* pvf_onm_undisc is based on intertemporal pvf_onm and pvf_capital, +* and is used for bulk system cost outputs +pvf_onm_undisc(t)$pvf_capital(t) = pvf_onm(t) / pvf_capital(t) ; + +*========================================== +* --- Technology start years --- +*========================================== + +* Note that some techs have a dummy firstyear of 2500 +parameter firstyear(i) "first year where new investment is allowed" +/ +$offlisting +$ondelim +$include inputs_case%ds%firstyear.csv +$offdelim +$onlisting +/ ; + +*---Add first year that capacity can be built: +firstyear(i)$[(firstyear(i) < firstyear_min)$firstyear(i)] = firstyear_min ; + +scalar co2_detail_startyr "--year-- Year to start the detailed representation of CO2 capture/storage" ; +co2_detail_startyr = smin{i$[ccs(i)$firstyear(i)], firstyear(i) } ; + +*========================================== + +scalar model_builds_start_yr "--integer-- Start year allowing new generators to be built" ; + +*Ignore gas units because gas-ct's are allowed in historical years +model_builds_start_yr = smin{i$[(not gas_ct(i))$(not distpv(i))$(not upgrade(i))$(not ban(i))$firstyear(i)], firstyear(i) } ; + +firstyear(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), firstyear(ii) } ; + +firstyear(i)$[not firstyear(i)] = model_builds_start_yr ; +firstyear(i)$[i_water_cooling(i)$(not Sw_WaterMain)] = NO ; +firstyear(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), firstyear(ii) } ; + +parameter firstyear_pcat(pcat) ; +firstyear_pcat(pcat)$[sum{i$[sameas(i,pcat)$(not ban(i))], firstyear(i) }] = sum{i$sameas(i,pcat), firstyear(i) } ; +firstyear_pcat("upv") = firstyear("upv_1") ; +firstyear_pcat("wind-ons") = firstyear("wind-ons_1") ; +firstyear_pcat("wind-ofs") = firstyear("wind-ofs_1") ; +firstyear_pcat("csp-ws") = firstyear("csp2_1") ; +firstyear_pcat("geohydro_allkm") = firstyear("geohydro_allkm_1") ; +firstyear_pcat("egs_allkm") = firstyear("egs_allkm_1") ; + + +*============================== +* Region specification +*============================== + +*set the state feasibility set +*determined by which regions are feasible +stfeas(st)$[sum{r$r_st(r,st), 1 }] = yes ; + + +*========================== +* -- existing capacity -- +*========================== + +*Begin loading of capacity data +parameter poi_cap_init(r) "--MW-- initial (pre-2010) capacity of all types" +/ +$offlisting +$ondelim +$include inputs_case%ds%poi_cap_init.csv +$offdelim +$onlisting +/ ; + +*created by /reeds/inputs/writecapdat.py +table capnonrsc(i,r,*) "--MW-- raw power capacity data for non-RSC tech created by .\reeds/inputs\writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%capnonrsc.csv +$offdelim +$onlisting +; + +*created by /reeds/inputs/writecapdat.py +$onempty +table capnonrsc_energy(i,r,*) "--MWh-- raw energy capacity data for battery tech created by .\reeds/inputs\writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%capnonrsc_energy.csv +$offdelim +$onlisting +; +$offempty + +*created by /reeds/inputs/writecapdat.py +$onempty +table caprsc(pcat,r,*) "--MW-- raw RSC capacity data, created by .\writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%caprsc.csv +$offdelim +$onlisting +; +$offempty + +*created by /reeds/inputs/writecapdat.py +* declared over allt to allow for external data files that extend beyond end_year +$onempty +table prescribednonrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for non-RSC tech created by writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%prescribed_nonRSC.csv +$offdelim +$onlisting +; +$offempty + +$onempty +table prescribednonrsc_energy(allt,pcat,r,*) "--MWh-- raw prescribed energy capacity data for non-RSC tech created by writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%prescribed_nonRSC_energy.csv +$offdelim +$onlisting +; +$offempty + +*Created using reeds/inputs\writecapdat.py +$onempty +table prescribedrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for RSC tech created by .\reeds/inputs\writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%prescribed_rsc.csv +$offdelim +$onlisting +; +$offempty + +$onempty +*For onshore and offshore wind, use outputs of hourlize to override what is in prescribedrsc +table prescribed_wind_ons(r,allt,*) "--MW-- prescribed wind capacity, created by hourlize" +$offlisting +$ondelim +$include inputs_case%ds%prescribed_builds_wind-ons.csv +$offdelim +$onlisting +; +$offempty + +prescribedrsc(allt,"wind-ons",r,"value") = prescribed_wind_ons(r,allt,"capacity") ; + +$onempty +table prescribed_wind_ofs(r,allt,*) "--MW-- prescribed wind capacity, created by hourlize" +$offlisting +$ondelim +$include inputs_case%ds%prescribed_builds_wind-ofs.csv +$offdelim +$onlisting +; +$offempty + +prescribedrsc(allt,"wind-ofs",r,"value") = prescribed_wind_ofs(r,allt,"capacity") ; + +*created by /reeds/inputs/writecapdat.py +*following does not include wind +*Retirements for techs binned by heatrates are handled in hintage_data.csv +$onempty +table prescribedretirements(allt,r,i,*) "--MW-- raw prescribed power capacity retirement data for non-RSC, non-heatrate binned tech created by /reeds/inputs/writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%retirements.csv +$offdelim +$onlisting +; +$offempty + +*created by /reeds/inputs/writecapdat.py +*Retirements for techs binned by heatrates are handled in hintage_data.csv +$onempty +table prescribedretirements_energy(allt,r,i,*) "--MWh-- raw prescribed energy capacity retirement data for battery tech created by /reeds/inputs/writecapdat.py" +$offlisting +$ondelim +$include inputs_case%ds%retirements_energy.csv +$offdelim +$onlisting +; +$offempty + +$onempty +parameter forced_retirements(i,st) "--integer-- year in which to force retirements of certain techs by state" +/ +$offlisting +$ondelim +$include inputs_case%ds%forced_retirements.csv +$offdelim +$onlisting +/ ; +$offempty + +set forced_retire(i,r,t) ; + +forced_retire(i,r,t)$[sum{st$r_st(r,st), (yeart(t)>=forced_retirements(i,st))$forced_retirements(i,st) }] = yes ; +* If the technology you would upgrade to is part of forced_retire, then include the +* upgrade tech in forced_retire +forced_retire(i,r,t)$[upgrade(i)$(sum{ii$upgrade_to(i,ii), forced_retire(ii,r,t) })] = yes ; + +set hintage_char "characteristics available in hintage_data" +/ +$offlisting +$include inputs_case%ds%hintage_char.csv +$onlisting +/ ; + +*created by /reeds/inputs/writehintage.py +table hintage_data(i,v,r,allt,hintage_char) "table of existing unit characteristics written by writehintage.py" +$offlisting +$ondelim +$include inputs_case%ds%hintage_data.csv +$offdelim +$onlisting +; + +* if not updating heat rate on upgrades, change to the default value +if((not Sw_UpgradeHeatRateAdj), + hintage_data(i,initv,r,t,"wCCS_Retro_HR")$hintage_data(i,initv,r,t,"wCCS_Retro_HR") + = hintage_data(i,initv,r,t,"wHR") ; +) ; + +set upgrade_hintage_char(hintage_char) "sets to operate over in extension of hintage_data characteristics when sw_upgrades = 1" +/ +$offlisting +$ondelim +$include inputs_case%ds%upgrade_hintage_char.csv +$offdelim +$onlisting +/ ; + +* need to extend characteristics for years where a tech could still exist if it was upgraded in a previous year +* - ie a hintages characteristics would need to persist if it is upgraded and has a lifetime extension +if(Sw_Upgrades = 1, +* need to loop over the model years as we set values to the previous modeled year that will +* also need to be updated given the check for whether data exists in current year or not + loop(tt$tmodel_new(tt), +* if there is still capacity for upgradeable units in Sw_UpgradeYear +* make sure to extend their characteristics out beyond Sw_UpgradeYear + hintage_data(i,v,r,tt,upgrade_hintage_char)$[sum{ii,upgrade_from(ii,i) } + $sum{ttt$[ttt.val = Sw_UpgradeYear],hintage_data(i,v,r,ttt,"cap") } + $(not hintage_data(i,v,r,tt,upgrade_hintage_char))] + +* set to the previous modeled year relative to the looped year + = sum{ttt$tprev(tt,ttt), hintage_data(i,v,r,ttt,upgrade_hintage_char) } ; + ) ; +) ; + +*created by /reeds/inputs/writecapdat.py +parameter binned_capacity(i,v,r,allt) "existing capacity (that is not rsc, but including distpv) binned by heat rates" ; + +binned_capacity(i,v,r,allt) = hintage_data(i,v,r,allt,"cap") ; + +parameter maxage(i) "--years-- maximum age for technologies" +/ +$offlisting +$ondelim +$include inputs_case%ds%maxage.csv +$offdelim +$onlisting +/ ; +* generators not included in maxage.csv get maxage=100 years +maxage(i)$[not maxage(i)] = maxage_default ; +* upgrades and cooling-water techs inherit maxage from the base tech +maxage(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), maxage(ii) } ; +maxage(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), maxage(ii) } ; + +*loading in capacity mandates here to avoid conflicts in calculation of valcap +* declared over allt to allow for external data files that extend beyond end_year +$onempty +parameter batterymandate(st,allt) "--MW-- cumulative battery mandate levels" +/ +$offlisting +$ondelim +$include inputs_case%ds%storage_mandates.csv +$offdelim +$onlisting +/ ; +$offempty + +scalar firstyear_battery "--year-- the first year battery technologies can be built, used to enforce storage mandate" ; +firstyear_battery = smin(i$battery(i),firstyear(i)) ; + +$onempty +table offshore_cap_req(st,allt) "--MW-- offshore wind capacity requirement by state" +$offlisting +$ondelim +$include inputs_case%ds%offshore_req.csv +$offdelim +$onlisting +; +$offempty + +parameter r_offshore(r,t) "regions where offshore wind is required by a mandate" ; +r_offshore(r,t)$[sum{st$r_st(r,st), offshore_cap_req(st,t) }] = 1 ; + +* initial smr capacity to ensure that exogenous H2 demand can be supplied, csv is written by writecapdat.py +$onempty +parameter h2_existing_smr_cap(r,t) "--MW-- capacity of existing SMR - used for meeting H2 demand before new H2 producing tech deployment is allowed to begin" +/ +$offlisting +$ondelim +$include inputs_case%ds%h2_existing_smr_cap.csv +$offdelim +$onlisting +/ ; +$offempty + +*========================================== +* --- Canadian Imports/Exports --- +*========================================== + +$ifthene.Canada %GSw_Canada% == 1 +* declared over allt to allow for external data files that extend beyond end_year +$onempty +table can_imports(r,allt) "--MWh-- [Sw_Canada=1] Imports from Canada by year" +$offlisting +$ondelim +$include inputs_case%ds%can_imports.csv +$offdelim +$onlisting +; + +parameter can_imports_capacity(r,allt) "--MW-- [Sw_Canada=1] Peak Canadian import capacity" +/ +$offlisting +$ondelim +$include inputs_case%ds%can_imports_capacity.csv +$offdelim +$onlisting +/ ; + +table can_exports(r,allt) "--MWh-- [Sw_Canada=1] Exports to Canada by year" +$offlisting +$ondelim +$include inputs_case%ds%can_exports.csv +$offdelim +$onlisting +; +$offempty + +$endif.Canada + + + +*============================= +* Resource supply curve setup +*============================= + +* Written by writesupplycurves.py +parameter rsc_dat(i,r,sc_cat,rscbin) "--units vary-- resource supply curve data for renewables with capacity in MW and costs in $/MW (MW-DC and $/MW-AC for UPV)" +/ +$offlisting +$ondelim +$include inputs_case%ds%rsc_combined.csv +$offdelim +$onlisting +/ ; + + +* Written by writesupplycurves.py +$onempty +parameter geo_discovery_factor(i,r) "--fraction-- factor representing undiscovered geothermal" +/ +$offlisting +$ondelim +$include inputs_case%ds%geo_discovery_factor.csv +$offdelim +$onlisting +/ ; +$offempty + +* Written by writesupplycurves.py +$onempty +parameter geo_discovery_rate(allt) "--fraction-- fraction of undiscovered geothermal that has been 'discovered'" +/ +$offlisting +$ondelim +$include inputs_case%ds%geo_discovery_rate.csv +$offdelim +$onlisting +/ ; +$offempty + +parameter geo_discovery(i,r,allt) "--fraction-- fraction of undiscovered geothermal that has been 'discovered'" ; +geo_discovery(i,r,t)$geo_hydro(i) = (1 - geo_discovery_factor(i,r)) * geo_discovery_rate(t) + geo_discovery_factor(i,r) ; + +* read data defining increase in hydropower upgrade availability over time. should only exist for hydUD and hydUND +$onempty +table hyd_add_upg_cap(r,i,rscbin,allt) "--MW-- cumulative increase in available upgrade capacity relative to base year" +$offlisting +$ondelim +$include inputs_case%ds%hyd_add_upg_cap.csv +$offdelim +$onlisting +; +$offempty + +parameter distance_spur(i,r,rscbin) "--miles-- Spur line distance" +/ +$offlisting +$ondelim +$include inputs_case%ds%distance_spur.csv +$offdelim +$onlisting +/ ; + +parameter distance_reinforcement(i,r,rscbin) "--miles-- Network reinforcement distance" +/ +$offlisting +$ondelim +$include inputs_case%ds%distance_reinforcement.csv +$offdelim +$onlisting +/ ; + +**rsc_dat adjustments (see additional adjustments to m_rsc_dat further below) + +*need to adjust units for pumped hydro costs from $ / KW to $ / MW +rsc_dat("pumped-hydro",r,"cost",rscbin) = rsc_dat("pumped-hydro",r,"cost",rscbin) * 1000 ; + +*need to adjust units for hydro costs from $ / KW to $ / MW +rsc_dat(i,r,"cost",rscbin)$hydro(i) = rsc_dat(i,r,"cost",rscbin) * 1000 ; + +*To allow pumped-hydro-flex via rscfeas and m_rscfeas, we set its supply curve capacity equal to pumped-hydro fixed. +*Note however that they will share the same supply curve capacity (see rsc_agg). +rsc_dat("pumped-hydro-flex",r,"cap",rscbin) = rsc_dat("pumped-hydro",r,"cap",rscbin) ; + +*Make pumped-hydro-flex more expensive than fixed pumped-hydro by a fixed percent +rsc_dat("pumped-hydro-flex",r,"cost",rscbin) = rsc_dat("pumped-hydro",r,"cost",rscbin) * %GSw_HydroVarPumpCostRatio% ; + +$ontext +Replicate the UPV supply curve data for hybrid PV+battery +"rsc_data" for hybrid PV+battery is never used in the resource constraint (see note above about rsc_dat and tg_rsc_upvagg). +This copy is necessary to ensure the conditionals for the supply curve investment variables get created for pvb. +Example: "m_rscfeas(r,i,rscbin)" is created for "eq_rsc_inv_account" +$offtext + +rsc_dat(i,r,sc_cat,rscbin)$pvb(i) = sum{ii$[upv(ii)$rsc_agg(ii,i)], rsc_dat(ii,r,sc_cat,rscbin) } ; + +*following set indicates which combinations of r and i are possible +*this is based on whether or not the bin has capacity available +rscfeas(i,r,rscbin)$rsc_dat(i,r,"cap",rscbin) = yes ; + +rscfeas(i,r,rscbin)$[csp2(i)$sum{ii$[csp1(ii)$csp2(i)$tg_rsc_cspagg(ii,i)], rscfeas(ii,r,rscbin) }] = yes ; +rscfeas(i,r,rscbin)$[csp3(i)$sum{ii$[csp1(ii)$csp3(i)$tg_rsc_cspagg(ii,i)], rscfeas(ii,r,rscbin) }] = yes ; +rscfeas(i,r,rscbin)$[csp4(i)$sum{ii$[csp1(ii)$csp4(i)$tg_rsc_cspagg(ii,i)], rscfeas(ii,r,rscbin) }] = yes ; + +*expand feasibility and supply curve data for water-enumerated PSH techs +*m_rsc_con will still require all PSH types to use the same resource base +rscfeas(i,r,rscbin)$[psh(i)$Sw_WaterMain$sum{ii$ctt_i_ii(i,ii), rsc_dat(ii,r,"cap",rscbin) }] = yes ; + +rscfeas(i,r,rscbin)$ban(i) = no ; + + +* This flag will deactivate eq_rsc_INVLIM when the RHS is < 1e-6 and set INV_RSC +* to zero for the r,i,rscbin combination. Because INV_RSC is a positive variable +* and RHS < 1e-6, INV_RSC would have to be < 1e-6 (which is basically zero). +set flag_eq_rsc_INVlim(r,i,rscbin,t) "flag for when there are small numbers in the RHS of eq_rsc_INVlim" ; +parameter rhs_eq_rsc_INVlim(r,i,rscbin,t) "RHS value of eq_rsc_INVlim" ; + +*Initialize values to 'no' +flag_eq_rsc_INVlim(r,i,rscbin,t) = no ; + +parameter binned_heatrates(i,v,r,allt) "--MMBtu / MWh-- existing capacity binned by heat rates" ; +binned_heatrates(i,v,r,allt) = hintage_data(i,v,r,allt,"wHR") ; + + +*Created by hourlize +*declared over allt to allow for external data files that extend beyond end_year +* Written by writesupplycurves.py +$onempty +parameter exog_wind_ons_rsc(i,r,rscbin,allt) "exogenous (pre-tfirst) wind capacity binned by capacity factor and rscbin" +/ +$offlisting +$ondelim +$include inputs_case%ds%exog_wind_ons_rsc.csv +$offdelim +$onlisting +/ ; +$offempty + +parameter exog_wind_ons(i,r,allt) "exogenous (pre-tfirst) wind capacity binned by capacity factor" ; +exog_wind_ons(i,r,t) = sum{rscbin, exog_wind_ons_rsc(i,r,rscbin,t) } ; + +*Created by hourlize +*declared over allt to allow for external data files that extend beyond end_year +* Written by writesupplycurves.py +$onempty +parameter exog_upv_rsc(i,r,rscbin,allt) "exogenous (pre-tfirst) upv capacity binned by capacity factor and rscbin" +/ +$offlisting +$ondelim +$include inputs_case%ds%exog_upv_rsc.csv +$offdelim +$onlisting +/ ; +$offempty + +parameter exog_upv(i,r,allt) "exogenous (pre-tfirst) upv capacity binned by capacity factor" ; +exog_upv(i,r,t) = sum{rscbin, exog_upv_rsc(i,r,rscbin,t) } ; + +parameter avail_retire_exog_rsc(i,v,r,t) "--MW-- available retired capacity for refurbishments" ; +avail_retire_exog_rsc(i,v,r,t) = 0 ; + +* declared over allt to allow for external data files that extend beyond end_year +$onempty +parameter capacity_exog(i,v,r,allt) "--MW-- exogenously specified capacity", + capacity_exog_energy(i,v,r,allt) "--MWh-- exogenously specified energy capacity", + capacity_exog_rsc(i,v,r,rscbin,allt) "--MW-- exogenous (pre-tfirst) capacity for wind-ons and upv", + m_capacity_exog(i,v,r,allt) "--MW-- exogenous power capacity used in the model", + m_capacity_exog_energy(i,v,r,allt) "--MWh-- exogenous energy capacity used in the model", + geo_cap_exog(i,r) "--MW-- existing geothermal capacity" +/ +$offlisting +$ondelim +$include inputs_case%ds%geoexist.csv +$offdelim +$onlisting +/ ; +$offempty + +set exog_rsc(i) "RSC techs whose exogenous (pre-tfirst) capacity is tracked by rscbin" ; +exog_rsc(i)$(onswind(i)) = yes ; +exog_rsc(i)$(upv(i)) = yes ; + +*Created by hourlize +*declared over allt to allow for external data files that extend beyond end_year +* Written by writesupplycurves.py +$onempty +parameter exog_geohydro_allkm_rsc(i,r,rscbin,allt) "exogenous (pre-tfirst) geohydro_allkm capacity binned by temperature and rscbin" +/ +$offlisting +$ondelim +$ifthene.readgeohydrorevexog ((%GSw_Geothermal%<>0)and(sameas(%geohydrosupplycurve%,reV))) +$include inputs_case%ds%exog_geohydro_allkm_rsc.csv +$endif.readgeohydrorevexog +$offdelim +$onlisting +/ ; +$offempty + +*reset all geothermal exogenous capacity levels +capacity_exog(i,v,r,t)$geo(i) = 0 ; + +$ifthen.geohydrorevexog %geohydrosupplycurve% == 'reV' +parameter exog_geohydro_allkm(i,r,allt) "exogenous (pre-tfirst) geohydro_allkm capacity binned by temperature" ; +exog_geohydro_allkm(i,r,t) = sum{rscbin, exog_geohydro_allkm_rsc(i,r,rscbin,t) } ; +exog_rsc(i)$(geo_hydro(i)) = yes ; +capacity_exog(i,"init-1",r,t)$geo_hydro(i) = exog_geohydro_allkm(i,r,t) ; +capacity_exog_rsc(i,"init-1",r,rscbin,t)$geo_hydro(i) = exog_geohydro_allkm_rsc(i,r,rscbin,t) ; +$else.geohydrorevexog +capacity_exog(i,"init-1",r,t)$geo_hydro(i) = geo_cap_exog(i,r) ; +$endif.geohydrorevexog + +capacity_exog(i,"init-1",r,t)$geo_egs(i) = geo_cap_exog(i,r) ; + +* existing capacity equals all 2010 capacity less retirements +* here we use the max of zero or that number to avoid any errors +* with variables that are gte to zero +* also have expiration of capital if t - tfirst is greater than the maximum age +* note the first conditional limits this calculation to units that +* do NOT have their capacity binned by heat rates (this include distpv for reasons explained below) +capacity_exog(i,"init-1",r,t)${[yeart(t)-sum{tt$tfirst(tt),yeart(tt) } capacity_exog(i,v,r,t))] = + capacity_exog(i,v,r,t-1) - capacity_exog(i,v,r,t) ; + +avail_retire_exog_rsc(i,v,r,t)$[not initv(v)] = 0 ; + +m_capacity_exog(i,v,r,t)$capacity_exog(i,v,r,t) = capacity_exog(i,v,r,t) ; +m_capacity_exog_energy(i,v,r,t)$capacity_exog_energy(i,v,r,t) = capacity_exog_energy(i,v,r,t) ; +m_capacity_exog(i,"init-1",r,t)$geo(i) = geo_cap_exog(i,r) ; + +* We assign the ~1.3 GW of exising csp-ns to upv throughout the model, but then +* convert 1.3 GW of upv back to csp-ns in the output processing. +$onempty +parameter cap_cspns(r,allt) "--MW-- csp-ns capacity" +/ +$offlisting +$ondelim +$include inputs_case%ds%cap_cspns.csv +$offdelim +$onlisting +/ ; +$offempty + + +* with regional h2 demands, we assume capacity follows demand and thus load in +* national demand values, the shares of national demand by each BA +* we then convert those to MW of capacity using the conversion of tons / mw +* +parameter h2_exogenous_demand(p,allt) "--metric tons/yr-- exogenous demand for hydrogen" +/ +$offlisting +$ondelim +$include inputs_case%ds%h2_exogenous_demand.csv +$offdelim +$onlisting +/ ; +* h2_exogenous_demand.csv is in million tons so convert to tons +h2_exogenous_demand(p,t) = 1e6 * h2_exogenous_demand(p,t) ; + +scalar h2_demand_start "--year-- first year that h2 demand should be modeled" + h2_gen_firstyear "--year-- first year that h2 generation technologies are available" +; + +* Identify the first year that hydrogen generation technologies are allowed +h2_gen_firstyear = smin{i$[h2_combustion(i)$(not ban(i))], firstyear(i) } ; + +* Set h2_demand_start to the first year that there is data +* in h2_exogenous_demand +h2_demand_start = smin{t$[sum{p, h2_exogenous_demand(p,t)}], yeart(t) } ; + +* If h2_gen_firstyear is smaller than h2_demand_start, set h2_demand_start +* to be h2_gen_firstyear +h2_demand_start$[h2_gen_firstyear=yeart(tt)], prescribednonrsc(tt,pcat,r,"value") } ; + + +m_required_prescriptions(pcat,r,t)$[tmodel_new(t) + $(sum{tt$[yeart(t)>=yeart(tt)], prescribedrsc(tt,pcat,r,"value") } + or caprsc(pcat,r,"value"))] + = sum{(tt)$[(yeart(t) >= yeart(tt))], prescribedrsc(tt,pcat,r,"value") } + + caprsc(pcat,r,"value") +; + +m_required_prescriptions_energy(pcat,r,t)$tmodel_new(t) + = sum{tt$[yeart(t)>=yeart(tt)], prescribednonrsc_energy(tt,pcat,r,"value") } ; + +parameter degrade(i,t,tt) "degradation factor by i" + degrade_pcat(pcat,t,tt) "degradation factor by pcat" ; + +parameter degrade_annual(i) "annual degredation rate" +/ +$offlisting +$ondelim +$include inputs_case%ds%degradation_annual.csv +$offdelim +$onlisting +/ ; + +* Hybrid degradation is initially defined as the battery degradation for calculating the ITC for the hybrid battery (degradation_annual_default.csv). +* Here, reassign hybrid PV+Battery to have the same value as UPV. +* Currently the degradation for the battery is zero, but if becomes non-zero, then two separate degradation factors should be defined +* (e.g., degrade_pvb_p, degrade_pvb_b) to allow for degradation to be applied to both the PV and battery. +degrade_annual(i)$pvb(i) = sum{ii$[upv(ii)$rsc_agg(ii,i)], degrade_annual(ii) } ; + +degrade_annual(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), degrade_annual(ii) } ; + +degrade(i,t,tt)$[(yeart(tt)>=yeart(t))$(not ban(i))] = 1 ; +degrade(i,t,tt)$[(yeart(tt)>=yeart(t))$(not ban(i))] = (1-degrade_annual(i))**(yeart(tt)-yeart(t)) ; + +set prescription_check(i,v,r,t) "check to see if prescriptive capacity comes online in a given year" ; + +parameter noncumulative_prescriptions(pcat,r,t) "--MW-- prescribed capacity that comes online in a given year" ; +* need to fill in for unmodeled, gap years via tprev but +* tprev is not defined with tprev(t,tfirst) +noncumulative_prescriptions(pcat,r,t)$tmodel_new(t) + = sum{tt$[(yeart(tt)<=yeart(t) +* this condition populates values of tt which exist between the +* previous modeled year and the current year + $(yeart(tt)>sum{ttt$tprev(t,ttt), yeart(ttt) })) + ], + prescribednonrsc(tt,pcat,r,"value") + prescribedrsc(tt,pcat,r,"value") + } ; + +parameter noncumulative_prescriptions_energy(pcat,r,t) "--MWh-- prescribed energy capacity that comes online in a given year" ; +noncumulative_prescriptions_energy(pcat,r,t)$tmodel_new(t) + = sum{tt$[(yeart(tt)<=yeart(t) + $(yeart(tt)>sum{ttt$tprev(t,ttt), yeart(ttt) })) + ], + prescribednonrsc_energy(tt,pcat,r,"value") + } ; + +prescription_check(i,newv,r,t)$[sum{pcat$prescriptivelink(pcat,i), noncumulative_prescriptions(pcat,r,t) } + $ivt(i,newv,t)$tmodel_new(t)$(not ban(i))] = yes ; + +*Extend feasibility for prescribed rsc capacity where there is no supply curve data. +*Resource will be manualy added to supply curve in bin1 in these cases. +*Only enable for bin1 if there is no resource in any bins to keep parameter size down. +m_rscfeas(r,i,"bin1")$[sum{(pcat,t)$[sameas(pcat,i)$tmodel_new(t)], noncumulative_prescriptions(pcat,r,t) }$rsc_i(i)$(not bannew(i))$(sum{rscbin, rsc_dat(i,r,"cap",rscbin) }=0)] = yes ; + +*========================================================== +*--- Interconnection queues (Capacity deployment limit) --- +*========================================================== +alias(tg,tgg) ; + +$onempty +table cap_limit(tg,r,allt) "--MW-- capacity deployment limit by region and technology based on interconnection queues" +$offlisting +$ondelim +$include inputs_case%ds%cap_limit.csv +$offdelim +$onlisting +; +$offempty + + +parameter cap_penalty(tg) "--per MW-- cost penalty for capacity deployment above cap limit" +/ +$offlisting +$ondelim +$include inputs_case%ds%cap_penalty.csv +$offdelim +$onlisting +/ ; + +*============================================= +* -- Explicit spur-line capacity (if used) -- +*============================================= + +* Indicate which technologies have spur lines handled endogenously (none by default) +set spur_techs(i) "Generators with endogenous spur lines" ; +spur_techs(i) = no ; + +* Written by writesupplycurves.py +$onempty +set x "reV resource sites" +/ +$offlisting +$include inputs_case%ds%x.csv +$onlisting +/ ; + +* Written by writesupplycurves.py +parameter spurline_cost(x) "--$/MW-- Spur-line cost for each reV site" +/ +$offlisting +$ondelim +$include inputs_case%ds%spurline_cost.csv +$offdelim +$onlisting +/ ; + +* Written by writesupplycurves.py +set spurline_sitemap(i,r,rscbin,x) "Mapping set from generators to reV sites" +/ +$offlisting +$ondelim +$include inputs_case%ds%spurline_sitemap.csv +$offdelim +$onlisting +/ ; + +* Written by writesupplycurves.py +set x_r(x,r) "Mapping set from reV sites to model regions" +/ +$offlisting +$ondelim +$include inputs_case%ds%x_r.csv +$offdelim +$onlisting +/ ; +$offempty + +* Include techs in spurline_sitemap in spur_techs (currently only wind-ons and upv) +$ifthene.spursites %GSw_SpurScen% == 1 +spur_techs(i)$(onswind(i) or upv(i)) = yes ; + +$ifthen.geohydrorev %geohydrosupplycurve% == 'reV' +spur_techs(i)$(geo_hydro(i)) = yes ; +$endif.geohydrorev + +$ifthen.egsrev %egssupplycurve% == 'reV' +spur_techs(i)$(geo_egs_allkm(i)) = yes ; +$endif.egsrev + +$endif.spursites + +* Indicate which reV sites are included in the model +set xfeas(x) "Sites to include in the model" ; +xfeas(x)$sum{r$x_r(x,r), 1} = yes ; + + +*========================================== +* -- Initialize tc_phaseout_mult -- +*========================================== +*initialize tc_phaseout_mult with full value +tc_phaseout_mult(i,v,t)$tmodel_new(t) = 1 ; +tc_phaseout_mult_t(i,t)$tmodel_new(t) = 1 ; + +*========================================== +* -- Valid Capacity and Generation Sets -- +*========================================== + +* -- valcap specification -- +* first all available techs are included +* then we remove those as specified + +* start with a blank slate +valcap(i,v,r,t) = no ; + +*existing plants are enabled if not in ban(i) +valcap(i,v,r,t)$[m_capacity_exog(i,v,r,t)$(not ban(i))$tmodel_new(t)] = yes ; + +* if a plant is still available by upgrade year +* and it is able to be upgraded - keep that plant in the valcap set +valcap(i,v,r,t)$[sum{tt$[tt.val = Sw_UpgradeYear], m_capacity_exog(i,v,r,tt) } + $(Sw_Upgrades = 1)$(t.val >= Sw_UpgradeYear) + $(not ban(i)) + $sum{ii, upgrade_from(ii,i) }$tmodel_new(t)] = yes ; + +*enable all new classes for balancing regions +*if available (via ivt) and if not an rsc tech +*and if it is not in ban or bannew +*the year also needs to be greater than the first year indicated +*for that specific class (this is the summing over tt portion) +*or it needs to be specified in prescriptivelink +valcap(i,newv,r,t)$[(not rsc_i(i))$tmodel_new(t)$(not ban(i))$(not bannew(i)) + $(sum{tt$(yeart(tt)<=yeart(t)), ivt(i,newv,tt) })$(not upgrade(i)) + ] = yes ; + +*for rsc technologies, enabled if m_rscfeas is populated +*similarly to non-rsc technologies and there is the additional +*condition that m_rscfeas must contain values in at least one rscbin +valcap(i,newv,r,t)$[rsc_i(i)$tmodel_new(t)$(not ban(i))$(not bannew(i)) + $sum{rscbin, m_rscfeas(r,i,rscbin) }$(not upgrade(i)) + $sum{tt$(yeart(tt)<=yeart(t)), ivt(i,newv,tt) } + ] = yes ; + + +*enable capacity if there is a required prescription in that region +*first for non-rsc techs +valcap(i,newv,r,t)$[(not rsc_i(i)) + $(sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,t) }) + $sum{tt$[sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,tt) } + $(yeart(tt)<=yeart(t))], ivt(i,newv,tt) } + $(not ban(i))] = yes ; + +*then for rsc techs +valcap(i,newv,r,t)$[rsc_i(i) + $(sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,t) }) + $sum{tt$[sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,tt) } + $(yeart(tt)<=yeart(t))], ivt(i,newv,tt) } + $(not ban(i)) + $sum{rscbin, m_rscfeas(r,i,rscbin) }] = yes ; + +* Techs where new investment are banned: Start by removing from valcap +valcap(i,newv,r,t)$bannew(i) = no ; +* Then add back only if they have prescribed capacity in years with the appropriate i/v/t combination +valcap(i,newv,r,t) + $[bannew(i) + $(not ban(i)) + $sum{(tt,pcat)$[ivt(i,newv,tt)$prescriptivelink(pcat,i)], + noncumulative_prescriptions(pcat,r,tt) }] + = yes ; + +*NEW capacity only valid in historical years if and only if it has required prescriptions +*logic here is that we don't want to populate the constraint with CAP <= 0 and instead +*want to simply remove the consideration for CAP altogether and make the constraint unnecessary +*note that the constraint itself is also conditioned on valcap + +*therefore remove the consideration of valcap if... +valcap(i,newv,r,t)$[ +*if there are no required prescriptions + (not sum{pcat$prescriptivelink(pcat,i), + m_required_prescriptions(pcat,r,t) } ) +*if the year is before the first year the technology is allowed + $(yeart(t)=Sw_UpgradeYear) + $(yeart(t)>=firstyear(i)) + $sum{ii$upgrade_from(i,ii), valcap(ii,initv,r,t) } + $(not ban(i)) + $(not sum{ii$upgrade_to(i,ii), ban(ii) }) + ] = yes ; + +*upgrades from new techs are included in valcap if... +* it is an upgrade tech, the switch is enabled, and past the beginning upgrade year +valcap(i,newv,r,t)$[upgrade(i)$Sw_Upgrades$(yeart(t)>=Sw_UpgradeYear) +*if the capacity that it is upgraded to is available + $sum{ii$upgrade_to(i,ii), valcap(ii,newv,r,t) } +*if the technology is not banned + $(not ban(i)) +*if the technology you upgrade to is not banned + $(not sum{ii$upgrade_to(i,ii), ban(ii) }) +*if it is past the first year that technology is available + $(yeart(t)>=firstyear(i)) +*if it is a valid ivt combination which is duplicated from upgrade_to + $sum{tt$(yeart(tt)<=yeart(t)), ivt(i,newv,tt) } + $(yeart(t)>=Sw_UpgradeYear) + ] = yes ; + +*remove any upgrade considerations if before the upgrade year +valcap(i,v,r,t)$[upgrade(i)$(yeart(t) caa_coal_retire_year) + $(sum{ii$(not forced_retire(ii,r,t)), upgrade_from(ii,i) }) ] = 0 ; + +* remove upgrade technologies that are explicitly banned +valcap(i,v,r,t)$[upgrade(i)$ban(i)] = no ; + +*Restrict valcap for nuclear in BAs that are impacted By state nuclear bans +if(Sw_NukeStateBan = 1, + valcap(i,v,r,t)$[nuclear(i)$newv(v)$nuclear_ba_ban(r)] = no ; +) ; + +$ifthene.hydEDban %GSw_hydED% == 0 +* Only leave hydED, turn off remaining hydro technologies +valcap(i,v,r,t)$[hydro(i)$(not sameas(i,"hydED"))] = no ; +$endif.hydEDban + +* Drop vintages in non-modeled future years +valcap(i,v,r,t)$[(not sum{tt$[tmodel_new(tt)], ivt(i,v,tt) })$newv(v)] = no ; + +* Remove non-offshore resources from offshore zones +valcap(i,v,r,t)$[offshore(r)$(not ofswind(i))] = no ; + +* Add aggregations of valcap +valcap_irt(i,r,t) = sum{v, valcap(i,v,r,t) } ; +valcap_iv(i,v)$sum{(r,t)$tmodel_new(t), valcap(i,v,r,t) } = yes ; +valcap_ir(i,r)$sum{(v,t)$tmodel_new(t), valcap(i,v,r,t) } = yes ; +valcap_i(i)$sum{v, valcap_iv(i,v) } = yes ; +valcap_ivr(i,v,r)$sum{t, valcap(i,v,r,t) } = yes ; + +* -- valinv specification -- +valinv(i,v,r,t) = no ; +valinv(i,v,r,t)$[valcap(i,v,r,t)$ivt(i,v,t)] = yes ; + +* Do not allow investments in regions where that technology is banned, expect for prescribed builds +valinv(i,v,r,t)$[tech_banned(i,r)$(not sum{pcat$prescriptivelink(pcat,i), noncumulative_prescriptions(pcat,r,t) })] = no ; + +*remove non-prescribed numeraire technologies that remain in valcap +valinv(i,newv,r,t)$[i_numeraire(i)$Sw_WaterMain$(not sum{pcat$prescriptivelink(pcat,i), noncumulative_prescriptions(pcat,r,t) })] = no ; + +*upgrades are not allowed for the INV variable as they are the sum of UPGRADES +valinv(i,v,r,t)$upgrade(i) = no ; + +valinv(i,v,r,t)$[(yeart(t)=h2_ptc_firstyear) +* if the generating tech itself is available + $valcap(i,v,r,t) +* if the technology has low enough of emissions to comply with the policy + $i_h2_ptc_gen(i) + ] = yes ; + +* -- valgen_h2ptc specification -- +* generators that can receive the hydrogen production tax credit are available based on valcap_h2ptc +valgen_h2ptc(i,v,r,t)$valcap_h2ptc(i,v,r,t) = yes ; + + +* -- m_refurb_cond specification -- + +* technologies can be refurbished if... +* they are part of refurbtech +* the number of years from tt to t are beyond the expiration of the tech (via maxage) +* it is valid capacity in t, the current solve year. +* it was a valid investment in year tt, the initial investment year. +m_refurb_cond(i,newv,r,t,tt)$[refurbtech(i) + $(yeart(tt) maxage(i)) + $valcap(i,newv,r,t)$valinv(i,newv,r,tt) + ] = yes ; + + +* -- inv_cond specification -- + +*if there is a link between the bintage and the year +*all previous years +*if the unit we invested in is not retired... +inv_cond(i,newv,r,t,tt)$[(not ban(i)) + $tmodel_new(t)$tmodel_new(tt) + $(yeart(tt) <= yeart(t)) + $valinv(i,newv,r,tt) + $(ord(t)-ord(tt) < maxage(i)) + ] = yes ; + +inv_cond(i,newv,r,t,tt)$[Sw_WaterMain$sum{ctt$bannew_ctt(ctt),i_ctt(i,ctt) }$tmodel_new(t)$tmodel_new(tt) + $sum{(pcat)$[sameas(pcat,i)], noncumulative_prescriptions(pcat,r,tt) } + $(yeart(tt) <= yeart(t)) + $valinv(i,newv,r,tt) + $(ord(t)-ord(tt) < maxage(i)) + ] = yes ; + + + +* cannot restrict by valcap here to maintain compatibility with water techs +co2_captured_incentive(i,v,r,t) = co2_captured_incentive_in(i,v,t) ; + +* expand to water techs +co2_captured_incentive(i,v,r,t)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), co2_captured_incentive(ii,v,r,t) } ; + +* expand to upgrade techs +co2_captured_incentive(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)] = + sum{ii$upgrade_to(i,ii),co2_captured_incentive(ii,v,r,t) } ; + +* incentive for captured co2 for initial plants set to the amount available +* as of upgradeyear, similar to other performance and cost characteristics +co2_captured_incentive(i,v,r,t)$[initv(v)$upgrade(i) + $valcap(i,v,r,t)$(Sw_Upgrades = 1) + $(yeart(t)>=Sw_UpgradeYear) + $(yeart(t) <= co2_capture_incentive_last_year_)] = +* note we populate the incentive for all years and then trim the incentive for later years +* when the upgrade occurs - this is after the solve statement +* we also cast this forward based on whether or not a plant was built in that year +* but remove the last year for consideration of that below via co2_capture_incentive_last_year_ + sum{(ii,vv,tt)$[upgrade_to(i,ii)$newv(vv) + $(firstyear_v(ii,vv) = Sw_UpgradeYear) + $(yeart(tt) = Sw_UpgradeYear)], + co2_captured_incentive(ii,vv,r,tt) } ; + +* plants can only receive the CO2 capture incentive for the length of the incentive 'co2_capture_incentive_length', starting in the first year of that tech, vintage combination +co2_captured_incentive(i,newv,r,t)$[(yeart(t) > firstyear_v(i,newv) + co2_capture_incentive_length)] = 0 ; +* vintages whose first year comes after 'co2_capture_incentive_last_year_' cannot receive the CO2 capture incentive because the incentive is no longer available +co2_captured_incentive(i,newv,r,t)$[(firstyear_v(i,newv) > co2_capture_incentive_last_year_)] = 0 ; + +* remove any invalid values to shrink parameter +co2_captured_incentive(i,v,r,t)$[not valcap(i,v,r,t)] = 0 ; + +* making h2_ptc for all regions +h2_ptc(i,v,r,t)$valcap(i,v,r,t) = h2_ptc_in(i,v,t) ; + +* if Sw_H2_PTC = 1, then tech 'electrolyzer' can also receive the hydrogen PTC, as designated in h2_ptc. +* Otherwise, we assume it receives $0/kg because the cleanliness of its carbon cannot be proven +h2_ptc("electrolyzer",v,r,t)$[(not Sw_H2_PTC)] = 0; + +set h2_ptc_years(t) "years in which the hydrogen production incentive is active"; +h2_ptc_years(t) = tmodel_new(t)$[sum{(i,v,r),h2_ptc(i,v,r,t)}]; + + +*========================================== +* --- Parameters for water constraints --- +*========================================== + +set sw(wst) "surface water types where access is based on consumption not withdrawal" +/ +$offlisting +$ondelim +$include inputs_case%ds%sw.csv +$offdelim +$onlisting +/ ; + +set i_water_surf(i) "subset of technologies that uses surface water", + i_w(i,w) "linking set between technology and water use type used in constraining water availability" ; + +i_water_surf(i)$[sum{(sw,ctt,ii)$i_ii_ctt_wst(i,ii,ctt,sw), 1}] = yes ; +i_w(i,"cons")$[i_water(i)$i_water_surf(i)] = yes ; +i_w(i,"with")$[i_water(i)$(not i_water_surf(i))] = yes ; + +parameter wat_supply_init(wst,r) "-- million gallons per year -- water supply allocated to initial fleet " ; + +*WatAccessAvail - water access available (Mgal/year) +*WatAccessCost - cost of water access (2004$/Mgal) +$onempty +parameter wat_supply_new(wst,*,r) "-- million gallons per year , $ per million gallons per year -- water supply curve for post-2010 capacity with *=cap,cost" +/ +$offlisting +$ondelim +$include inputs_case%ds%wat_access_cap_cost.csv +$offdelim +$onlisting +/ ; +$offempty + +parameter m_watsc_dat(wst,*,r,t) "-- million gallons per year, $ per million gallons per year -- water supply curve data with *=cap,cost" ; + +* UnappWaterSeaDistr - seasonal distribution factors for new unappropriated water access +$onempty +table watsa_temp(wst,r,quarter) "fractional quarterly allocation of water" +$offlisting +$ondelim +$include inputs_case%ds%unapp_water_sea_distr.csv +$offdelim +$onlisting +; +$offempty + +m_watsc_dat(wst,"cost",r,t)$tmodel_new(t) = wat_supply_new(wst,"cost",r) ; + +*not allowed to invest in upgrade techs since they are a product of upgrades +inv_cond(i,v,r,t,tt)$upgrade(i) = no ; + + +*===================================== +* --- Regional Carbon Constraints --- +*===================================== + +$onempty +set RGGI_States(st) "states with RGGI regulation" +/ +$offlisting +$include inputs_case%ds%rggi_states.csv +$onlisting +/ +; +$offempty + +Set RGGI_r(r) "BAs with RGGI regulation" ; + +RGGI_r(r)$[sum{st$RGGI_States(st),r_st(r,st) }] = yes ; + +* declared over allt to allow for external data files that extend beyond end_year +parameter RGGI_cap(allt) "--metric tons-- CO2 emissions cap for RGGI states" +/ +$offlisting +$ondelim +$include inputs_case%ds%rggicon.csv +$offdelim +$onlisting +/ ; + +* These values are based on 42 MMT trajectory from section 8.1 of the CPUC "Inputs & Assumptions: +* "2019-2020 Integrated Resource Planning." This document can be found at +* ftp://ftp.cpuc.ca.gov/energy/modeling/Inputs%20%20Assumptions%202019-2020%20CPUC%20IRP%202020-02-27.pdf +$onempty +parameter state_cap(st,allt) "--metric tons-- CO2 emissions cap for state cap and trade policies" +/ +$offlisting +$ondelim +$include inputs_case%ds%state_cap.csv +$offdelim +$onlisting +/ ; +$offempty + + +*========================== +* -- Climate heuristics -- +*========================== +parameter climate_heuristics_yearfrac(allt) "--fraction-- annual scaling factor for climate heuristics" +$onempty +/ +$offlisting +$ondelim +$include inputs_case%ds%climate_heuristics_yearfrac.csv +$offdelim +$onlisting +/ ; +$offempty + +set climate_param "parameters defined in climate_heuristics_finalyear" +/ +$offlisting +$include inputs_case%ds%climate_param.csv +$onlisting +/ ; + +parameter climate_heuristics_finalyear(climate_param) "--fraction-- climate heuristic adjustment in final year" +$onempty +/ +$offlisting +$ondelim +$include inputs_case%ds%climate_heuristics_finalyear.csv +$offdelim +$onlisting +/ ; +$offempty + +* hydro_capcredit_delta applies to dispatchable hydro. +* We don't apply it through cap_hyd_szn_adj because we only want to change cap credit, not energy dispatch. +parameter hydro_capcredit_delta(i,allt) "--fraction-- fractional adjustment to dispatchable hydro capacity credit from climate heuristics" ; +hydro_capcredit_delta(i,t)$hydro_d(i) = + climate_heuristics_finalyear('hydro_capcredit_delta') * climate_heuristics_yearfrac(t) +; + +*==================================== +* --- RPS data --- +*==================================== + +set RPSCat "RPS constraint categories, including clean energy standards" +/ +$offlisting +$include inputs_case%ds%RPSCat.csv +$onlisting +/ ; + +set RPSCat_i(RPSCat,i,st) "mapping between rps category and technologies for each state", + RecMap(i,RPSCat,st,ast,t) "Mapping set for technologies to RPS categories and indicates if credits can be sent from st to ast", + RecStates(RPSCat,st,t) "states that can generate RECS for their own or other states' requirements", + RecTrade(RPSCat,st,ast,t) "mapping set between states that can trade RECs with each other (from st to ast)", + RecTech(RPSCat,i,st,t) "set to indicate which technologies and classes can contribute to a state's RPSCat", + r_st_rps(r,st) "mapping of eligible regions to each state for RPS/CES purposes" ; + +Parameter RecPerc(RPSCat,st,t) "--fraction-- fraction of total generation for each state that must be met by RECs for each category" + RPSTechMult(RPSCat,i,st) "--fraction-- fraction of generation from each technology that counts towards the requirement for each category" +; + +* Create a new r-to-state mapping set that allows voluntary purchases +r_st_rps(r,st) = r_st(r,st) ; +* All regions can create voluntary RECS +r_st_rps(r,"voluntary") = yes ; + +$onempty +table techs_banned_rps(i,st) "Techs that are banned for serving RPS in a given state" +$offlisting +$ondelim +$include inputs_case%ds%techs_banned_rps.csv +$offdelim +$onlisting +; +$offempty + +$onempty +parameter RecStyle(st,RPSCat) "--integer-- Indication for how to apply state requirement (0 = end-use sales, 1 = bus-bar sales, 2 = generation). Default is 0." +/ +$offlisting +$ondelim +$include inputs_case%ds%recstyle.csv +$offdelim +$onlisting +/ +; +$offempty + +$onempty +table techs_banned_ces(i,st) "Techs that are banned for serving CES in a given state" +$offlisting +$ondelim +$include inputs_case%ds%techs_banned_ces.csv +$offdelim +$onlisting +; +$offempty + +$onempty +* declared over allt to allow for external data files that extend beyond end_year +Table rps_fraction(allt,st,RPSCat) "--fraction-- requirement for state RPS" +$offlisting +$ondelim +$include inputs_case%ds%rps_fraction.csv +$offdelim +$onlisting +; +$offempty + +$onempty +parameter ces_fraction(allt,st) "--fraction-- requirement for clean energy standard" +/ +$offlisting +$ondelim +$include inputs_case%ds%ces_fraction.csv +$offdelim +$onlisting +/ +; +$offempty + +RecPerc(RPSCat,st,t) = sum{allt$att(allt,t), rps_fraction(allt,st,RPSCat) } ; +RecPerc(RPSCat,st,t)$[(Sw_StateRPS_Carveouts = 0)$(sameas(RPSCat, "RPS_solar") or sameas(RPSCat, "RPS_Wind"))] = 0; +RecPerc("CES",st,t) = ces_fraction(t,st) ; + +* RE generation creates both CES and RPS credits, which can cause double-counting +* if a state has an RPS but not a CES. By setting each state's CES as the maximum +* of its RPS or CES, we prevent the double-counting. +RecPerc("CES",st,t) = max(RecPerc("CES",st,t), RecPerc("RPS_all",st,t)) ; + +*Some links (value in RECtable = 2) restricted to bundled trading, while +*some (value in RECtable = 1) allowed to also trade unbundled RECs. +*Note the reversed set index order for rectable as compared to RecTrade, RecMap, and RECS. +$onempty +table rectable(st,ast) "Allowed credit trade from ast to st. [1] Unbundled allowed; [2] Only bundled allowed" +$offlisting +$ondelim +$include inputs_case%ds%rectable.csv +$offdelim +$onlisting +; +$offempty + +table acp_price(st,allt) "$/REC - alternative compliance payment price for RPS constraint" +$offlisting +$ondelim +$include inputs_case%ds%acp_prices.csv +$offdelim +$onlisting +; + +$onempty +parameter acp_disallowed(st,RPSCat) "--integer-- Indication for whether ACP purchases are disallowed (1) or allowed (0)." +/ +$offlisting +$ondelim +$include inputs_case%ds%acp_disallowed.csv +$offdelim +$onlisting +/ +; +$offempty + +RecStates(RPSCat,st,t)$[RecPerc(RPSCat,st,t) or sum{ast, rectable(ast,st) }] = yes ; + +*If both states have an RPS for the RPSCat and if they're allowed to trade, they can trade +RecTrade(RPSCat,st,ast,t)$((rectable(ast,st)=1)$RecStates(RPSCat,ast,t)) = yes ; + +*If both states have an RPS for the RPSCat and if they're allowed to trade, they can trade +RecTrade("RPS_bundled",st,ast,t)$[(rectable(ast,st)=2)$RecStates("RPS_all",ast,t)] = yes ; +RecTrade("CES_bundled",st,ast,t)$[(rectable(ast,st)=2)$RecStates("CES",ast,t)] = yes ; + +*Assign eligible techs for RPS_All +RPSCat_i("RPS_All",i,st)$[re(i)$(not techs_banned_rps(i,st))$valcap_i(i)] = yes ; + +*Assign eligible techs for RPS_Wind +RPSCat_i("RPS_Wind",i,st)$[wind(i)$(not techs_banned_rps(i,st))$valcap_i(i)] = yes ; + +*Assign eligible techs for RPS_Solar +RPSCat_i("RPS_Solar",i,st)$[(pv(i) or pvb(i))$(not techs_banned_rps(i,st))$valcap_i(i)] = yes ; + +*We allow CCS techs and upgrades to be eligible for CES policies +*CCS contribution is limited based on the amount of emissions captured later on down +RPSCat_i("CES",i,st)$[(RPSCAT_i("RPS_All",i,st) or nuclear(i) or hydro(i) or ccs(i) or canada(i)) + $(not techs_banned_ces(i,st)) + $valcap_i(i)] = yes ; + +RecTech(RPSCat,i,st,t)$(RPSCat_i(RPSCat,i,st)$RecStates(RPSCat,st,t)) = yes ; +RecTech(RPSCat,i,st,t)$(hydro(i)$RecTech(RPSCat,"hydro",st,t)$valcap_i(i)) = yes ; +RecTech("RPS_Bundled",i,st,t)$[RecTech("RPS_All",i,st,t)] = yes ; + +RecTech("CES_Bundled",i,st,t)$[RecTech("CES",i,st,t)] = yes ; + +*Voluntary market RECs can come from any RE tech +RecTech(RPSCat,i,"voluntary",t)$re(i) = yes ; + +*Remove combinations that are not allowed by valgen +RecTech(RPSCat,i,st,t)$[not sum{(v,r)$r_st_rps(r,st), valgen(i,v,r,t) }] = no ; + +*Remove CCS techs if explicitly disallowed +RecTech(RPSCat,i,st,t)$[ccs(i)$Sw_CCS_NotRecTech] = no ; + +$onempty +table hydrofrac_policy(st,RPSCat) "fraction of hydro RPS or CES credits that can count towards policy targets" +$offlisting +$ondelim +$include inputs_case%ds%hydrofrac_policy.csv +$offdelim +$onlisting +; +$offempty + +*initialize values to 1 +RPSTechMult(RPSCat,i,st)$[sum{t, RecTech(RPSCat,i,st,t) }] = 1 ; +*reduce multipliers for hydro technologies based on eligibility fractions +RPSTechMult(RPSCat,i,st)$[hydro(i)$valcap_i(i)] = hydrofrac_policy(st,RPSCat) ; +RPSTechMult("RPS_bundled",i,st)$[hydro(i)$valcap_i(i)] = RPSTechMult("RPS_All",i,st) ; +RPSTechMult("CES_bundled",i,st)$[hydro(i)$valcap_i(i)] = RPSTechMult("CES",i,st) ; + +*Reduce RPS/CES values for distributed PV based on distloss because we increase their generation to the busbar level +RPSTechMult(RPSCat,i,st)$[(distpv(i))$RPSTechMult(RPSCat,i,st)] = 1 - distloss ; + +$onempty +table techs_banned_imports_rps(i,st) "Techs that are not allowed to be imported into a state to meet the RPS" +$offlisting +$ondelim +$include inputs_case%ds%techs_banned_imports_rps.csv +$offdelim +$onlisting +; +$offempty + +*CCS technologies have a variety of capture rates, so we assign them below after reading in capture rates + +RecMap(i,RPSCat,st,ast,t)$[ +*if the receiving state has a requirement for RPSCat + RecPerc(RPSCat,ast,t) +*if both states can use that technology + $RecTech(RPSCat,i,st,t) + $RecTech(RPSCat,i,ast,t) +*if the state can trade + $RecTrade(RPSCat,st,ast,t) + ] = yes ; + +RecMap(i,"RPS_bundled",st,ast,t)$( +*if the receiving state has a requirement for RPSCat + RecPerc("RPS_all",ast,t) +*if both states can use that technology + $RecTech("RPS_bundled",i,st,t) + $RecTech("RPS_bundled",i,ast,t) +*if the state can trade + $RecTrade("RPS_bundled",st,ast,t) + ) = yes ; + + +RecMap(i,"CES_bundled",st,ast,t)$( +*if the receiving state has a requirement for RPSCat + RecPerc("CES",ast,t) +*if both states can use that technology + $RecTech("CES_bundled",i,st,t) + $RecTech("CES_bundled",i,ast,t) +*if the state can trade + $RecTrade("CES_bundled",st,ast,t) + ) = yes ; + +*states can "import" their own RECs (except for "voluntary") +RecMap(i,RPSCat,st,ast,t)$[ + sameas(st,ast) + $RecTech(RPSCat,i,st,t) + $RecPerc(RPSCat,st,t) + $(not sameas(st,"voluntary")) + ] = yes ; + +*states that allow hydro to fulfill their RPS requirements can trade hydro recs +RecMap(i,RPSCat,st,ast,t)$[ + hydro(i) + $RPSTechMult(RPSCat,i,st) + $RPSTechMult(RPSCat,i,ast) + $RecMap("hydro",RPSCat,st,ast,t) + $valcap_i(i) + ] = yes ; + +*Do not allow banned imports +RecMap(i,RPSCat,st,ast,t)$[ + (sameas(RPSCat,"RPS_All") or sameas(RPSCat,"RPS_bundled")) + $(not sameas(st,ast)) + $techs_banned_imports_rps(i,ast) + ] = no ; + +*Only allow voluntary market to use renewable energy when consuming CES credits +RecMap(i,RPSCat,st,"voluntary",t)$[ + (not re(i)) + $(sameas(RPSCat,"CES") or sameas(RPSCat,"CES_bundled")) + ] = no ; + +*Do not allow voluntary market to use canadian imports +RecMap(i,RPSCat,st,"voluntary",t)$[ + (canada(i)) + ] = no ; + +if(Sw_WaterMain=1, + RecMap(i,RPSCat,st,ast,t)$[i_water_cooling(i)$(not RecMap(i,RPSCat,st,ast,t))] + = sum{ii$ctt_i_ii(i,ii), RecMap(ii,RPSCat,st,ast,t) } ; +) ; + +$onempty +parameter RPS_oosfrac(st) "fraction of RECs from out of state that can meet the RPS" +/ +$offlisting +$ondelim +$include inputs_case%ds%oosfrac.csv +$offdelim +$onlisting +/ ; +$offempty + +$onempty +table RPS_unbundled_limit_in(st,allt) "--fraction-- upper bound of state RPS that can be met with unbundled RECS" +$offlisting +$ondelim +$include inputs_case%ds%unbundled_limit_rps.csv +$offdelim +$onlisting +; +$offempty + +$onempty +table CES_unbundled_limit_in(st,allt) "--fraction-- upper bound of state CES that can be met with unbundled RECS" +$offlisting +$ondelim +$include inputs_case%ds%unbundled_limit_ces.csv +$offdelim +$onlisting +; +$offempty + +parameter REC_unbundled_limit(RPScat,st,allt) '--fraction-- portion for RPS/CES constraint that can be met with unbundled RECS' ; +set st_unbundled_limit(RPScat,st) "states that have a unbundled limit on RECs" ; + +REC_unbundled_limit("RPS_All",st,t) = RPS_unbundled_limit_in(st,t) ; +REC_unbundled_limit("CES",st,t) = CES_unbundled_limit_in(st,t) ; + +st_unbundled_limit(RPSCat,st)$sum{t, REC_unbundled_limit(RPSCat,st,t) } = yes ; + +parameter national_gen_frac(allt) "--%-- national fraction of load + losses that must be met by RE" +/ +$offlisting +$ondelim +$include inputs_case%ds%gen_mandate_trajectory.csv +$offdelim +$onlisting +/ ; + +parameter nat_gen_tech_frac(i) "--fraction-- fraction of each tech generation that may be counted toward eq_national_gen" +/ +$offlisting +$ondelim +$include inputs_case%ds%gen_mandate_tech_list.csv +$offdelim +$onlisting +/ ; +nat_gen_tech_frac(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), nat_gen_tech_frac(ii) } ; + +*==================== +* --- CSAPR Data --- +*==================== + +* a CSAPR budget indicates the cap for trading whereas +* assurance indicates the maximum amount a state can emit regardless of trading +set csapr_cat "CSAPR regulation categories" +/ +$offlisting +$include inputs_case%ds%csapr_cat.csv +$onlisting +/ ; + +*trading rules dictate there are two groups of states that can trade with each other +set csapr_group "CSAPR trading group" +/ +$offlisting +$include inputs_case%ds%csapr_group.csv +$onlisting +/ ; + +$onempty +table csapr_cap(st,csapr_cat,allt) "--metric tons-- maximum amount of NOX emissions during the ozone season (May-September)" +$offlisting +$ondelim +$include inputs_case%ds%csapr_ozone_season.csv +$offdelim +$onlisting +; +$offempty + +$onempty +set csapr_group1_ex(st) "CSAPR states that cannot trade with those in group 2" +/ +$offlisting +$include inputs_case%ds%csapr_group1_ex.csv +$onlisting +/ +; +$offempty + +$onempty +set csapr_group2_ex(st) "CSAPR states that cannot trade with those in group 1" +/ +$offlisting +$include inputs_case%ds%csapr_group2_ex.csv +$onlisting +/ +; +$offempty + +set csapr_group_st(csapr_group,st) "final crosswalk set for use in modeling CSAPR trade relationships" ; + +csapr_group_st("cg1",st)$[sum{t,csapr_cap(st,"budget",t) }$(not csapr_group1_ex(st))$stfeas(st)] = yes ; +csapr_group_st("cg2",st)$[sum{t,csapr_cap(st,"budget",t) }$(not csapr_group2_ex(st))$stfeas(st)] = yes ; + +*assumption here is that the ozone season covers only 1/3 of the months +*in the spring and fall but the entire season in summer, +*therefore weighting each seasons emissions accordingly +parameter quarter_weight_csapr(quarter) "quarter weights for CSAPR ozone season constraints" + / spri 0.333, summ 1, fall 0.333 /; + + + +*============================== +* --- Transmission Inputs --- +*============================== + +* --- transmission sets --- +set trtype "transmission capacity type" +/ +$offlisting +$include inputs_case%ds%trtype.csv +$onlisting +/ ; + +set aclike(trtype) "AC transmission capacity types" +/ +$offlisting +$ondelim +$include inputs_case%ds%aclike.csv +$offdelim +$onlisting +/ ; + +set notvsc(trtype) "transmission capacity types that are not VSC" +/ +$offlisting +$ondelim +$include inputs_case%ds%notvsc.csv +$offdelim +$onlisting +/ ; + +set lcclike(trtype) "transmission capacity types where lines are bundled with AC/DC converters" +/ +$offlisting +$ondelim +$include inputs_case%ds%lcclike.csv +$offdelim +$onlisting +/ ; + +set trancap_fut_cat "categories of near-term transmission projects that describe the likelihood of being completed" +/ +$offlisting +$include inputs_case%ds%trancap_fut_cat.csv +$onlisting +/ ; + +set routes(r,rr,trtype,t) "final conditional on transmission feasibility" + routes_inv(r,rr,trtype,t) "routes where new transmission investment is allowed" + routes_prm(r,rr) "routes where PRM trading is allowed" + opres_routes(r,rr,t) "final conditional on operating reserve flow feasibility" +; + +alias(trtype,intype,outtype) ; + +* Specify the transmission types that are limited by Sw_TransCapMax and Sw_TransCapMaxTotal +set trtypemax(trtype) "trtypes to limit" ; +trtypemax(trtype)$[(Sw_TransCapMaxTypes=0)] = no ; +trtypemax(trtype)$[(Sw_TransCapMaxTypes=1)] = yes ; +trtypemax(trtype)$[(Sw_TransCapMaxTypes=2)$sameas(trtype,'VSC')] = yes ; +trtypemax(trtype)$[(Sw_TransCapMaxTypes=3)$(not sameas(trtype,'AC'))] = yes ; + +* --- initial transmission capacity --- +* transmission capacity input data are defined in both directions for each region-to-region pair +* Written by transmission.py +$onempty +parameter trancap_init_energy(r,rr,trtype) "--MW-- initial transmission capacity for energy trading" +/ +$offlisting +$ondelim +$include inputs_case%ds%trancap_init_energy.csv +$offdelim +$onlisting +/ ; + +parameter trancap_init_prm(r,rr,trtype) "--MW-- initial transmission capacity for capacity (PRM) trading" +/ +$offlisting +$ondelim +$include inputs_case%ds%trancap_init_prm.csv +$offdelim +$onlisting +/ ; +$offempty + +* --- future transmission capacity --- +* Transmission additions are defined in one direction for each region-to-region pair with the lowest region number listed first +* Written by transmission.py +$onempty +parameter trancap_fut(r,rr,trancap_fut_cat,trtype,allt) "--MW-- potential future transmission capacity by type (one direction)" +/ +$offlisting +$ondelim +$include inputs_case%ds%trancap_fut.csv +$offdelim +$onlisting +/ ; +$offempty + +* --- exogenously specified transmission capacity --- +* Transmission additions are defined in one direction for each region-to-region pair with the lowest region number listed first +parameter invtran_exog(r,rr,trtype,t) "--MW-- exogenous transmission capacity investment (one direction)" ; +* "certain" future transmission project capacity in the current year t +invtran_exog(r,rr,trtype,t)$trancap_fut(r,rr,"certain",trtype,t) = trancap_fut(r,rr,"certain",trtype,t) ; + +* --- valid transmission routes --- + +*transmission routes are enabled if: +* (1) there is transmission capacity between the two regions +routes(r,rr,trtype,t)$[ + trancap_init_energy(r,rr,trtype) or trancap_init_energy(rr,r,trtype) + or trancap_init_prm(r,rr,trtype) or trancap_init_prm(rr,r,trtype) + or invtran_exog(r,rr,trtype,t) or invtran_exog(rr,r,trtype,t) +] = yes ; +* (2) there is future capacity available between the two regions +routes(r,rr,trtype,t)$[sum{(tt,trancap_fut_cat)$(yeart(tt)<=yeart(t)), + trancap_fut(r,rr,trancap_fut_cat,trtype,tt) }] = yes ; +* (3) there exists a route (r,rr) that is in the opposite direction as (rr,r) +routes(rr,r,trtype,t)$(routes(r,rr,trtype,t)) = yes ; +* (4) the year is modeled +routes(r,rr,trtype,t)$(not tmodel_new(t)) = no ; + +* disable AC routes that cross interconnect boundaries (only happens if aggregating regions across interconnects) +routes(r,rr,trtype,t) + $[routes(r,rr,trtype,t) + $aclike(trtype) + $[(not sum{interconnect$[r_interconnect(r,interconnect)$r_interconnect(rr,interconnect)], 1 })] + ] = no ; + +* Disable links between offshore zones if specified +routes(r,rr,trtype,t)$[(not Sw_OffshoreBackbone)$offshore(r)$offshore(rr)] = no ; + +* If any routes use VSC, activate the VSC constraints and variables +scalar Sw_VSC "Activate VSC constraints and variables" ; +Sw_VSC = sum{routes(r,rr,trtype,t)$sameas(trtype,'VSC'), 1} ; + +* initialize all investment routes to no +routes_inv(r,rr,trtype,t) = no ; +* allow new investment along existing routes +routes_inv(r,rr,trtype,t)$[notvsc(trtype)$routes(r,rr,trtype,t)] = yes ; +* Do not allow transmission expansion on most interfaces until firstyear_trans_nearterm +routes_inv(r,rr,trtype,t)$[yeart(t) sum{country$r_country(rr,country),ord(country) }] +* then add the cost_hurdle by country (not defined for USA) +* for both the r and rr regions + = sum{country$r_country(r,country),cost_hurdle_country(country) } + + sum{country$r_country(rr,country),cost_hurdle_country(country) } ; + +cost_hurdle_regiongrp2(r,rr,t)$[sum{country$r_country(r,country),ord(country) } + <> sum{country$r_country(rr,country),ord(country) }] + = sum{country$r_country(r,country),cost_hurdle_country(country) } + + sum{country$r_country(rr,country),cost_hurdle_country(country) } ; + + +* define hurdle rates for intra-country lines +cost_hurdle_regiongrp1(r,rr,t) + $[sum{country$[r_country(r,country)$r_country(rr,country)], 1 } + $sum{trtype, routes(r,rr,trtype,t) }] = cost_hurdle_rate1(t) ; + +cost_hurdle_regiongrp2(r,rr,t) + $[sum{country$[r_country(r,country)$r_country(rr,country)], 1 } + $sum{trtype, routes(r,rr,trtype,t) }] = cost_hurdle_rate2(t) ; + +* set hurdle rates for regions within the same GSw_TransHurdleLevel region to zero +$ifthen.hurdlelevel_regiongrp1 %GSw_TransHurdleLevel1% == 'r' + ; +$else.hurdlelevel_regiongrp1 + cost_hurdle_regiongrp1(r,rr,t) + $[sum{%GSw_TransHurdleLevel1% + $[r_%GSw_TransHurdleLevel1%(r,%GSw_TransHurdleLevel1%) + $r_%GSw_TransHurdleLevel1%(rr,%GSw_TransHurdleLevel1%)], 1 } + $sum{trtype, routes(r,rr,trtype,t) }] + = 0 ; +$endif.hurdlelevel_regiongrp1 + +$ifthen.hurdlelevel_regiongrp2 %GSw_TransHurdleLevel2% == 'r' + ; +$else.hurdlelevel_regiongrp2 + cost_hurdle_regiongrp2(r,rr,t) + $[sum{%GSw_TransHurdleLevel2% + $[r_%GSw_TransHurdleLevel2%(r,%GSw_TransHurdleLevel2%) + $r_%GSw_TransHurdleLevel2%(rr,%GSw_TransHurdleLevel2%)], 1 } + $sum{trtype, routes(r,rr,trtype,t) }] + = 0 ; +$endif.hurdlelevel_regiongrp2 + +* The final hurdle cost is the higher cost among regiongrp1 and regiongrp2, and hurdle_rate_floor +cost_hurdle(r,rr,t)$[sum{trtype, routes(r,rr,trtype,t) }] = max{cost_hurdle_regiongrp1(r,rr,t),cost_hurdle_regiongrp2(r,rr,t), hurdle_rate_floor} ; + +* --- transmission distance --- + +* The distance for a transmission interface is calculated in reV using the same "least-cost-path" +* algorithm and cost tables as for wind and solar spur lines. +* Distances are more representative of new greenfield lines than existing lines. +* Written by transmission.py +$onempty +parameter distance(r,rr,trtype) "--miles-- distance between BAs by line type" +/ +$offlisting +$ondelim +$include inputs_case%ds%transmission_miles.csv +$offdelim +$onlisting +/ ; + + +* --- transmission losses --- +* Written by transmission.py +parameter tranloss(r,rr,trtype) "--fraction-- transmission loss between r and rr" +/ +$offlisting +$ondelim +$include inputs_case%ds%tranloss.csv +$offdelim +$onlisting +/ ; +$offempty + + +* --- VSC HVDC macrogrid --- +set val_converter(r,t) "BAs where VSC converter investment is allowed" ; +val_converter(r,t) = no ; + +* VSC converters are allowed in BAs on either side of a valid VSC interface +val_converter(r,t)$[sum{rr, routes_inv(r,rr,"VSC",t) }] = yes ; +val_converter(r,t)$[sum{rr, routes_inv(rr,r,"VSC",t) }] = yes ; + +* Use LCC DC per-MW costs for VSC (converters are handled separately) +transmission_line_fom(r,rr,"VSC")$sum{t, routes(r,rr,"VSC",t) } = transmission_line_fom(r,rr,"LCC") ; + + +* --- Transmission switches --- +* Sw_TransInvMax: According to +* https://www.energy.gov/eere/wind/articles/land-based-wind-market-report-2021-edition-released +* the maximum annual growth rate since 2009 was in 2013, with 543 miles of ≤230 kV, +* 3632 miles of 345 kV, and 466 miles of 500 kV. Using the WECC/TEPPC assumption of +* 1500 MW for 500 kV, 750 MW for 345 kV, and 400 MW for 230 kV (all single-circuit) +* [https://www.wecc.org/Administrative/TEPPC_TransCapCostCalculator_E3_2019_Update.xlsx] +* gives a maximum of 3.64 TWmile/year and an average of 1.36 TWmile/year. + +parameter trans_inv_max(allt) "--TWmile/year-- annual limit on transmission investments" ; +trans_inv_max(t)$[ + tmodel_new(t) + $(yeart(t) >= firstyear_trans_nearterm) + $(yeart(t) < firstyear_trans_longterm) +] = Sw_TransInvMaxNearterm ; + +trans_inv_max(t)$[ + tmodel_new(t) + $(yeart(t) >= firstyear_trans_longterm) +] = Sw_TransInvMaxLongterm ; + +*============================ +* --- Fuel Prices --- +*============================ +*Note - NG supply curve has its own section + +set f "fuel types" +/ +$offlisting +$include inputs_case%ds%f.csv +$onlisting +/ ; + +set fuel2tech(f,i) "mapping between fuel types and generations" +/ +$offlisting +$ondelim +$include inputs_case%ds%fuel2tech.csv +$offdelim +$onlisting +/ ; + +*double check in case any sets have been changed. +fuel2tech("coal",i)$coal(i) = yes ; +fuel2tech("naturalgas",i)$gas(i) = yes ; +fuel2tech("uranium",i)$nuclear(i) = yes ; +fuel2tech(f,i)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), fuel2tech(f,ii) } ; +fuel2tech(f,i)$upgrade(i) = sum{ii$upgrade_to(i,ii), fuel2tech(f,ii) } ; + +*=============================== +* Generator Characteristics +*=============================== + +set plantcat "categories for plant characteristics" +/ +$offlisting +$include inputs_case%ds%plantcat.csv +$onlisting +/ ; + +* declared over allt to allow for external data files that extend beyond end_year +parameter plant_char0(i,allt,plantcat) "--units vary-- input plant characteristics" +/ +$offlisting +$ondelim +$include inputs_case%ds%plantcharout.csv +$offdelim +$onlisting +/ ; + +parameter + winter_cap_ratio(i,v,r) "--scalar-- ratio of winter capacity to summer capacity" + winter_cap_frac_delta(i,v,r) "--scalar-- fractional change in winter compared to summer capacity" + quarter_cap_frac_delta(i,v,r,quarter,allt) "--scalar-- fractional change in quarterly capacity compared to summer" + ccseason_cap_frac_delta(i,v,r,ccseason,allt) "--scalar-- fractional change in ccseason capacity compared to summer" +; + +parameter derate_geo_vintage(i,v) "--fraction-- fraction of capacity available for gen; only geo is <1" ; +derate_geo_vintage(i,v)$valcap_iv(i,v) = 1 ; + +* Initialize values to one so that we do not accidentally zero out capacity +winter_cap_ratio(i,v,r)$valcap_ivr(i,v,r) = 1 ; +* Existing capacity is assigned the winter capacity ratio based on the value from the existing unit database +* Only hintage_data techs (which have a heat rate) are used here. Hydro, CSP, and other techs that might have +* different winter capacities are not treated here. +* Don't filter by valcap because you need the full dataset for calculating new vintages +winter_cap_ratio(i,initv,r)$hintage_data(i,initv,r,'%startyear%','cap') + = hintage_data(i,initv,r,'%startyear%','wintercap') / hintage_data(i,initv,r,'%startyear%','cap') ; +* New capacity is given the capacity-weighted average value from existing units +winter_cap_ratio(i,newv,r)$[valcap_ivr(i,newv,r) + $sum{(initv,rr), hintage_data(i,initv,rr,'%startyear%','wintercap') }] + = sum{(initv,rr), winter_cap_ratio(i,initv,rr) * hintage_data(i,initv,rr,'%startyear%','wintercap') } + / sum{(initv,rr), hintage_data(i,initv,rr,'%startyear%','wintercap') } ; + +* Assign H2-CT and H2-CC techs to have the same winter_cap_ratio as their corresponding gas techs +winter_cap_ratio(i,newv,r)$h2_ct(i) = winter_cap_ratio('gas-ct',newv,r) ; +winter_cap_ratio(i,newv,r)$h2_cc(i) = winter_cap_ratio('gas-cc',newv,r) ; + +* Assign additional nuclear techs to have the same winter_cap_ratio as 'nuclear' +winter_cap_ratio(i,newv,r)$nuclear(i) = winter_cap_ratio('nuclear',newv,r) ; + +* Upgraded plant have the same winter_cap_ratio as what they are upgraded from +winter_cap_ratio(i,newv,r)$upgrade(i) = sum{ii$upgrade_from(i,ii), winter_cap_ratio(ii,newv,r) } ; + +* Remove entries where valcap is false +winter_cap_ratio(i,v,r)$[not valcap_ivr(i,v,r)] = 0 ; + +* Calculate fractional change in winter capacity relative to summer capacity to avoid +* having lots of 1 values +winter_cap_frac_delta(i,v,r)$winter_cap_ratio(i,v,r) = round((winter_cap_ratio(i,v,r) - 1), 3) ; +* Seasonal capacity fraction delta is zero except in winter +quarter_cap_frac_delta(i,v,r,quarter,t)$[winter_cap_frac_delta(i,v,r)$sameas(quarter,'wint')] = winter_cap_frac_delta(i,v,r) ; +ccseason_cap_frac_delta(i,v,r,ccseason,t)$[winter_cap_frac_delta(i,v,r)$sameas(ccseason,'cold')] = winter_cap_frac_delta(i,v,r) ; + +* Apply thermal_summer_cap_delta through seas_cap_frac_delta +quarter_cap_frac_delta(i,v,r,quarter,t)$[conv(i)$sameas(quarter,'summ')] = + climate_heuristics_finalyear('thermal_summer_cap_delta') * climate_heuristics_yearfrac(t) +; +ccseason_cap_frac_delta(i,v,r,ccseason,t)$[conv(i)$sameas(ccseason,'hot')] = + climate_heuristics_finalyear('thermal_summer_cap_delta') * climate_heuristics_yearfrac(t) +; + + + +*============================================ +* -- Consume technologies specification -- +*============================================ + +$onempty +set routes_adjacent(r,rr) "all pairs of adjacent land-based BAs" +/ +$offlisting +$ondelim +$include inputs_case%ds%routes_adjacent.csv +$offdelim +$onlisting +/ ; +$offempty +* Remove offshore zones +routes_adjacent(r,rr)$(offshore(r) or offshore(rr)) = no ; + +set h2_routes(r,rr) "set of feasible pipeline corridors for hydrogen" + h2_routes_inv(r,rr) "set of feasible investment pipeline corridors for hydrogen" +; +* First allow H2 pipelines between any two adjacent zones +h2_routes(r,rr)$[routes_adjacent(r,rr)$Sw_H2_Transport] = yes ; +* Restrict pipelines to the level indicated by GSw_H2_TransportLevel +$ifthen.h2transportlevel %GSw_H2_TransportLevel% == 'r' + h2_routes(r,rr) = no ; +$else.h2transportlevel + h2_routes(r,rr) + $[(not sum{%GSw_H2_TransportLevel% + $[r_%GSw_H2_TransportLevel%(r,%GSw_H2_TransportLevel%) + $r_%GSw_H2_TransportLevel%(rr,%GSw_H2_TransportLevel%)], 1 })] + = no ; +$endif.h2transportlevel + +* Populate H2 pipeline investment routes +h2_routes_inv(r,rr) = h2_routes(r,rr) ; +* Only keep routes with r < rr for investment +h2_routes_inv(r,rr)$(ord(rr) nuke_fom_adj_age_threshold)] = + cost_fom(i,initv,r,t) + nuke_fom_adj * Sw_NukeCoalFOM ; + +table hyd_fom(i,r) "--$/MW-year -- Fixed O&M for hydro technologies" +$offlisting +$ondelim +$include inputs_case%ds%hyd_fom.csv +$offdelim +$onlisting +; + +*note conditional here that will only replace fom +*for hydro techs if it is included in hyd_fom(i,r) +cost_fom(i,v,r,t)$[valcap(i,v,r,t)$hydro(i)$hyd_fom(i,r)] = hyd_fom(i,r) ; + +* Add FOM cost for dr shed resource to cost_fom +cost_fom(i,v,r,t)$[valcap(i,v,r,t)$dr_shed(i)] = fom_dr_shed(i,r,t) ; + +cost_fom(i,initv,r,t)$[(not Sw_BinOM)$valcap(i,initv,r,t)] = sum{tt$tfirst(tt), cost_fom(i,initv,r,tt) } ; + +*upgrade fom costs for initial classes are the fom costs for that tech +*plus the delta between upgrade_to and upgrade_from for the initial year +cost_fom(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,initv,r,t)] = + sum{(v,ii,tt)$[newv(v)$ivt(ii,v,tt)$upgrade_to(i,ii)$(tt.val=Sw_UpgradeChar_Year)], + plant_char(ii,v,tt,"FOM") + hyd_fom(ii,r)$hydro(ii) } +; + +*if available, set cost_fom for upgrades of CCS plants to those specified in hintage_data +cost_fom(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$ccs(i)$Sw_UpgradeFOM_Nems$unitspec_upgrades(i) + $sum{ii$upgrade_from(i,ii), valcap(ii,initv,r,t) } + $sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_FOM") }] = + 1e3 * sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_FOM") } +; + +*upgrade fom costs for new classes are the fom costs +*of the plant that it is being upgraded to +cost_fom(i,newv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,newv,r,t)] = + sum{ii$upgrade_to(i,ii), cost_fom(ii,newv,r,t) } ; + +*==================== +* --- Heat Rates --- +*==================== + +parameter heat_rate(i,v,r,t) "--MMBtu/MWh-- heat rate" ; + +heat_rate(i,v,r,t)$valcap(i,v,r,t) = plant_char(i,v,t,'heatrate') ; + +heat_rate(i,newv,r,t)$[valcap(i,newv,r,t)$countnc(i,newv)] = + sum{tt$ivt(i,newv,tt), plant_char(i,newv,tt,'heatrate') } / countnc(i,newv) ; + +* fill in heat rate for initial capacity that does not have a binned heatrate +heat_rate(i,initv,r,t)$[valcap(i,initv,r,t)$(not heat_rate(i,initv,r,t))] = plant_char(i,initv,"%startyear%",'heatrate') ; + +*note here conversion from btu/kwh to MMBtu/MWh +heat_rate(i,v,r,t)$[valcap(i,v,r,t)$sum{allt$att(allt,t), binned_heatrates(i,v,r,allt) }] = + sum{allt$att(allt,t), binned_heatrates(i,v,r,allt) } / 1000 ; + + +set prepost "set defining pre-2010 values versus post-2010 values" +/ +$offlisting +$include inputs_case%ds%prepost.csv +$onlisting +/ ; + +*part load heatrate adjust based on historical EIA generation and fuel use data +*this reflects the indescrepancy from the partial-loaded heat rate +*and the fully-loaded heat rate + +table heat_rate_adj(i,prepost) "--unitless-- partial load heatrate adjuster based on historical EIA generation and fuel use data" +$offlisting +$ondelim +$include inputs_case%ds%heat_rate_adj.csv +$offdelim +$onlisting +; + +heat_rate_adj(i,prepost)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), heat_rate_adj(ii,prepost) } ; + +heat_rate_adj(i,prepost)$upgrade(i) = sum{ii$upgrade_to(i,ii), heat_rate_adj(ii,prepost) } ; + +*upgrade heat rates for initial classes are the heat rates for that tech +*plus the delta between upgrade_to and upgrade_from for the initial year +heat_rate(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,initv,r,t)] = + sum{(v,ii,tt)$[newv(v)$ivt(ii,v,tt)$upgrade_to(i,ii)$(tt.val=Sw_UpgradeChar_Year)], + plant_char(ii,v,tt,"heatrate") } +; + +*if available, set heat_rate for upgrades of CCS plants to those specified in hintage_data +heat_rate(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$ccs(i) + $sum{ii$upgrade_from(i,ii), valcap(ii,initv,r,t) } + $sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_HR") }] = + sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_HR") / 1000 } +; + +*upgrade heat rates for new classes are the heat rates for +*the bintage and technology for what it is being upgraded to +heat_rate(i,newv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,newv,r,t)] = + sum{ii$upgrade_to(i,ii), heat_rate(ii,newv,r,t) } ; + +heat_rate(i,v,r,t)$[heat_rate_adj(i,'pre2010')$initv(v)] = heat_rate_adj(i,'pre2010') * heat_rate(i,v,r,t) ; +heat_rate(i,v,r,t)$[heat_rate_adj(i,'post2010')$newv(v)] = heat_rate_adj(i,'post2010') * heat_rate(i,v,r,t) ; + +*========================================= +* --- Fuel Prices --- +*========================================= + +parameter fuel_price(i,r,t) "$/MMBtu - fuel prices by technology" ; + + +* Written by reeds/inputs\fuelcostprep.py +* declared over allt to allow for external data files that extend beyond end_year +table fprice(allt,r,f) "--2004$/MMBtu-- fuel prices by fuel type" +$offlisting +$ondelim +$include inputs_case%ds%fprice.csv +$offdelim +$onlisting +; + +fuel_price(i,r,t)$[sum{f$fuel2tech(f,i),1}] = + sum{(f,allt)$[fuel2tech(f,i)$(year(allt)=yeart(t))], fprice(allt,r,f) } ; + +fuel_price(i,r,t)$[sum{f$fuel2tech(f,i),1}$(not fuel_price(i,r,t))] = + sum{rr$fuel_price(i,rr,t), fuel_price(i,rr,t) } / max(1,sum{rr$fuel_price(i,rr,t), 1 }) ; + +fuel_price(i,r,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), fuel_price(ii,r,t) } ; + + +*===================================================== +* -- Climate impacts on nondispatchable hydropower -- +*===================================================== + +$ifthen.climatehydro %GSw_ClimateHydro% == 1 + +* declared over allt to allow for external data files that extend beyond end_year +* Written by climateprep.py +table climate_hydro_annual(r,allt) "annual dispatchable hydropower availability" +$offlisting +$ondelim +$include inputs_case%ds%climate_hydadjann.csv +$offdelim +$onlisting +; +$endif.climatehydro + + +*===================================================== +* -- Climate impacts on nondispatchable hydropower -- +*===================================================== + +$ifthen.climatewater %GSw_ClimateWater% == 1 + +* Written by climateprep.py +table wat_supply_climate(wst,r,allt) "time-varying annual water supply" +$offlisting +$ondelim +$include inputs_case%ds%climate_UnappWaterMultAnn.csv +$offdelim +$onlisting +; + +set wst_climate(wst) "water sources affected by climate change" +/ +$offlisting +$ondelim +$include inputs_case%ds%wst_climate.csv +$offdelim +$onlisting +/ +; +$endif.climatewater + + +*============================================= +* -- Capacity Factor Adjustments over Time -- +*============================================= + +*created by /reeds/inputs/writecapdat.py +parameter cap_hyd_ccseason_adj(i,ccseason,r) "--fraction-- ccseason max capacity adjustment for dispatchable hydro" +/ +$offlisting +$ondelim +$include inputs_case%ds%cap_hyd_ccseason_adj.csv +$offdelim +$onlisting +/ ; + +table wind_cf_adj_t(allt,i) "--unitless-- wind capacity factor adjustments by class, from ATB" +$offlisting +$ondelim +$include inputs_case%ds%windcfmult.csv +$offdelim +$onlisting +; + +parameter pv_cf_improve(allt) "--unitless-- PV capacity factor improvement" +/ +$offlisting +$ondelim +$include inputs_case%ds%pv_cf_improve.csv +$offdelim +$onlisting +/ ; + +parameter cf_adj_t(i,v,t) "--unitless-- capacity factor adjustment over time for RSC technologies" ; + +cf_adj_t(i,v,t)$[(rsc_i(i) or hydro(i))$sum{r, valcap(i,v,r,t) }] = 1 ; + +* Existing wind uses 2010 cf adjustment +cf_adj_t(i,initv,t)$[wind(i)$sum{r, valcap(i,initv,r,t) }] = wind_cf_adj_t("%startyear%",i) ; + +cf_adj_t(i,newv,t)$[wind_cf_adj_t(t,i)$countnc(i,newv)$sum{r, valcap(i,newv,r,t) }] = + sum{tt$ivt(i,newv,tt), wind_cf_adj_t(tt,i) } / countnc(i,newv) ; + +* Apply PV capacity factor improvements +cf_adj_t(i,newv,t)$[(pv(i) or pvb(i))$countnc(i,newv)$sum{r, valcap(i,newv,r,t) }] = + sum{tt$ivt(i,newv,tt), pv_cf_improve(tt) } / countnc(i,newv) ; + + + +*======================================== +* --- OPERATING RESERVES --- +*======================================== + +set ortype "types of operating reserve constraints" +/ +$offlisting +$include inputs_case%ds%ortype.csv +$onlisting +/ ; + +set opres_model(ortype) "operating reserve types modeled" ; + +set orcat "operating reserve category for RHS calculations" +/ +$offlisting +$include inputs_case%ds%orcat.csv +$onlisting +/ ; + +* define elements in opres_model based on sw_opres +opres_model(ortype)$[not Sw_Opres] = no ; +opres_model(ortype)$[(Sw_Opres = 1)$(not sameas(ortype,"combo"))] = yes ; +opres_model("combo")$[(Sw_Opres = 2)] = yes ; + + +Parameter + reserve_frac(i,ortype) "--fraction-- fraction of a technology's online capacity that can contribute to a reserve type" + ramptime(ortype) "--minutes-- minutes for ramping limit constraint in operating reserves" +/ +$offlisting +$ondelim +$include inputs_case%ds%ramptime.csv +$offdelim +$onlisting +/ ; + + +table orperc(ortype,orcat) "operating reserve percentage by type and category" +$offlisting +$ondelim +$include inputs_case%ds%orperc.csv +$offdelim +$onlisting +; + +* for simplified combination, make the constraints as +* stringent as possible - ie sum over all requirements +orperc("combo",orcat) = sum{ortype,orperc(ortype,orcat) } ; + +* combo ramptime is average across all ortypes where defined +ramptime("combo") = sum{ortype$ramptime(ortype) , ramptime(ortype) } + / sum{ortype$ramptime(ortype) , 1 } ; + +* multiplier for reserves requirement +orperc(ortype,orcat) = orperc(ortype,orcat) * Sw_OpResReqMult ; + +*ramp rates are used to limit a technology's contribution to Operating Reserve. +parameter ramprate(i) "--fraction/min-- ramp rate of dispatchable generators" +/ +$offlisting +$ondelim +$include inputs_case%ds%ramprate.csv +$offdelim +$onlisting +/ ; + +*dispatchable hydro is the only "hydro" technology that can provide operating reserves. +ramprate(i)$hydro_d(i) = ramprate("hydro") ; +ramprate(i)$geo(i) = ramprate("geothermal") ; + +*if running with flexible nuclear, set ramp rate of nuclear to that of coal +ramprate(i)$[nuclear(i)$Sw_NukeFlex] = ramprate("coal-new") ; + +ramprate(i)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), ramprate(ii) } ; + +ramprate(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), ramprate(ii) } ; + +* Do not allow the reserve fraction to exceed 100%, so use the minimum of 1 or the computed value. +reserve_frac(i,ortype) = min(1,ramprate(i) * ramptime(ortype)) ; + +reserve_frac(i,ortype)$upgrade(i) = sum{ii$upgrade_to(i,ii), reserve_frac(ii,ortype) } ; + +* input data for opres reserve costs by generator type (in $2004) +* current options are bottom up costs by generator type ("default") or estimates based on historical reserve prices ("market") +* market data based on national reserve prices in https://www.nrel.gov/docs/fy19osti/72578.pdf (converted from $2017) +table cost_opres_input(i,ortype) +$offlisting +$ondelim +$include inputs_case%ds%cost_opres_%GSw_OpResCost%.csv +$offdelim +$onlisting +; + +parameter cost_opres(i,ortype,t) "--$ / MWh-- cost of reg operating reserves" ; +cost_opres(i,ortype,t) = cost_opres_input(i, ortype) ; + +* assign reserve costs to all geothermal techs +cost_opres(i,ortype,t)$geo(i) = cost_opres("geothermal",ortype,t) ; + +* Assign hybrid PV+battery the same value as battery_li +cost_opres(i,ortype,t)$pvb(i) = cost_opres("battery_li",ortype,t) ; + +* add heat rate penalty for providing reserves (currently only applied to spin) +* input data calculated based on heat rates in the PLEXOS EI database as of Dec. 2020 +parameter spin_hr_penalty(i) "--fraction-- heat rate penalty for providing spinning reserves" +/ +$offlisting +$ondelim +$include inputs_case%ds%heat_rate_penalty_spin.csv +$offdelim +$onlisting +/ ; + +* calculate average heat rate and fuel prices +parameter fuel_price_avg(i,t) ; +parameter heat_rate_avg(i,t) ; + +* calculate average fuel price and heat rates +fuel_price_avg(i,t)$[sum{r, fuel_price(i,r,t) }] = sum{r, fuel_price(i,r,t) } / sum{r, 1$[fuel_price(i,r,t)] } ; + +heat_rate_avg(i,t)$[sum{(v,r), heat_rate(i,v,r,t) }] = + sum{(v,r), heat_rate(i,v,r,t) } / sum{(v,r), 1$[heat_rate(i,v,r,t)] } ; + +* calculate penalty value, assign to cost_opres +* only assign penalty in instances where spin costs are not already defined +cost_opres(i,"spin",t)$[not cost_opres(i,"spin",t)] = + spin_hr_penalty(i) * heat_rate_avg(i,t) * fuel_price_avg(i,t) ; + +cost_opres(i,ortype,t)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), cost_opres(ii,ortype,t) } ; + +cost_opres(i,ortype,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), cost_opres(ii,ortype,t) } ; + +* again - making the assumption that the combination +* operating reserve is the most stringent/costly +cost_opres(i,"combo",t) = smax{ortype, cost_opres(i,ortype,t) } ; + + +*====================================================== +* --- MinLoading (only used if Sw_MinLoading != 0) --- +*====================================================== + +parameter minloadfrac0(i) "--fraction-- initial minimum loading fraction" +/ +$offlisting +$ondelim +$include inputs_case%ds%minloadfrac0.csv +$offdelim +$onlisting +/ ; + +minloadfrac0(i)$geo(i) = minloadfrac0("geothermal") ; + +parameter hydmin_quarter(i,r,quarter) "minimum hydro loading factors by quarter and region" +/ +$offlisting +$ondelim +$include inputs_case%ds%hydro_mingen.csv +$offdelim +$onlisting +/ ; + +parameter startcost(i) "--$/MW-- linearized startup cost" +/ +$offlisting +$ondelim +$include inputs_case%ds%startcost.csv +$offdelim +$onlisting +/ ; +startcost(i)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), startcost(ii) } ; +startcost(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), startcost(ii) } ; +* Turn off startcost for some techs based on GSw_StartCost +startcost(i)$[(Sw_StartCost=0)] = 0 ; +startcost(i)$[(Sw_StartCost=1)$(not nuclear(i))] = 0 ; +startcost(i)$[(Sw_StartCost=2)$(not nuclear(i))$(not ccs(i))$(not coal(i))] = 0 ; +startcost(i)$[(Sw_StartCost=3)$(not ccs(i))$(not coal(i))] = 0 ; +startcost(i)$[(Sw_StartCost=4)$(not ccs(i))$(not coal(i))$(not gas_cc(i))$(not h2_cc(i))] = 0 ; +startcost(i)$[(Sw_StartCost=5)$(nuclear(i))] = 0 ; + +parameter mingen_fixed(i) "--fraction-- minimum generation level across all hours" +/ +$offlisting +$ondelim +$include inputs_case%ds%mingen_fixed.csv +$offdelim +$onlisting +/ ; + +*========================================= +* --- Load --- +*========================================= + +$onempty +parameter loadsite_annual(loadsitereg,allt) "--MW-- Load trajectory by loadsitereg" +/ +$offlisting +$ondelim +$include inputs_case%ds%loadsite_annual.csv +$offdelim +$onlisting +/ ; +set val_loadsite(r) "Valid regions for load sites" ; +val_loadsite(r) + $sum{(loadsitereg,t)$r_loadsitereg(r,loadsitereg), + loadsite_annual(loadsitereg,t) + } = yes ; + +table can_growth_rate(st,allt) "growth rate for candadian demand by province" +$offlisting +$ondelim +$include inputs_case%ds%cangrowth.csv +$offdelim +$onlisting +; + +parameter mex_growth_rate(allt) "growth rate for mexican demand - national" +/ +$offlisting +$ondelim +$include inputs_case%ds%mex_growth_rate.csv +$offdelim +$onlisting +/ ; +$offempty + + +*============================== +* --- Planning Reserve Margin --- +*================================ + +parameter prm(r,t) "--fraction-- planning reserve margin by model year" +/ +$offlisting +$ondelim +$include inputs_case%ds%prm_initial.csv +$offdelim +$onlisting +/ ; + +$onempty +parameter firm_import_limit(nercr,allt) "--fraction-- limit on net firm imports into NERC regions" +/ +$offlisting +$ondelim +$include inputs_case%ds%firm_import_limit.csv +$offdelim +$onlisting +/ ; + +parameter peakload_nercr(nercr,allt) "--MW-- Peak exogenous demand across all weather years by NERC region" +/ +$offlisting +$ondelim +$include inputs_case%ds%peakload_nercr.csv +$offdelim +$onlisting +/ ; +$offempty + + +* =========================================================================== +* Regional and temporal capital cost multipliers +* =========================================================================== +* Load scenario-specific capital cost multiplier components + +parameter ccmult(i,allt) "construction cost multiplier" +/ +$offlisting +$ondelim +$include inputs_case%ds%ccmult.csv +$offdelim +$onlisting +/ ; + +parameter tax_rate(allt) "all-in tax rate" +/ +$offlisting +$ondelim +$include inputs_case%ds%tax_rate.csv +$offdelim +$onlisting +/ ; + +parameter itc_frac_monetized(i,allt) "fractional value of the ITC, after adjusting for the costs of monetization" +/ +$offlisting +$ondelim +$include inputs_case%ds%itc_frac_monetized.csv +$offdelim +$onlisting +/ ; + +$onempty +parameter itc_energy_comm_bonus(i,r) "energy community tax credit bonus factor" +/ +$offlisting +$ondelim +$include inputs_case%ds%itc_energy_comm_bonus.csv +$offdelim +$onlisting +/ ; +$offempty + +parameter pv_frac_of_depreciation(i,allt) "present value of depreciation, expressed as a fraction of the capital cost of the investment" +/ +$offlisting +$ondelim +$include inputs_case%ds%pv_frac_of_depreciation.csv +$offdelim +$onlisting +/ ; + +parameter degradation_adj(i,allt) "adjustment to reflect degradation over the lifetime of an asset" +/ +$offlisting +$ondelim +$include inputs_case%ds%degradation_adj.csv +$offdelim +$onlisting +/ ; + +parameter financing_risk_mult(i,allt) "multiplier to reflect higher financing costs for riskier assets" +/ +$offlisting +$ondelim +$include inputs_case%ds%financing_risk_mult.csv +$offdelim +$onlisting +/ ; + +parameter reg_cap_cost_diff(i,r) "regional capital cost difference [fraction] (note that wind-ons and upv have separate multiplers in the supply curve cost)" +/ +$offlisting +$ondelim +$include inputs_case%ds%reg_cap_cost_diff.csv +$offdelim +$onlisting +/ ; + +parameter eval_period_adj_mult(i,allt) "adjustment multiplier for the capital costs of techs with non-standard evaluation periods" +/ +$offlisting +$ondelim +$include inputs_case%ds%eval_period_adj_mult.csv +$offdelim +$onlisting +/ ; + +eval_period_adj_mult(i,t)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), eval_period_adj_mult(ii,t) } ; +eval_period_adj_mult(i,t)$[upgrade(i)] = + sum{ii$upgrade_to(i,ii),eval_period_adj_mult(ii,t) } ; + + +* Define and calculate the scenario-specific capital cost multipliers +* If the ITC is phasing out dynamically, these will need to be re-calculated based on the phase-out +parameter +cost_cap_fin_mult(i,r,t) "final capital cost multiplier for regions and technologies - used in the objective function", +cost_cap_fin_mult_noITC(i,r,t) "final capital cost multiplier excluding ITC - used only in outputs", +cost_cap_fin_mult_no_credits(i,r,t) "final capital cost multiplier ITC/PTC/Depreciation (i.e. the actual expenditures) - used only in outputs", +cost_cap_fin_mult_out(i,r,t) "final capital cost multiplier for system cost outputs" ; + +parameter trans_cost_cap_fin_mult(allt) "capital cost multiplier for transmission - used in the objective function" +/ +$offlisting +$ondelim +$include inputs_case%ds%trans_cap_cost_mult.csv +$offdelim +$onlisting +/ ; + +parameter trans_cost_cap_fin_mult_noITC(allt) "capital cost multiplier for transmission excluding ITC - used only in outputs" +/ +$offlisting +$ondelim +$include inputs_case%ds%trans_cap_cost_mult_noITC.csv +$offdelim +$onlisting +/ ; + + +* --- Hybrid PV+Battery --- +* Hybrid PV+Battery: PV portion +parameter cost_cap_fin_mult_pvb_p(i,r,t) "capital cost multiplier for the PV portion of hybrid PV+Battery" + cost_cap_fin_mult_pvb_p_noITC(i,r,t) "capital cost multiplier for the PV portion of hybrid PV+Battery, excluding ITC" + cost_cap_fin_mult_pvb_p_no_credits(i,r,t) "capital cost multiplier for the PV portion of hybrid PV+Battery, excluding ITC/PTC/Depreciation" +; + +* Hybrid PV+Battery: Battery portion +parameter cost_cap_fin_mult_pvb_b(i,r,t) "capital cost multiplier for the battery portion of hybrid PV+Battery" + cost_cap_fin_mult_pvb_b_noITC(i,r,t) "capital cost multiplier for the battery portion of hybrid PV+Battery, excluding ITC" + cost_cap_fin_mult_pvb_b_no_credits(i,r,t) "capital cost multiplier for the battery portion of hybrid PV+Battery, excluding ITC/PTC/Depreciation" +; + + +* --- Nuclear Ban --- +*Assign increased cost multipliers to regions with state nuclear bans +scalar nukebancostmult "--fraction-- penalty for constructing new nuclear in a restricted region" /%GSw_NukeStateBanCostMult%/ ; + +* --- Renewable Supply Curves --- +* For offshore wind, rsc_fin_mult(i,r,t) also carries the ITC that is applied to the transmission costs in the resource supply curve cost, while rsc_fin_mult_no_ITC(i,r,t) carries financing multipliers for its transmission costs without the ITC +parameter rsc_fin_mult(i,r,t) "--fraction-- financial cost multiplier for resource supply curve technologies that have their capital costs included in the supply curves (capital cost reduction multipliers are also included where relevant)" + rsc_fin_mult_noITC(i,r,t) "--fraction-- financial cost multiplier excluding ITC for resource supply curve technologies that have their capital costs included in the supply curves" +; + +*========================================= +* --- Emission Rate --- +*========================================= + +* Emission rate by technology and etype (broken down to process and upstream) +* Note that CH4 upstream emission rate of natural gas is 0 here +* as we will use CH4 methane leakage from GSw_MethaneLeakageScen for it later) +table emit_rate_fuel(i,etype,e) "--metric tons per MMBtu-- emissions rate of fuel by technology and emission type" +$offlisting +$ondelim +$include inputs_case%ds%emitrate.csv +$offdelim +$onlisting +; + +* this table links CCS techs with their uncontrolled tech counterpart (where such a tech exists) +set ccs_link(i,ii) "links CCS techs with their uncontrolled tech counterpart (where such a tech exists)" +/ +$offlisting +$ondelim +$include inputs_case%ds%ccs_link.csv +$ifthen.ctech %GSw_WaterMain% == 1 +$include inputs_case%ds%ccs_link_water.csv +$endif.ctech +$offdelim +$onlisting +/ ; + +parameter capture_rate_input(i,e) "--fraction-- fraction of emissions that are captured" ; + +* Set CO2 capture rate for new CCS capacity +capture_rate_input(i,"CO2")$[ccs_mod(i)]=Sw_CCS_Rate_New_mod; +capture_rate_input(i,"CO2")$[ccs_max(i)]=Sw_CCS_Rate_New_max; + +* Set CO2 capture rate for retrofit/upgrade CCS capacity +capture_rate_input(i,"CO2")$[upgrade(i)$(coal_ccs(i) or gas_cc_ccs(i))$ccs_mod(i)]=Sw_CCS_Rate_Upgrade_mod; +capture_rate_input(i,"CO2")$[upgrade(i)$(coal_ccs(i) or gas_cc_ccs(i))$ccs_max(i)]=Sw_CCS_Rate_Upgrade_max; + +* emit_rate_fuel water expansion +emit_rate_fuel(i,etype,e)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), emit_rate_fuel(ii,etype,e) } ; + +* Assign the appropriate % of generation for each technology to count toward CES requirements. +* Exclude capture rates of BECCS, which receive full credit in a CES and were already set to 1 above in the "RPS" section. +RPSTechMult(RPSCat,i,st)$[ccs(i)$(sameas(RPSCat,"CES") or sameas(RPSCat,"CES_Bundled"))$(not beccs(i))] = capture_rate_input(i,"CO2") ; + +* calculate process emit rate for CCS techs (except beccs techs, which are defined directly in emitrate.csv) +emit_rate_fuel(i,"process",e)$[ccs(i)$(not beccs(i))] = + (1 - capture_rate_input(i,e)) * sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"process",e) } ; + +* calculate upstream emit rate for CCS techs (except beccs techs, which are defined directly in emitrate.csv) +emit_rate_fuel(i,"upstream",e)$[ccs(i)$(not beccs(i))] = sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"upstream",e) } ; + +* assign flexible ccs the same process emission rate as the uncontrolled technology to allow variable CO2 removal (e.g., for gas-cc-ccs-f1, use gas-cc) +emit_rate_fuel(i,"process",e)$[ccsflex(i)] = sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"process",e) } ; + +* set upgrade tech process emissions for non-CCS upgrades (e.g. gas-ct -> h2-ct); CCS upgrade emissions are handled above +emit_rate_fuel(i,"process",e)$[upgrade(i)$(not ccs(i))] = sum{ii$upgrade_to(i,ii), emit_rate_fuel(ii,"process",e) } ; + +* set upgrade tech upstream emissions for upgrades +emit_rate_fuel(i,"upstream",e)$[upgrade(i)] = sum{ii$upgrade_to(i,ii), emit_rate_fuel(ii,"upstream",e) } ; + +* parameters for calculating captured emissions +parameter capture_rate_fuel(i,e) "--metric tons per MMBtu-- emissions capture rate of fuel by technology type"; +capture_rate_fuel(i,e) = capture_rate_input(i,e) * sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"process",e) } ; + +* capture_rate_fuel is used to calculate how much CO2 is captured and stored; +* for beccs, the captured CO2 is the entire negative emissions rate +* since any uncontrolled emissions are assumed to be lifecycle net zero +capture_rate_fuel(i,"CO2")$beccs(i) = - emit_rate_fuel(i,"process","CO2") + +parameter capture_rate(e,i,v,r,t) "--metric tons per MWh-- emissions capture rate" ; + +parameter methane_leakage_rate(allt) "--fraction-- methane leakage as fraction of gross production" +* best estimate for fixed leakage rate is 0.023 (Alvarez et al. 2018, https://dx.doi.org/10.1126/science.aar7204) +/ +$offlisting +$ondelim +$include inputs_case%ds%methane_leakage_rate.csv +$offdelim +$onlisting +/ ; + +scalar methane_tonperMMBtu "--metric tons per MMBtu-- methane content of natural gas" ; +* [ton CO2 / MMBtu] * [ton CH4 / ton CO2] +methane_tonperMMBtu = emit_rate_fuel("gas-CC","process","CO2") * molWeightCH4 / molWeightCO2 ; + +* H2 leakage rate by technology and etype (broken down to process and upstream) +parameter h2_leakage_rate(i) "--fraction-- h2 leakage rate as a fraction of total production by technology and emission type" +/ +$offlisting +$ondelim +$include inputs_case%ds%h2_leakage_rate.csv +$offdelim +$onlisting +/ ; + +parameter prod_emit_rate(etype,e,i,allt) "--metric tons emitted per metric ton product-- emissions rate per metric ton of product (e.g. tonCO2/tonH2 for SMR & SMR-CCS)" ; +* Steam methane reformer (SMR)'s process emission here refers to emissions from steam methane reforming process +prod_emit_rate("process","CO2","smr",t) = smr_co2_intensity ; +prod_emit_rate("process","CO2","smr_ccs",t) = smr_co2_intensity * (1 - smr_capture_rate) ; +prod_emit_rate("process","CO2","dac",t)$Sw_DAC = -1 ; +prod_emit_rate("process","CO2","dac_gas",t)$Sw_DAC_Gas = -1 ; + +scalar smr_methane_rate "--metric tons CH4 per metric ton H2-- methane used to produce a metric ton of H2 via SMR" ; +* NOTE that we don't yet include the impact of CCS on methane use +* [ton CH4 used / ton H2] = [ton CO2 emitted / ton H2] * [ton CH4 used / ton CO2 emitted], where +* [ton CH4 used / ton CO2 emitted] is the ratio of the molecular weight of CH4 to CO2 +smr_methane_rate = smr_co2_intensity * molWeightCH4 / molWeightCO2 ; + +* Upstream fuel emissions for SMR +*** [ton CH4 used / ton H2] * [ton CH4 leaked / ton CH4 produced] * [ton CH4 produced / ton CH4 used] +prod_emit_rate("upstream",e,i,t) + $[sameas(e,"CH4") + $smr(i) + $methane_leakage_rate(t)] + = smr_methane_rate * methane_leakage_rate(t) / (1 - methane_leakage_rate(t)) +; + +* Process H2 emissions for SMR, SMR-CC, and electrolyzer +*** [ton H2 leaked / ton H2 produced] * [ton H2 produced / ton H2 used] +prod_emit_rate("process",e,i,t) + $[sameas(e,"H2") + $h2(i) + $h2_leakage_rate(i)] + = h2_leakage_rate(i) / (1 - h2_leakage_rate(i)) +; + +parameter + emit_rate(etype,eall,i,v,r,t) "--metric tons per MWh-- emissions rate" + emit_r_tc(r,t) "--metric tons-- CO2 emissions, regional" + emit_nat_tc(t) "--metric tons-- CO2 emissions, national" +; + +emit_rate(etype,e,i,v,r,t)$[emit_rate_fuel(i,etype,e)$valcap(i,v,r,t)] + = round(heat_rate(i,v,r,t) * emit_rate_fuel(i,etype,e),10) ; + +*only emissions from the coal portion of cofire plants are considered +emit_rate(etype,e,i,v,r,t)$[sameas(i,"cofire")$emit_rate_fuel("coal-new",etype,e)$valcap(i,v,r,t)] + = round((1-bio_cofire_perc) * heat_rate(i,v,r,t) * emit_rate_fuel("coal-new",etype,e),10) ; + +* Fill in CH4 upstream emission rate +*** [MMBtu/MWh] * [ton methane used / MMBtu] * [ton methane leaked / ton methane produced] +*** * [ton methane produced / ton methane used] = [ton methane leaked / MWh] +emit_rate("upstream",e,i,v,r,t) + $[methane_leakage_rate(t) + $gas(i) + $sameas(e,"CH4")] + = heat_rate(i,v,r,t) * methane_tonperMMBtu * methane_leakage_rate(t) / (1 - methane_leakage_rate(t)) +; + +* Fill in H2 process emission rates for H2 combustion techs (h2-ct and h2-cc) (and fuel cell later) +*** [heat rate (MMBtu/MWh)] * [h2 combustion intensity (metric ton h2 used / MMBtu)] * [h2 leakage rate (metric ton h2 leaked / ton h2 produced)] +*** * [ton h2 produced / ton h2 used] = [ton h2 leaked / MWh] +emit_rate("process",e,i,v,r,t) + $[h2_leakage_rate(i) + $sameas(e,"H2")] + = heat_rate(i,v,r,t) * h2_combustion_intensity * h2_leakage_rate(i) / (1 - h2_leakage_rate(i)) +; + +* set upgraded H2 tech emissions +emit_rate("process","H2",i,v,r,t)$[upgrade(i)] = sum{ii$upgrade_to(i,ii), emit_rate("process","H2",ii,v,r,t) } ; + +* Global warming potential of different pollutants +parameter gwp(e) "--metric ton CO2-equivalents --global warming potential" +/ +$ondelim +$include inputs_case%ds%gwp.csv +$offdelim +/ ; + +* CO2(e) emissions rate (used in postprocessing only) +emit_rate(etype,"CO2e",i,v,r,t)$[Sw_AnnualCap=2] + = round(sum{e, emit_rate(etype,e,i,v,r,t) * gwp(e)$[(not sameas(e, "H2"))]},10) ; + +emit_rate(etype,"CO2e",i,v,r,t)$[Sw_AnnualCap<>2] + = round(sum{e, emit_rate(etype,e,i,v,r,t) * gwp(e)},10) ; + +* calculate emissions capture rates (same logic as emissions calc above) +capture_rate(e,i,v,r,t)$[capture_rate_fuel(i,e)$valcap(i,v,r,t)] + = round(heat_rate(i,v,r,t) * capture_rate_fuel(i,e),10) ; + +capture_rate(e,i,v,r,t)$[upgrade(i)$capture_rate_fuel(i,e)] = round(heat_rate(i,v,r,t) * capture_rate_fuel(i,e),10) ; + +* Regional emissions rate +parameter + co2_emit_rate_r(r,t) "--metric tons per MWh-- CO2 emissions rate by ReEDS region, for use in state carbon caps" + co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t) "--metric tons per MWh-- CO2 regional emissions rate, for use in state carbon caps" +; +co2_emit_rate_r(r,t) = 0 ; +co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t) = 0 ; + +* =========================================================================== +* Regional emissions rate limit (currently unused) +* =========================================================================== + +set emit_rate_con(e,r,t) "set to enable or disable emissions rate limits by pollutant and region" ; +emit_rate_con(e,r,t) = no ; + +parameter emit_rate_limit(e,r,t) "--metric tons per MWh-- emission rate limit" ; +emit_rate_limit(e,r,t) = 0 ; + +*============================ +* Growth limits and penalties +*============================ + +set gbin "growth bins" +/ +$offlisting +$include inputs_case%ds%gbin.csv +$onlisting +/ ; + +*absolute growth penalties based on greatest annual change of capacity for each tech group from 1990-2016 +parameter growth_limit_absolute(tg) "--MW-- growth limit for technology groups in absolute terms" +/ +$offlisting +$ondelim +$include inputs_case%ds%growth_limit_absolute.csv +$offdelim +$onlisting +/ ; + +parameter growth_penalty(gbin) "--unitless-- multiplier penalty on the capital cost for growth in each bin" +/ +$offlisting +$ondelim +$include inputs_case%ds%growth_penalty.csv +$offdelim +$onlisting +/ ; + +* gbin_min is based on the representative plant size for a single plant in that tech group +parameter gbin_min(tg) "--MW-- minimum size of the first (zero cost) growth bin" +/ +$offlisting +$ondelim +$include inputs_case%ds%gbin_min.csv +$offdelim +$onlisting +/ ; + +parameter growth_bin_size_mult(gbin) "--unitless-- multiplier for each growth bin to be applied to the prior solve year's annual deployment" +/ +$offlisting +$ondelim +$include inputs_case%ds%growth_bin_size_mult.csv +$offdelim +$onlisting +/ ; + +parameter growth_bin_limit(gbin,st,tg,t) "--MW/yr-- size of each growth bin" + last_year_max_growth(st,tg,t) "--MW-- maximum growth that could have been achieved in the prior year (acutal year, not solve year)" + cost_growth(i,st,t) "--$/MW-- cost basis for growth penalties" +; + +* Initialize values +growth_bin_limit(gbin,st,tg,tfirst)$stfeas(st) = gbin_min(tg) ; +cost_growth(i,st,t) = 0 ; + +*==================================== +* --- CES Gas supply curve setup --- +*==================================== + +set gb "gas price bin must be an odd number of bins, e.g. gb1*gb15" +/ +$offlisting +$include inputs_case%ds%gb.csv +$onlisting +/ ; + +alias(gb,gbb) ; + +* gassupply scale determines how far the bins reference quantity should deviate from its reference price +* with gassupply scale = -0.5, the center of the reference price bin will be the reference quantity +* with gassupplyscale = 0, the end of the reference gas price's bin will the limit for that reference bin + +*note that the supply curve is set up such that the edge of the bin pertaining to the reference +*price sits at its upper limit, we want to move the curve such that the reference price sits at the middle of the +*respective bin + +parameter gasprice(cendiv,gb,t) "--$/MMBtu-- price of each gas bin", + gasquant(cendiv,gb,t) "--MMBtu - natural gas quantity for each bin", + gaslimit(cendiv,gb,t) "--MMBtu-- gas limit by gas bin" + gassupply_ele(cendiv,t) "--MMBtu-- reference gas consumption by the ELE sector" + gassupply_tot(cendiv,t) "--MMBtu-- reference gas consumption by the ELE sector" ; + +* declared over allt to allow for external data files that extend beyond end_year +table gasprice_ref(cendiv,allt) "--2004$/MMBtu-- natural gas price by census division" +$offlisting +$ondelim +$include inputs_case%ds%gasprice_ref.csv +$offdelim +$onlisting +; + +* fuel costs for H2 production +* starting units for gas efficiency are MMBtu / kg - need to express this in terms of +* $ / MT through MMBtu / kg * (kg / MT) * ($ / MMBtu) +* SMR production costs seem high given gas-intensity and units +* -- cost of production, for now, just gas_intensity times reference gas price, can revisit gas price assumptions -- +parameter h2_fuel_cost(i,v,r,t) "--$ per metric ton-- fuel cost for hydrogen production" ; +h2_fuel_cost(i,newv,r,t)$[h2(i)$valcap(i,newv,r,t)] = 1000 * (sum{tt$ivt(i,newv,tt),consume_char0(i,tt,"gas_efficiency") } / countnc(i,newv)) + * sum{cendiv$r_cendiv(r,cendiv),gasprice_ref(cendiv,t) } * industrialGasMult ; + +* initial capacity gets charged at the initial NG efficiency +h2_fuel_cost(i,initv,r,t)$[h2(i)$valcap(i,initv,r,t)] = 1000 * consume_char0(i,"%startyear%","gas_efficiency") + * sum{cendiv$r_cendiv(r,cendiv),gasprice_ref(cendiv,t) } * industrialGasMult ; + +* -- adding in $ / metric ton adder for transport and storage and h2 vom cost +parameter + h2_stor_tran(i,t) "--$ per metric ton-- adder for the cost of hydrogen transport and storage" + h2_vom(i,t) "--$ per metric ton-- variable cost of hydrogen production" +; + +* h2_stor_tran cost applies if running hydrogen nationally (Sw_H2=1) +* if running regionally (Sw_H2=2) the costs are endogenized in the h2 network +h2_stor_tran(i,t)$[Sw_H2=1] = deflator("2016") * consume_char0(i,t,"stortran_adder") ; + +* option to apply a uniform H2 storage/transport cost that does not vary by tech or year +* note that this overrides input values from the consume_char input file +h2_stor_tran(i,t)$[(Sw_H2=1)$Sw_H2_TransportUniform$h2(i)$sum{(v,r), valcap(i,v,r,t) }] = Sw_H2_TransportUniform ; + +* multiply vom by 1000 because input costs are in $/kg +h2_vom(i,t)$h2(i) = deflator("2016") * consume_char0(i,t,"vom") * 1000 ; + +* total cost of h2 production activities ($ per metric ton) +cost_prod(i,v,r,t)$[h2(i)$valcap(i,v,r,t)] = h2_fuel_cost(i,v,r,t) + h2_vom(i,t) + h2_stor_tran(i,t) ; + +* include VOM for DAC in cost_prod +cost_prod(i,v,r,t)$[dac(i)$valcap(i,v,r,t)] = consume_char0(i,t,"vom") ; + + +table gasquant_elec(cendiv,allt) "--Quads-- Natural gas consumption in the electricity sector" +$offlisting +$ondelim +$include inputs_case%ds%ng_demand_elec.csv +$offdelim +$onlisting +; + +table gasquant_tot(cendiv,allt) "--Quads-- Total natural gas consumption" +$offlisting +$ondelim +$include inputs_case%ds%ng_demand_tot.csv +$offdelim +$onlisting +; + +*need to convert from quadrillion btu to million btu +gassupply_ele(cendiv,t) = 1e9 * gasquant_elec(cendiv,t) ; +gassupply_tot(cendiv,t) = 1e9 * gasquant_tot(cendiv,t) ; + + +parameter +gassupply_ele_nat(t) "--quads-- national reference gas supply for electricity " , +gasprice_nat(t) "--$/MMBtu-- national NG price", +gasquant_nat(t) "--quads-- national NG usage", +gasquant_nat_bin(gb,t) "--quads-- national NG quantity by bin", +gasprice_nat_bin(gb,t) "--$/MMbtu-- price for each national NG bin", +gaslimit_nat(gb,t) "--MMbtu-- national gas bin limit" ; + +gassupply_ele_nat(t) = sum{cendiv$gassupply_ele(cendiv,t), gassupply_ele(cendiv,t) } ; + +gasprice_nat(t) = sum{cendiv$gassupply_ele(cendiv,t), gassupply_ele(cendiv,t) * gasprice_ref(cendiv,t) } + / gassupply_ele_nat(t) ; + +*now compute the amounts going into each gas bin +*this is computed as the amount relative to the reference amount based on the ordinal of the +*gas bin - e.g. gas bin 4 (with a central gas bin of 6 and bin width of 0.1) +*will be gassupply_ele * (1+4-6*0.1) = 0.8 * reference +gasquant(cendiv,gb,t)$gassupply_ele(cendiv,t) = gassupply_ele(cendiv,t) * + (1+(ord(gb)-(smax(gbb,ord(gbb)) / 2 + 0.5)) * 0.1) ; + + +gasquant_nat_bin(gb,t)$gassupply_ele_nat(t) = gassupply_ele_nat(t) * + (1+(ord(gb)-(smax(gbb,ord(gbb)) / 2 + 0.5)) * 0.1) ; + + +gasprice(cendiv,gb,t)$gassupply_ele(cendiv,t) = + gas_scale * round(gasprice_ref(cendiv,t) * + ( +* numerator is the quantity in the bin +* [plus] all natural gas usage +* [minus] gas usage in the ele sector + (gasquant(cendiv,gb,t) + gassupply_tot(cendiv,t) - gassupply_ele(cendiv,t)) + /(gassupply_tot(cendiv,t)) + ) ** (1 / gas_elasticity),4) ; + + +gasprice_nat_bin(gb,t)$sum{cendiv, gassupply_tot(cendiv,t) } = + gas_scale * round(gasprice_nat(t) * + ( + (gasquant_nat_bin(gb,t) + sum{cendiv, gassupply_tot(cendiv,t) } - gassupply_ele_nat(t)) + /(sum{cendiv, gassupply_tot(cendiv,t) }) + ) ** (1 / gas_elasticity),4) ; + + +*the quantity available in each bin is the quantity on the supply curve minus the previous bin's quantity supplied +gaslimit(cendiv,gb,t) = round((gasquant(cendiv,gb,t) - gasquant(cendiv,gb-1,t)),0) / gas_scale; + + +gaslimit(cendiv,"gb1",t) = gaslimit(cendiv,"gb1",t) + - gassupplyscale * sum{gb$[ord(gb)=(smax(gbb,ord(gbb)) / 2 + 0.5)],gaslimit(cendiv,gb,t) } ; + +*final category gets a huge bonus so we make sure we do not run out of gas +gaslimit(cendiv,gb,t)$[ord(gb)=smax(gbb,ord(gbb))] = 5 * gaslimit(cendiv,gb,t) ; + + +gaslimit_nat(gb,t) = round((gasquant_nat_bin(gb,t) - gasquant_nat_bin(gb-1,t)),0) / gas_scale; + +gaslimit_nat("gb1",t) = gaslimit_nat("gb1",t) + - gassupplyscale * sum{gb$[ord(gb)=(smax(gbb,ord(gbb)) / 2 + 0.5)],gaslimit_nat(gb,t) } ; + +*final category gets a huge bonus so we make sure we do not run out of gas +gaslimit_nat(gb,t)$(ord(gb)=smax(gbb,ord(gbb))) = 5 * gaslimit_nat(gb,t) ; + +*Penalizing new gas built within cost recovery period of 30 years for states that +* require fossil plants to retire in some future model period. +* This value is calculated as the ratio of CRF_X / CRF_30 where X is the number of +* years until the required retirement year. +$onempty +parameter ng_crf_penalty_st(allt,st) "--unitless-- cost adjustment for NG in states where all NG techs must be retired by a certain year" +/ +$offlisting +$ondelim +$include inputs_case%ds%ng_crf_penalty_st.csv +$offdelim +$onlisting +/ ; +$offempty + +parameter ng_carb_lifetime_cost_adjust(allt) "--unitless-- cost adjustment for NG with full-region zero-carbon policy" +/ +$offlisting +$ondelim +$include inputs_case%ds%ng_crf_penalty.csv +$offdelim +$onlisting +/ ; + +parameter ng_crf_penalty_nat(i,t) "--unitless-- cost adjustment for NG techs that must be retired by a certain year" ; +* Penalize new gas that can be upgraded to recover upgrade costs prior to upgrade within 20 years of a zero-carbon policy +ng_crf_penalty_nat(i,t)$[gas(i)$sum{r, valcap_irt(i,r,t) }] = ((ng_carb_lifetime_cost_adjust(t) - 1) * .2) + 1 ; +* Do not apply the penalty to CCS technologies +ng_crf_penalty_nat(i,t)$[gas(i)$ccs(i)$sum{r, valcap_irt(i,r,t) }] = 1 ; + +*=========================================== +* --- Regional Gas supply curve --- +*=========================================== + +set fuelbin "gas usage bracket" +/ +$offlisting +$include inputs_case%ds%fuelbin.csv +$onlisting +/ ; + +alias(fuelbin,afuelbin) ; + +Scalar numfuelbins "number of fuel bins", + normfuelbinwidth "typical fuel bin width", + botfuelbinwidth "bottom fuel bin width" +; + +parameter cd_beta(cendiv,t) "--$/MMBtu per Quad-- beta value for census divisions' natural gas supply curves", + nat_beta(t) "--$/MMBtu per Quad-- beta value for national natural gas supply curves", + gasbinwidth_regional(fuelbin,cendiv,t) "--MMBtu-- census division's gas bin width", + gasbinwidth_national(fuelbin,t) "--MMBtu-- national gas bin width", + gasbinp_regional(fuelbin,cendiv,t) "--$/MMBtu-- price for each gas bin", + gasusage_national(t) "--MMBtu-- reference national gas usage", + gasbinqq_regional(fuelbin,cendiv,t) "--MMBtu-- regional reference level for supply curve calculation of each gas bin", + gasbinqq_national(fuelbin,t) "--MMBtu-- national reference level for supply curve calculation of each gas bin", + gasbinp_national(fuelbin,t) "--$/MMBtu--price for each national gas bin", + gasmultterm(cendiv,t) "parameter to be multiplied by total gas usage to compute the reference costs of gas consumption, from which the bins deviate" ; + +*note these do not change over years, only exception +* is that the value in the first year is set to zero +parameter cd_beta0(cendiv) "--$/MMBtu per Quad-- reference census division beta levels electric sector" +/ +$offlisting +$ondelim +$include inputs_case%ds%cd_beta0.csv +$offdelim +$onlisting +/ ; + +parameter cd_beta0_allsector(cendiv) "--$/MMBtu per Quad-- reference census division beta levels all sectors" +/ +$offlisting +$ondelim +$include inputs_case%ds%cd_beta0_allsector.csv +$offdelim +$onlisting +/ ; + +$ifthen.gassector %GSw_GasSector% == 'energy_sector' + +*beginning year value is zero (i.e., no elasticity) +cd_beta(cendiv,t)$[not tfirst(t)] = cd_beta0_allsector(cendiv) ; + +nat_beta(t)$(not tfirst(t)) = nat_beta_energy ; + +$else.gassector + +*beginning year value is zero (i.e., no elasticity) +cd_beta(cendiv,t)$[not tfirst(t)] = cd_beta0(cendiv) ; + +*see documentation for how value is calculated +nat_beta(t)$(not tfirst(t)) = nat_beta_nonenergy ; + +$endif.gassector + +* Written by reeds/inputs\fuelcostprep.py +* declared over allt to allow for external data files that extend beyond end_year +table cd_alpha(allt,cendiv) "--$/MMBtu-- alpha value for natural gas supply curves" +$offlisting +$ondelim +$include inputs_case%ds%alpha.csv +$offdelim +$onlisting +; + +table cendiv_weights(r,cendiv) "--unitless-- weights to smooth gas prices between census regions to avoid abrupt price changes at the cendiv borders" +$offlisting +$ondelim +$include inputs_case%ds%cendivweights.csv +$offdelim +$onlisting +; + + +*number of fuel bins is just the sum of fuel bins +numfuelbins = sum{fuelbin, 1} ; + +*note we subtract two here because top and bottom bins are not included +normfuelbinwidth = (normfuelbinmax - normfuelbinmin)/(numfuelbins - 2) ; + +*set the bottom fuel bin width +botfuelbinwidth = normfuelbinmin ; + +*national gas usage computed as sum over census divisions' gas usage +gasusage_national(t) = sum{cendiv, gassupply_ele(cendiv,t) } ; + +*gas bin width is typically the reference gas usage times the bin width +gasbinwidth_regional(fuelbin,cendiv,t) = gassupply_ele(cendiv,t) * normfuelbinwidth ; + +*bottom and top bins get special treatment +*in that they are expanded by botfuelbinwidth and topfuelbinwidth +gasbinwidth_regional(fuelbin,cendiv,t)$[ord(fuelbin) = 1] = gassupply_ele(cendiv,t) * botfuelbinwidth ; +gasbinwidth_regional(fuelbin,cendiv,t)$[ord(fuelbin) = smax(afuelbin,ord(afuelbin))] = + gassupply_ele(cendiv,t) * topfuelbinwidth ; + +*don't want any super small or zero values -- this follows the same calculations in heritage ReEDS +gasbinwidth_regional(fuelbin,cendiv,t)$[gasbinwidth_regional(fuelbin,cendiv,t) < 10] = 10 ; + +*gas bin widths are defined similarly on the national level +gasbinwidth_national(fuelbin,t) = gasusage_national(t) * normfuelbinwidth ; +gasbinwidth_national(fuelbin,t)$[ord(fuelbin) = 1] = gasusage_national(t) * botfuelbinwidth ; +gasbinwidth_national(fuelbin,t)$[ord(fuelbin)=smax(afuelbin,ord(afuelbin))] = gasusage_national(t) * topfuelbinwidth ; + +*comment from heritage reeds: +*gasbinqq is the centerpoint of each of the smaller bins and is used to determine the price of each bin. The first and last bin have +*gasbinqqs that are just one more step before and after the smaller bins. +gasbinqq_regional(fuelbin,cendiv,t) = + gassupply_ele(cendiv,t) * (normfuelbinmin + + (ord(fuelbin) - 1)*normfuelbinwidth - normfuelbinwidth / 2) ; + +gasbinqq_national(fuelbin,t) = gasusage_national(t) * (normfuelbinmin + (ord(fuelbin) - 1)*normfuelbinwidth - normfuelbinwidth / 2) ; + +*bins' prices are those from the supply curves +*1e9 converts from MMBtu to Quads +gasbinp_regional(fuelbin,cendiv,t) = + round((cd_beta(cendiv,t) * (gasbinqq_regional(fuelbin,cendiv,t) - gassupply_ele(cendiv,t))) / 1e9,5) ; + +gasbinp_national(fuelbin,t)= round(nat_beta(t)*(gasbinqq_national(fuelbin,t) - gasusage_national(t)) / 1e9,5) ; + + +*this is the reference price of gas given last year's gas usage levels +gasmultterm(cendiv,t) = (cd_alpha(t,cendiv) + + nat_beta(t) * gasusage_national(t-2) / 1e9 + + cd_beta(cendiv,t) * gassupply_ele(cendiv,t-2) / 1e9 + ) ; + + + +*================================= +* ---- Storage ---- +*================================= + +* --- Storage Efficiency --- + +parameter storage_eff(i,t) "--fraction-- round-trip efficiency of storage technologies" ; + +storage_eff(i,t)$storage(i) = 1 ; +storage_eff(i,t)$psh(i) = storage_eff_psh ; +storage_eff(i,t)$[storage(i)$plant_char0(i,t,'rte')] = plant_char0(i,t,'rte') ; +storage_eff(i,t)$[evmc_storage(i)$plant_char0(i,t,'rte')] = plant_char0(i,t,'rte') ; +storage_eff(i,t)$pvb(i) = storage_eff("battery_li",t) ; + +parameter storage_eff_pvb_p(i,t) "--fraction-- efficiency of hybrid PV+battery when charging from the coupled PV" + storage_eff_pvb_g(i,t) "--fraction-- efficiency of hybrid PV+battery when charging from the grid" ; + +*when charging from PV the pvb system will have a higher efficiency due to one less inverter conversion +storage_eff_pvb_p(i,t)$pvb(i) = storage_eff(i,t) / inverter_efficiency ; +*when charging from the grid the efficiency will be the same as standalone storage +storage_eff_pvb_g(i,t)$pvb(i) = storage_eff("battery_li",t) ; + +*upgrade plants assume the same as what theyre upgraded to +storage_eff(i,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), storage_eff(ii,t) } ; + +* --- Storage Input Capacity --- + +parameter minstorfrac(i,v,r) "--fraction-- minimum storage_in as a fraction of total input capacity"; +minstorfrac(i,v,r)$[valcap_ivr(i,v,r)$psh(i)] = %GSw_HydroStorInMinLoad% ; +* Expand for water technologies +minstorfrac(i,v,r)$[i_water_cooling(i)$valcap_ivr(i,v,r)$psh(i)$Sw_WaterMain] + = sum{ii$ctt_i_ii(i,ii), minstorfrac(ii,v,r) } ; + +parameter storinmaxfrac(i,v,r) "--fraction-- max storage input capacity as a fraction of output capacity" ; + +$ifthen.storcap %GSw_HydroStorInMaxFrac% == "data" +$onempty +parameter storinmaxfrac_data(i,v,r) "--fraction-- data for max storage input capacity as a fraction of capacity if data is available" +/ +$offlisting +$ondelim +$ifthen.readstorinmaxfrac %GSw_Storage% == 1 +$include inputs_case%ds%storinmaxfrac.csv +$endif.readstorinmaxfrac +$offdelim +$onlisting +/ ; +$offempty +* Use data file for available PSH data +storinmaxfrac(i,v,r)$[valcap_ivr(i,v,r)$psh(i)] = storinmaxfrac_data(i,v,r) ; +$else.storcap +* Use numerical value from case file for PSH only +storinmaxfrac(i,v,r)$[valcap_ivr(i,v,r)$psh(i)] = %GSw_HydroStorInMaxFrac% ; +$endif.storcap +* Fill any gaps with values of 1 +storinmaxfrac(i,v,r)$[(storage_standalone(i) or hyd_add_pump(i))$(not storinmaxfrac(i,v,r))$valcap_ivr(i,v,r)] = 1 ; + +* --- Hybrid PV+Battery --- + +table pvbcapmult(allt,pvb_config) "PV+Battery capital cost multipliers over time" +$offlisting +$ondelim +$include inputs_case%ds%pvbcapcostmult.csv +$offdelim +$onlisting +; + +* the capital cost for PVB includes both the PV and battery portions +* total cost = cost(PV) * cap(PV) + cost(B) * cap(B) +* = cost(PV) * cap(PV) + cost(B) * bcr * cap(PV) +* = [cost(PV) + cost(B) * bcr ] * cap(PV) +cost_cap(i,t)$pvb(i) = (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) * sum{pvb_config$pvb_agg(pvb_config,i), pvbcapmult(t,pvb_config) } ; + +scalar pvb_itc_qual_frac "--fraction-- fraction of energy that must be charged from local PV for hybrid PV+battery" ; +pvb_itc_qual_frac = %GSw_PVB_Charge_Constraint% ; + +* --- CSP with storage --- + +* used in eq_rsc_INVlim +* csp: this include the SM for representative configurations, divided by the representative SM (2.4) for CSP supply curve; +* all other technologies are 1 +parameter + csp_sm(i) "--unitless-- solar multiple for configurations" + resourcescaler(i) "--unitless-- resource scaler for rsc technologies" +; + +csp_sm(i)$csp1(i) = csp_sm_1 ; +csp_sm(i)$csp2(i) = csp_sm_2 ; +csp_sm(i)$csp3(i) = csp_sm_3 ; +csp_sm(i)$csp4(i) = csp_sm_4 ; + +resourcescaler(i)$[(not CSP_Storage(i))$(not ban(i))] = 1 ; +resourcescaler(i)$csp(i) = CSP_SM(i) / csp_sm_baseline ; + +* --- Storage Duration --- + +* For PSH, tech-specific storage duration sets a default value. +* Then when when GSw_HydroPSHDurData = 1, +* region- and vintage-specific durations are defined where data exists. +parameter storage_duration(i) "--hours-- storage duration by tech" +/ +$offlisting +$ondelim +$include inputs_case%ds%storage_duration.csv +$offdelim +$onlisting +/ ; + +$onempty +scalar psh_sc_duration "--hours-- PSH storage duration corresponding to selected supply curve" +/ +$offlisting +$ifthene.readpshscduration %GSw_Storage%<>0 +$include inputs_case%ds%psh_sc_duration.csv +$endif.readpshscduration +$onlisting +/ ; +$offempty + +* Note that this PSH duration overwrites what is contained in storage_duration.csv +storage_duration(i)$psh(i) = psh_sc_duration ; + +storage_duration(i)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), storage_duration(ii) } ; + +storage_duration(i)$pvb(i) = %GSw_PVB_Dur% ; + +*upgrade plants assume the same as what they're upgraded to +storage_duration(i)$upgrade(i) = sum{ii$upgrade_to(i,ii),storage_duration(ii) } ; + +parameter storage_duration_m(i,v,r) "--hours-- storage duration by tech, vintage, and region" + cc_storage(i,sdbin) "--fraction-- capacity credit of storage by duration" + bin_duration(sdbin) "--hours-- duration of each storage duration bin" + bin_penalty(sdbin) "--$-- penalty to incentivize solve to fill the shorter duration bins first" +; +$onempty +parameter storage_duration_pshdata(i,v,r) "--hours-- storage duration data for PSH" +/ +$offlisting +$ondelim +$ifthene.readpshstorageduration ((%GSw_Storage%=1)and(%GSw_HydroPSHDurData%=1)) +$include inputs_case%ds%storage_duration_pshdata.csv +$endif.readpshstorageduration +$offdelim +$onlisting +/ ; +$offempty + +* Initialize using generic tech-specific duration +storage_duration_m(i,v,r)$[storage_duration(i)$valcap_ivr(i,v,r)] = storage_duration(i) ; +* Overwrite storage duration for existing PSH capacity when using datafile +$ifthen %GSw_HydroPSHDurData% == 1 +storage_duration_m(i,v,r)$[storage_duration_pshdata(i,v,r)$psh(i)$valcap_ivr(i,v,r)] = storage_duration_pshdata(i,v,r) ; +$endif + +* set the duration of each storage duration bin +bin_duration(sdbin) = sdbin.val ; + +* set the capacity credit of each storage technology for each storage duration bin. +* for example, 2-hour batteries get CC=1 for the 2-hour bin and CC=0.5 for the 4-hour bin +* likewise, 6-hour batteries get CC=1 for the 2-, 4-, and 6-hour bins, but only 0.75 for the 8-hour bin, etc. +* For capacity credit, CSP is treated like VRE rather than storage +cc_storage(i,sdbin)$[(not ban(i))$(not csp(i))] = storage_duration(i) / bin_duration(sdbin) ; +cc_storage(i,sdbin)$(cc_storage(i,sdbin) > 1) = 1 ; + +* for battery, the capacity credit for each bin is always 1, +* since the duration of continuous battery will be automatically greater than the sdbin duration. +cc_storage(i,sdbin)$(battery(i)) = 1 ; + +* The 8760 bin is included as a safety valve so that the model can build additional storage +* beyond what is available for diurnal peaking capacity +cc_storage(i,'8760') = 0 ; + +bin_penalty(sdbin) = 0 ; +bin_penalty(sdbin)$Sw_StorageBinPenalty = 1e-5 * (ord(sdbin) - 1) ; + +*upgrade plants assume the same as what they're upgraded to +cc_storage(i,sdbin)$upgrade(i) = sum{ii$upgrade_to(i,ii), cc_storage(ii,sdbin) } ; + +* --- storage fixed OM cost --- + +*fom and vom costs are constant for pumped-hydro +*values are taken from ATB +cost_fom(i,v,r,t)$[psh(i)$valcap(i,v,r,t)] = cost_fom_psh ; +cost_vom(i,v,r,t)$[psh(i)$valcap(i,v,r,t)] = cost_vom_psh ; + +* Apply a minimum VOM cost for storage (to avoid degeneracy with curtailment) +* Only apply the value to storage that does not have a VOM value +cost_vom(i,v,r,t)$[storage(i)$valgen(i,v,r,t)$(not cost_vom(i,v,r,t))] = storage_vom_min ; + +* --- minimum capacity factor ---- +parameter minCF(i,t) "--fraction-- minimum annual capacity factor for each tech fleet, applied to (i,r)" + maxdailycf(i,t) "--fraction-- maximum daily capacity factor" ; + +* 6% for H2-CT and H2-CC is based on unpublished PLEXOS runs of 100% RE scenarios performed in summer 2019 +parameter minCF_input(i) "--fraction-- minimum annual capacity factor for each tech fleet, applied to (i,r)" +/ +$offlisting +$ondelim +$include inputs_case%ds%minCF.csv +$offdelim +$onlisting +/ ; +minCF(i,t) = minCF_input(i) ; +minCF(i,t)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), minCF(ii,t) } ; +minCF(i,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), minCF(ii,t) } ; + +* adjust fleet mincf for nuclear when using flexible nuclear +minCF(i,t)$[nuclear(i)$Sw_NukeFlex] = minCF_nuclear_flex ; + +parameter maxdailycf_input(i) "--fraction-- maximum daily capacity factor for a technology" +/ +$offlisting +$ondelim +$include inputs_case%ds%maxdailycf.csv +$offdelim +$onlisting +/ ; + +maxdailycf(i,t) = maxdailycf_input(i) ; +maxdailycf(i,t)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), maxdailycf(ii,t) } ; +maxdailycf(i,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), maxdailycf(ii,t) } ; + +*================================= +* ---- Upgrades ---- +*================================= +*The last instance of cost_cap has already occurred, so now assign upgrade costs + +*costs for upgrading are the difference in capital costs +*between the initial techs and the tech to which the unit is upgraded +cost_upgrade(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)] = sum{ii$upgrade_to(i,ii), cost_cap(ii,t) } + - sum{ii$upgrade_from(i,ii), cost_cap(ii,t) } ; + +*increase cost_upgrade by 1% to prevent building and upgrading in the same year +*(otherwise there is a degeneracy between building new and building+upgrading in the same year) +cost_upgrade(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)] = cost_upgrade(i,v,r,t) * 1.01 ; + +*Sets upgrade costs for H2-CT and H2-CC plants based relative to capital cost for H2-CT +*this is done because upgrade costs are higher than new build costs +cost_upgrade('Gas-CT_H2-CT',v,r,t)$[valcap('Gas-CT_H2-CT',v,r,t)] = + cost_cap('gas-ct',t) * cost_upgrade_gasct2h2ct ; + +*(The set of filters on cost_upgrades yields "Gas-CC_H2-CC", but does so in a way to capture +*water techs when the water switch is turned on) +cost_upgrade(i,v,r,t)$[h2_combustion(i)$upgrade(i)$(not ccs(i))$valcap(i,v,r,t) + $sum{ii$gas_cc(ii), upgrade_from(i,ii) }] = + cost_cap('gas-cc',t) * cost_upgrade_gascc2h2cc ; + +*Override any upgrade costs computed above with exogenously specified retrofit costs +cost_upgrade(i,v,r,t)$[upgrade(i)$plant_char0(i,t,"upgradecost")$valcap(i,v,r,t)] + = plant_char0(i,t,"upgradecost") ; + +*the coal-CCS input from ATB 2021 and on is for a pulverized coal plant +*assume that the upgrade cost for coal-IGCC_coal-CCS is the same as for +*coal-new_coal-CCS +cost_upgrade('coal-IGCC_coal-CCS_mod',v,r,t)$valcap('coal-IGCC_coal-CCS_mod',v,r,t) = + cost_upgrade('coal-new_coal-CCS_mod',v,r,t) ; + +cost_upgrade('coal-IGCC_coal-CCS_max',v,r,t)$valcap('coal-IGCC_coal-CCS_max',v,r,t) = + cost_upgrade('coal-new_coal-CCS_max',v,r,t) ; + +* Assign upgrade costs for hydro technology upgrades using values from cases file +cost_upgrade('hydEND_hydED',v,r,t)$valcap('hydEND_hydED',v,r,t) = %GSw_HydroCostAddDispatch% ; +cost_upgrade('hydED_pumped-hydro',v,r,t)$valcap('hydED_pumped-hydro',v,r,t) = %GSw_HydroCostAddPump% ; +cost_upgrade('hydED_pumped-hydro-flex',v,r,t)$valcap('hydED_pumped-hydro-flex',v,r,t) = %GSw_HydroCostAddPump% ; + +parameter ccs_upgrade_costs_coal(allt) "--$2004/kW-- CCS upgrade costs for coal techs" +/ +$offlisting +$ondelim +$include inputs_case/upgrade_costs_ccs_coal.csv +$offdelim +$onlisting +/ ; + +parameter ccs_upgrade_costs_gas(allt) "--$2004/kW-- CCS upgrade costs for gas techs" +/ +$offlisting +$ondelim +$include inputs_case/upgrade_costs_ccs_gas.csv +$offdelim +$onlisting +/ ; + +* update ccs retrofit costs with conversion from kw to mw +* based on selected ccs upgrade cost case +cost_upgrade(i,v,r,t)$[upgrade(i)$coal_ccs(i)$valcap(i,v,r,t)] = 1e3 * ccs_upgrade_costs_coal(t) ; +cost_upgrade(i,v,r,t)$[upgrade(i)$gas_cc_ccs(i)$valcap(i,v,r,t)] = 1e3 * ccs_upgrade_costs_gas(t) ; + +* if specified, use the overnight retrofit +* costs specified in the EIA unit database +cost_upgrade(i,v,r,t)$[initv(v)$hintage_data(i,v,r,t,"wCCS_Retro_OvernightCost")$valcap(i,v,r,t)] = +* conversion from $ / kw to $ / mw + upgrade_inflator * 1e3 * hintage_data(i,v,r,t,"wCCS_Retro_OvernightCost") ; + +* set floor on the cost of an upgrade to prevent negative upgrade costs +cost_upgrade(i,v,r,t)$[valcap(i,v,r,t)$upgrade(i)] = max{0, cost_upgrade(i,v,r,t) } ; + + +*============================================================= +* ---------- Cost Adjustment for cost_upgrade Techs +*============================================================= +table upgrade_mult(i,allt) "--fraction-- cost adjustment for cost_upgrade techs" +$offlisting +$ondelim +$include inputs_case%ds%upgrade_mult_final.csv +$offdelim +$onlisting +; + +upgrade_mult(i,t)$[sum{ii$ctt_i_ii(i,ii), upgrade_mult(ii,t) }$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), upgrade_mult(ii,t) } ; + +cost_upgrade(i,v,r,t)$[initv(v)$valcap(i,v,r,t)$sum{ii$upgrade_from(i,ii),cost_upgrade(ii,v,r,t) }$unitspec_upgrades(i)$(not Sw_UpgradeATBCosts)] = + upgrade_mult(i,t) * sum{ii$upgrade_from(i,ii),cost_upgrade(ii,v,r,t) } ; + +* start with specifying upgrade_derate as zero +upgrade_derate(i,v,r,t) = 0 ; + +upgrade_derate(i,initv,r,t)$[upgrade(i)$ccs(i)$unitspec_upgrades(i)$valcap(i,initv,r,t) + $sum{ii$upgrade_from(i,ii),hintage_data(ii,initv,r,t,"wCCS_Retro_HR") }] = +* following calculation is from NEMS/EIA - stating the derate is 1 - [the original heat_rate] / [new heat rate] +* take the max of it and zero + max(0,1 - sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wHR") / hintage_data(ii,initv,r,t,"wCCS_Retro_HR") }); + +* set upgrade derate for new plants and existing plants without data +* to the average across all values from NETL CCRD: +* https://www.osti.gov/servlets/purl/1887588 +upgrade_derate(i,initv,r,t)$[upgrade(i)$ccs(i)$coal(i) + $(not upgrade_derate(i,initv,r,t)) + $valcap(i,initv,r,t)] = 0.29 ; + +upgrade_derate(i,initv,r,t)$[upgrade(i)$ccs(i)$gas(i) + $(not upgrade_derate(i,initv,r,t)) + $valcap(i,initv,r,t)] = 0.14 ; + +* same assumptions for new plants +upgrade_derate(i,newv,r,t)$[upgrade(i)$ccs(i)$coal(i)$valcap(i,newv,r,t)] = 0.29 ; +upgrade_derate(i,newv,r,t)$[upgrade(i)$ccs(i)$gas(i)$valcap(i,newv,r,t)] = 0.14 ; + +* If a technology is not retired, its upgrade_derate value is zero. +upgrade_derate(i,v,r,t)$[upgrade(i)$(not noret_upgrade_tech(i))] = 0 ; + +if((not Sw_UpgradeDerate), + upgrade_derate(i,v,r,t) = 0 +) ; + + +*============================== +* --- BIOMASS SUPPLY CURVES --- +*============================== + +* supply curves defined by 21 price increments +set bioclass +/ +$offlisting +$include inputs_case%ds%bioclass.csv +$onlisting +/ ; + +set biofeas(r) "regions with biomass supply and biopower"; + +* supply curve derived from 2016 ORNL Billion Ton study +* annual supply of woody biomass available to the power sector (in million dry tons) +* by USDA region at price P (2015$ per dry ton) +table biosupply(usda_region,bioclass,*) "biomass supply (million dry tons) and biomass cost ($/dry ton)" +$offlisting +$ondelim +$include inputs_case%ds%bio_supplycurve.csv +$offdelim +$onlisting +; + +* convert biomass supply from million dry tons to MMBtu +* assuming 13 MMBtu per dry ton based on 2016 ORNL Billion Ton Study +scalar bio_energy_content "MMBtu per dry ton of biomass" / 13 / ; +biosupply(usda_region,bioclass,"cap") = biosupply(usda_region, bioclass,"cap") * 1E6 * bio_energy_content ; + +* multiplier for total biomass supply, set by user via input switch (default is 1) +biosupply(usda_region,bioclass,"cap") = biosupply(usda_region,bioclass,"cap") * Sw_BioSupply ; + +* convert price into $ per MMBtu +* input price ($/ton) / (MMBtu/ton) = $/MMBtu +biosupply(usda_region,bioclass,"price") = biosupply(usda_region, bioclass,"price") / bio_energy_content ; + +* regions with biomass supply +biofeas(r)$[sum{bioclass, sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"cap") } } ] = yes ; + +*removal of bio techs that are not in biofeas(r) +valcap(i,v,r,t)$[(cofire(i) or bio(i))$(not biofeas(r))] = no ; +valgen(i,v,r,t)$[(cofire(i) or bio(i))$(not biofeas(r))] = no ; +valinv(i,v,r,t)$[(cofire(i) or bio(i))$(not biofeas(r))] = no ; + +valgen(i,v,r,t)$[not valcap(i,v,r,t)] = no ; +valinv(i,v,r,t)$[not valcap(i,v,r,t)] = no ; + +scalar bio_transport_cost ; +* biomass transport cost enter in $ per ton, convert to $ per MMBtu +bio_transport_cost = Sw_BioTransportCost / bio_energy_content ; + +* get price of cheapest supply curve bin that has resources (needed for Augur) +* price includes any transport costs for biomass +parameter rep_bio_price_unused(r) "--2004$/MWh-- marginal price of lowest cost available supply curve bin for biofuel" ; +rep_bio_price_unused(r)$[sum{usda_region, 1$r_usda(r,usda_region) }] = + smin{bioclass$[sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"cap") }], + sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"price") } } + bio_transport_cost ; + +parameter cost_curt(t) "--$/MWh-- price paid for curtailed VRE" ; + +cost_curt(t)$[yeart(t)>=model_builds_start_yr] = Sw_CurtMarket ; + +*====================== +* Emissions cap and tax +*====================== + +parameter emit_cap(eall,t) "--metric tons per year-- annual CO2 emissions cap", + yearweight(t) "--unitless-- weights applied to each solve year for the banking and borrowing cap - updated in d_solveprep.gms", + emit_tax(e,r,t) "--$ per metric ton-- tax applied to emissions" ; + +emit_cap(e,t) = 0 ; +emit_tax(e,r,t) = 0 ; + +yearweight(t) = 0 ; +yearweight(t)$tmodel_new(t) = sum{tt$tprev(tt,t), yeart(tt) } - yeart(t) ; +yearweight(t)$tlast(t) = 1 + smax{yearafter, yearafter.val } ; + +* declared over allt to allow for external data files that extend beyond end_year +parameter co2_cap(allt) "--metric tons-- CO2 emissions cap used when Sw_AnnualCap is on" +/ +$offlisting +$ondelim +$include inputs_case%ds%co2_cap.csv +$offdelim +$onlisting +/ ; +if(Sw_AnnualCap = 1, + emit_cap("CO2",t) = co2_cap(t) ; +) ; + +if(Sw_AnnualCap > 1, + emit_cap("CO2e",t) = co2_cap(t) ; +) ; + +parameter co2_tax(allt) "--$/metric ton-- CO2 tax used when Sw_CarbTax is on" +/ +$offlisting +$ondelim +$include inputs_case%ds%co2_tax.csv +$offdelim +$onlisting +/ ; + + +* set the carbon tax based on switch arguments +if(Sw_CarbTax = 1, +emit_tax("CO2",r,t) = co2_tax(t) ; +) ; + +* All emissions are included in reported, but not all emissions need to be modeled +* This set captures only the emissions that need to be included in the model +set emit_modeled(e,r,t) "set of emissions that are necessary to include in the model" ; + +* CO2 for RGGI regions and years +emit_modeled("CO2",r,t)$[ + (yeart(t)>=RGGI_start_yr) + $RGGI_r(r) + $Sw_RGGI] = yes ; + +* CO2 for state cap regions and years +emit_modeled("CO2",r,t)$[ + (yeart(t)>=state_cap_start_yr) + $sum{(tt,st)$r_st(r,st), state_cap(st,tt) } + $Sw_StateCap] = yes ; + +* Emissions with an emission rate limit constraint +emit_modeled(e,r,t)$emit_rate_con(e,r,t) = yes ; + +* Model all emissions with global warming potential +emit_modeled(e,r,t)$gwp(e) = yes ; + +* Emissions associated with bankbarrowcap +emit_modeled(e,r,t)$[ + Sw_BankBorrowCap + $sum{tt, emit_cap(e,tt) }] = yes ; + +* Emissions subject to an emissions tax +emit_modeled(e,r,t)$emit_tax(e,r,t) = yes ; + +* Remove years not modeled +emit_modeled(e,r,t)$[not tmodel_new(t)] = no ; + +* Set of emissions that are being capped +set emit_capped(e) "set of emissions that are included in emission cap depending on Sw_AnnualCap setting" ; +emit_capped(e)$[Sw_AnnualCap=0] = no ; +emit_capped("CO2")$[Sw_AnnualCap=1] = yes ; +emit_capped(e)$[gwp(e)$(not sameas(e, "H2"))$(Sw_AnnualCap=2)] = yes ; +emit_capped(e)$[gwp(e)$(Sw_AnnualCap=3)] = yes ; + +*==================================== +* --- Endogenous Retirements --- +*==================================== + +set valret(i,v) "technologies and classes that can be retired" ; + +set noretire(i) "technologies that will never be retired" +/ +$offlisting +$ondelim +$include inputs_case%ds%noretire.csv +$offdelim +$onlisting +/ ; + +* storage technologies are not appropriately attributing capacity value to CAP variable +* therefore not allowing them to endogenously retire +noretire(i)$[(storage_standalone(i) or hyd_add_pump(i))] = yes ; + +*all existings plants of any technology can be retired if Sw_Retire = 1 +valret(i,v)$[(Sw_Retire=1)$initv(v)$(not noretire(i))] = yes ; + +*only existing coal and gas are retirable if Sw_Retire = 2 +valret(i,v)$[(Sw_Retire=2)$initv(v)$(not noretire(i)) + $(coal(i) or gas(i) or ogs(i))] = yes ; + +*All new and existing nuclear, coal, gas, and hydrogen are retirable if Sw_Retire = 3 +*Existing plants have to meet the min_retire_age before retiring +valret(i,v)$[((Sw_Retire=3) or (Sw_Retire=5))$(not noretire(i)) + $(coal(i) or gas(i) or nuclear(i) or ogs(i) or h2_combustion(i) or h2(i))] = yes ; + +*new and existings plants of any technology can be retired if Sw_Retire = 4 +valret(i,v)$[(Sw_Retire=4)$(not noretire(i))] = yes ; + +retiretech(i,v,r,t)$[valret(i,v)$valcap(i,v,r,t)] = yes ; + +* when Sw_Retire = 3 ensure that plants do not retire before their minimum age +retiretech(i,v,r,t)$[((Sw_Retire=3) or (Sw_Retire=5))$initv(v)$(not noretire(i))$(plant_age(i,v,r,t) <= min_retire_age(i)) + $(coal(i) or gas(i) or nuclear(i) or ogs(i) or h2_combustion(i) or h2(i))] = no ; + +* for sw_retire=5, don't allow nuclear to retire until 2030 +retiretech(i,v,r,t)$[(Sw_Retire=5)$nuclear(i)$(yeart(t)<=2030)] = no ; + +*several states have subsidies for nuclear power, so do not allow nuclear to retire in these states +*before the year specified (see https://www.eia.gov/todayinenergy/detail.php?id=41534) +*Note that Ohio has since repealed their nuclear subsidy, so is no longer included +$onempty +parameter nuclear_subsidies(st) '--year-- the year a nuclear subsidy ends in a given state' +/ +$offlisting +$ondelim +$include inputs_case%ds%nuclear_subsidies.csv +$offdelim +$onlisting +/ +; +$offempty + +retiretech(i,initv,r,t)$[(yeart(t) < sum{st$r_st(r,st), nuclear_subsidies(st) })$valcap(i,initv,r,t)$nuclear(i)] = no ; + +* if Sw_NukeNoRetire is enabled, don't allow nuclear to retire through Sw_NukeNoRetireYear +if(Sw_NukeNoRetire = 1, + retiretech(i,v,r,t)$[nuclear(i)$(yeart(t)<=Sw_NukeNoRetireYear)] = no ; +) ; + + +*Do not allow retirements before they are allowed +retiretech(i,v,r,t)$[(yeart(t)=Sw_Upgradeyear)$(yeart(t)>=Sw_Retireyear)$(Sw_Upgrades = 2) + $sum{ii$[upgrade_from(ii,i)$valcap(ii,v,r,t)], 1 }] = yes ; + +*========================================= +* BEGIN MODEL SPECIFIC PARAMETER CREATION +*========================================= + +parameter m_rsc_dat(r,i,rscbin,sc_cat) "--MW or $/MW-- resource supply curve attributes" ; + +m_rsc_dat(r,i,rscbin,sc_cat) + $[sum{(ii,t)$[rsc_agg(i,ii)$tmodel_new(t)], valcap_irt(ii,r,t) }] + = rsc_dat(i,r,sc_cat,rscbin) ; + +parameter m_rsc_dat_original(r,i,rscbin,sc_cat) "--MW or $/MW-- resource supply curve attributes before any adjustments" ; +*m_rsc_dat_original is used to compare the magnitude of possible adjustments in supply curves. +*It is only used for model validation and debugging purposes. +m_rsc_dat_original(r,i,rscbin,sc_cat) = m_rsc_dat(r,i,rscbin,sc_cat) ; + +*========================================= +* Reduced Resource Switch +*========================================= + +parameter rsc_reduct_frac(pcat,r) "--unitless-- fraction of renewable resource that is reduced from the supply curve" + prescrip_rsc_frac(pcat,r) "--unitless-- fraction of prescribed builds to the resource available" + rsc_capacity_scalar(i,r,t) "--unitless-- resource scalar for any technology that has a change in the supply curve capacity over time" +; + +set rsc_capacity_scalar_i(i) "technologies that have a capacity resource scalar" ; + +rsc_reduct_frac(pcat,r) = 0 ; +prescrip_rsc_frac(pcat,r) = 0 ; +rsc_capacity_scalar(i,r,t) = 0 ; +rsc_capacity_scalar_i(i) = no ; + +* if the Sw_ReducedResource is on, reduce the available resource by reduced_resource_frac +if (Sw_ReducedResource = 1, +*Calculate the fraction of prescribed builds to the available resource +* 2021-05-05 the prescriptions are being applied across all years until we decide a better way to do this + prescrip_rsc_frac(pcat,r)$[sum{(i,rscbin)$prescriptivelink(pcat,i), m_rsc_dat(r,i,rscbin,"cap") } > 0] = + smax(tt,m_required_prescriptions(pcat,r,tt)) / sum{(i,rscbin)$prescriptivelink(pcat,i), m_rsc_dat(r,i,rscbin,"cap") } ; +*Set the default resource reduction fraction + rsc_reduct_frac(pcat,r) = reduced_resource_frac ; +*If the resource reduction fraction will reduce the resource to the point that prescribed builds will be infeasible, +*then replace the resource reduction fraction with the maximum that the resource can be reduced to still have a feasible solution + rsc_reduct_frac(pcat,r)$[prescrip_rsc_frac(pcat,r) > (1 - rsc_reduct_frac(pcat,r))] = 1 - prescrip_rsc_frac(pcat,r) ; + +*In order to avoid small number issues, round down at the 3rd decimal place +*Because the floor function returns an integer, we multiply and divide by 1000 to get proper rounding + rsc_reduct_frac(pcat,r) = rsc_reduct_frac(pcat,r) * 1000 ; + rsc_reduct_frac(pcat,r) = floor(rsc_reduct_frac(pcat,r)) ; + rsc_reduct_frac(pcat,r) = rsc_reduct_frac(pcat,r) / 1000 ; + +*Now reduce the resource by the updated resource reduction fraction +*(only do this for hydro, geothermal, PSH, and CSP; PV and wind have limited resource supply curves) + m_rsc_dat(r,i,rscbin,"cap")$[rsc_i(i)$(csp(i) or hydro(i) or psh(i) or geo(i))] = + m_rsc_dat(r,i,rscbin,"cap") * (1 - sum{pcat$prescriptivelink(pcat,i), rsc_reduct_frac(pcat,r) }) ; +) ; + +*Currently only geothermal and dr_shed have supply curve capacities that change over time +rsc_capacity_scalar(i,r,t) = geo_discovery(i,r,t) + dr_shed_capacity_scalar(i,r,t) ; +rsc_capacity_scalar_i(i)$[sum{(r,t), rsc_capacity_scalar(i,r,t) }] = yes ; + +*convert UPV and PVB interconnection costs from $/MW-AC to $/MW-DC using ILR +m_rsc_dat(r,i,rscbin,"cost")$[m_rsc_dat(r,i,rscbin,"cap")$(upv(i) or pvb(i))] = m_rsc_dat(r,i,rscbin,"cost") / ilr(i) ; + +*Fill in cost_trans for outputs. +m_rsc_dat(r,i,rscbin,"cost_trans")$[m_rsc_dat(r,i,rscbin,"cost")$[not sccapcosttech(i)]] = + m_rsc_dat(r,i,rscbin,"cost") - m_rsc_dat(r,i,rscbin,"cost_cap") ; + +*Ensure sufficient resource is available to cover existing capacity rsc_i capacity +m_rsc_dat(r,i,rscbin,"cap")$[rsc_i(i) + $(m_rsc_dat(r,i,rscbin,"cap") * (1$[not rsc_capacity_scalar_i(i)] + sum{t$tfirst(t), rsc_capacity_scalar(i,r,t) }$rsc_capacity_scalar_i(i)) + < sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], capacity_exog_rsc(ii,v,r,rscbin,tt) })] = +*Use ceiling function to three decimal places so that we don't run into infeasibilities due to rounding later on + ceil(1000 * sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], capacity_exog_rsc(ii,v,r,rscbin,tt) + / (1$[not rsc_capacity_scalar_i(ii)] + rsc_capacity_scalar(ii,r,tt)$rsc_capacity_scalar_i(ii)) } ) / 1000 ; + +*Ensure sufficient resource availability to cover prescribed builds +*while considering existing capacity (capacity_exog_rsc) +*and prescribed capacity (noncumulative_prescriptions). + +*Two types of adjustments: +*1- If at least one element of m_rsc_dat(r,i,rscbin,"cap") is nonzero within a technology group (pcat), +* apply a multiplier to all associated i-classes so that the total available capacity +* meets or exceeds prescribed capacity. +*2- If m_rsc_dat(r,i,rscbin,"cap") is zero for all i-classes within the technology group, +* but prescribed capacity exists, assign prescribed capacity to the first bin at zero cost. + +*Define auxiliary parameters to organize the computation +parameter cap_existing(i,r) "--MW-- amount of existing resource supply curve (rsc) capacity in each region" + cap_prescribed(i,r) "--MW-- amount of prescribed (required builds) rsc capacity in each region" + available_supply(i,r) "--MW-- amount of available rsc supply in each region" +; + +*Initialize the available supply to zero +available_supply(i,r) = 0 ; + +*Get existing capacity +cap_existing(i,r)$exog_rsc(i) = sum{(v,t,rscbin)$[tfirst(t)], capacity_exog_rsc(i,v,r,rscbin,t) } ; + +*Get prescribed capacity +cap_prescribed(i,r)$rsc_i(i) = sum{(pcat,t)$[(sameas(pcat,i) or prescriptivelink(pcat,i)) + $tmodel_new(t)], + noncumulative_prescriptions(pcat,r,t) } ; + +*Loop over all regions +loop(r, +*Loop over non-geothermal rsc technologies + loop(i$[rsc_i(i)$sum{(v,t)$newv(v), valcap(i,v,r,t) }$(not prescriptivelink("geothermal",i))], + +*Get total available supply for all ii associated with pcat of i. +*For example, if i = {upv_2}, then ii = {upv_2, upv_3, ...} and pcat = {UPV}. + available_supply(i,r) = sum{(pcat,ii,rscbin)$[prescriptivelink(pcat,i) + $prescriptivelink(pcat,ii)], + m_rsc_dat(r,ii,rscbin,"cap") } ; + +*Apply multiplier if prescribed capacity exceeds available supply + if ([((cap_existing(i,r) + cap_prescribed(i,r)) > available_supply(i,r))$(available_supply(i,r))], + m_rsc_dat(r,ii,rscbin,"cap")$[sum{pcat$(prescriptivelink(pcat,i)$prescriptivelink(pcat,ii)), 1 }] + = m_rsc_dat(r,ii,rscbin,"cap") * ((cap_existing(i,r) + cap_prescribed(i,r)) / available_supply(i,r)) ; + ) ; + +*Assign prescribed capacity to first bin at no cost if no supply is available + if ([(cap_prescribed(i,r) > 0)$(not available_supply(i,r))] , + m_rsc_dat(r,i,"bin1","cap") = cap_prescribed(i,r) ; + ) ; + ) ; +) ; + +*Compute the difference between m_rsc_dat_original and m_rsc_dat +parameter rsc_cap_diff(r,i,rscbin) "--MW or $/MW-- total supply added to m_rsc_dat to adjust for prescriptions" ; +rsc_cap_diff(r,i,rscbin) = m_rsc_dat(r,i,rscbin,"cap") - m_rsc_dat_original(r,i,rscbin,"cap") ; + +*Round up to the nearest 3rd decimal place +m_rsc_dat(r,i,rscbin,"cap")$m_rsc_dat(r,i,rscbin,"cap") = ceil(m_rsc_dat(r,i,rscbin,"cap") * 1000) / 1000 ; + +*Geothermal is not a tech with sameas(i,pcat), so handle it separately here +*Loop over regions that have geothermal prescribed builds +loop(r$sum{(i,t)$[prescriptivelink("geothermal",i)$tmodel_new(t)], noncumulative_prescriptions("geothermal",r,t) }, +*Then loop over eligible geothermal technologies + loop(i$[prescriptivelink("geothermal",i)$sum{(v,t)$newv(v), valcap(i,v,r,t) }$geo_discovery(i,r,"%startyear%")], +*If capacity is insufficient, add enough capacity to make the model feasible +*Use the 2010 geothermal discovery (geo_discovery) rate for the calculation. That will slightly +*overestimate geothermal resource for any prescribed builds happening after the discovery rate +*begins to increase (currently after 2021) + m_rsc_dat(r,i,"bin1","cap")$[((sum{(rscbin), m_rsc_dat(r,i,rscbin,"cap") } * (1$[not geo_hydro(i)] + geo_discovery(i,r,"%startyear%")$geo_hydro(i))) < sum{t$tmodel_new(t), noncumulative_prescriptions("geothermal",r,t) }) + $(1$[not geo_hydro(i)] + geo_discovery(i,r,"%startyear%")$geo_hydro(i))] = + (sum{t$tmodel_new(t), noncumulative_prescriptions("geothermal",r,t) } + - sum{(rscbin), m_rsc_dat(r,i,rscbin,"cap") } + + m_rsc_dat(r,i,"bin1","cap") + ) / (1$[not geo_hydro(i)] + geo_discovery(i,r,"%startyear%")$geo_hydro(i)) ; + break ; + ) ; +) ; + +* * Apply spur-line cost multiplier for relevant technologies +* m_rsc_dat(r,i,rscbin,"cost")$(pv(i) or pvb(i) or wind(i) or csp(i)) = +* m_rsc_dat(r,i,rscbin,"cost") * Sw_SpurCostMult ; +set m_rsc_con(r,i) "set to detect numeraire rsc techs that have capacity value" ; +m_rsc_con(r,i)$sum{rscbin, m_rsc_dat(r,i,rscbin,"cap") } = yes ; + +m_rscfeas(r,i,rscbin) = no ; +m_rscfeas(r,i,rscbin)$m_rsc_dat(r,i,rscbin,"cap") = yes ; +m_rscfeas(r,i,rscbin)$[sum{ii$tg_rsc_cspagg(ii, i),m_rscfeas(r,ii,rscbin) } + $sum{t$tmodel_new(t), valcap_irt(i,r,t) }] = yes ; +m_rscfeas(r,i,rscbin)$[sum{ii$rsc_agg(ii,i),m_rscfeas(r,ii,rscbin) }$sum{t$tmodel_new(t),valcap_irt(i,r,t) }$psh(i)$Sw_WaterMain] = yes ; +m_rsc_dat(r,i,rscbin,sc_cat)$[sum{ii$rsc_agg(ii,i), m_rsc_dat(r,ii,rscbin,sc_cat) } + $sum{t$tmodel_new(t), valcap_irt(i,r,t) } + $(psh(i) or csp(i)) + $Sw_WaterMain] = + sum{ii$rsc_agg(ii,i), m_rsc_dat(r,ii,rscbin,sc_cat) } ; + + +set force_pcat(pcat,t) "conditional to indicate whether the force prescription equation should be active for pcat" ; + +force_pcat(pcat,t)$[yeart(t) < firstyear_pcat(pcat)] = yes ; +force_pcat(pcat,t)$[sum{r, noncumulative_prescriptions(pcat,r,t) }] = yes ; + +*========================================= +* Decoupled Capacity/Energy Upgrades for hydropower +*========================================= +Parameter +cost_cap_up(i,v,r,rscbin,t) "--2004$/MW-- capacity upgrade costs", +cost_ener_up(i,v,r,rscbin,t) "--2004$/MW-- energy upgrade costs.", +cap_cap_up(i,v,r,rscbin,t) "--MW-- capacity of capacity upgrades", +cap_ener_up(i,v,r,rscbin,t) "--MW-- capacity of energy upgrades", +allow_cap_up(i,v,r,rscbin,t) "i, v, r, and t combinations that are allowed for capacity upsizing", +allow_ener_up(i,v,r,rscbin,t) "i, v, r, and t combinations that are allowed for energy upsizing" +; + +* Adjust available capacity and costs for hydropower upgrades using switch input. +m_rsc_dat(r,'hydUD',rscbin,"cap") = m_rsc_dat(r,'hydUD',rscbin,"cap") * %GSw_HydroUpgradeCapMult% ; +m_rsc_dat(r,'hydUND',rscbin,"cap") = m_rsc_dat(r,'hydUND',rscbin,"cap") * %GSw_HydroUpgradeCapMult% ; +m_rsc_dat(r,'hydUD',rscbin,"cost") = m_rsc_dat(r,'hydUD',rscbin,"cost") * %GSw_HydroUpgradeCostMult% ; +m_rsc_dat(r,'hydUND',rscbin,"cost") = m_rsc_dat(r,'hydUND',rscbin,"cost") * %GSw_HydroUpgradeCostMult% ; + +* Use hydropower upgrade supply curves and multiplier from switch input to define decoupled capacity/energy upgrade costs. +cost_cap_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cost") * %GSw_HydroCostFracCapUp% ; +cost_cap_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cost") * %GSw_HydroCostFracCapUp% ; +cost_ener_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cost") * %GSw_HydroCostFracEnerUp% ; +cost_ener_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cost") * %GSw_HydroCostFracEnerUp% ; + +* Initialize available capacity/energy upgrades to zero to avoid double counting if using coupled capacity/energy upgrades. +cap_cap_up(i,v,r,rscbin,t) = 0 ; +cap_ener_up(i,v,r,rscbin,t) = 0 ; + +* If decoupling hydropower capacity/energy upgrades, use upgrade supply curves to define upgrade resource availability. +$ifthene.hydup2 %GSw_HydroCapEnerUpgradeType% == 2 +* Need to re-multiply by 1000 because inclusion of hydUD and hydUND in the ban(i) set with this setting +* prevents correct scaling of hydro costs. +cost_cap_up(i,v,r,rscbin,t)$cost_cap_up(i,v,r,rscbin,t) = cost_cap_up(i,v,r,rscbin,t) * 1000 ; +cost_ener_up(i,v,r,rscbin,t)$cost_ener_up(i,v,r,rscbin,t) = cost_ener_up(i,v,r,rscbin,t) * 1000 ; + +cap_cap_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cap") + hyd_add_upg_cap(r,'hydUD',rscbin,t) ; +cap_cap_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cap") + hyd_add_upg_cap(r,'hydUND',rscbin,t) ; +cap_ener_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cap") + hyd_add_upg_cap(r,'hydUD',rscbin,t) ; +cap_ener_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cap") + hyd_add_upg_cap(r,'hydUND',rscbin,t) ; +$endif.hydup2 + +* Use available decoupled upgrade resource to define sets for allowable decoupled capacity/energy upgrades. +allow_cap_up(i,v,r,rscbin,t)$[valcap(i,v,r,t)$cap_cap_up(i,v,r,rscbin,t)$(t.val>=Sw_UpgradeYear)] = yes ; +allow_ener_up(i,v,r,rscbin,t)$[valcap(i,v,r,t)$cap_ener_up(i,v,r,rscbin,t)$(t.val>=Sw_UpgradeYear)] = yes ; + + +* Track the initial amount of m_rsc_dat capacity to compare in e_report +* We adjust upwards by small amounts given potential for infeasibilities +* in very tiny amounts and thus track the extent of the adjustments +parameter m_rsc_dat_init(r,i,rscbin) "--MW-- Initial amount of resource supply curve capacity to compare with final amounts after adjustments" ; +m_rsc_dat_init(r,i,rscbin)$m_rsc_dat(r,i,rscbin,"cap") = m_rsc_dat(r,i,rscbin,"cap") ; + + +*======================================== +* -- CO2 Capture and Storage Network -- +*======================================== +$onempty +set csfeas(cs) "carbon storage sites with available capacity" + r_cs(r,cs) "mapping from BA to carbon storage sites" +/ +$offlisting +$ondelim +$include inputs_case%ds%r_cs.csv +$offdelim +$onlisting +/ , + co2_routes(r,rr) "set of available inter-ba co2 trade relationships" ; + +parameter co2_storage_limit(cs) "--metric tons-- total cumulative storage capacity per carbon storage site", + co2_injection_limit(cs) "--metric tons/hr-- co2 site injection rate upper bound", + cost_co2_pipeline_cap(r,rr,t) "--$2004/(metric ton-mi/hr)-- capital costs associated with investing in co2 pipeline infrastructure", + cost_co2_pipeline_fom(r,rr,t) "--$2004/((metric ton-mi/hr)-yr)-- FO&M costs associated with maintaining co2 pipeline infrastructure", + cost_co2_stor_bec(cs,t) "--$2004/metric ton-- breakeven cost for storing carbon - CF determined by GSw_CO2_BEC", + cost_co2_spurline_cap(r,cs,t) "--$2004/(metric ton-mi/hr)-- capital costs associated with investing in spur lines to injection sites", + cost_co2_spurline_fom(r,cs,t) "--2004/((metric ton-mi/hr)-yr)-- FO&M costs associated with maintaining co2 spurline infrastructure", + r_cs_distance(r,cs) "--mi-- euclidean distance between BA transmission endpoints and storage formations" +/ +$offlisting +$offdigit +$ondelim +$include inputs_case%ds%r_cs_distance_mi.csv +$offdelim +$ondigit +$onlisting +/ , + min_co2_spurline_distance "--mi-- minimum distance for a spur line (used to provide a floor for pipeline distances in r_cs_distance)" +; +$offempty + +* Wherever BA centroids fall within formation boundaries, assume some average spur line distance to connect a CCS or DAC plant with an injection site +min_co2_spurline_distance = 20 ; +r_cs_distance(r,cs)$[r_cs_distance(r,cs) < min_co2_spurline_distance] = min_co2_spurline_distance ; + +* Assign spurline costs +cost_co2_spurline_cap(r,cs,t)$[r_cs(r,cs)$tmodel_new(t)] = Sw_CO2_spurline_cost * r_cs_distance(r,cs) ; + +* CO2 pipelines can be build between any two adjacent BAs +cost_co2_pipeline_cap(r,rr,t)$[routes_adjacent(r,rr)$tmodel_new(t)] = Sw_CO2_pipeline_cost * pipeline_distance(r,rr) ; +cost_co2_pipeline_fom(r,rr,t)$[routes_adjacent(r,rr)$tmodel_new(t)] = Sw_CO2_pipeline_fom * pipeline_distance(r,rr) ; + +co2_routes(r,rr)$routes_adjacent(r,rr) = yes ; + +$onempty +table co2_char(cs,*) "co2 site characteristics including injection rate limit, total storage limit, and break even cost" +$ondelim +$include inputs_case%ds%co2_site_char.csv +$offdelim +; +$offempty + +*note that original units Mton == 'million tons' +co2_storage_limit(cs) = 1e6 * co2_char(cs,"max_stor_cap") ; +co2_injection_limit(cs) = co2_char(cs,"max_inj_rate") ; +cost_co2_stor_bec(cs,t) = co2_char(cs,"bec_%GSw_CO2_BEC%"); + +* only want to consider storage sites that have both available capacity and injection limits +csfeas(cs)$[co2_storage_limit(cs)$co2_injection_limit(cs)] = yes ; +* only want to consider r_cs pairs which have available capacity +r_cs(r,cs)$[not csfeas(cs)] = no ; + +cost_co2_spurline_fom(r,cs,t)$[r_cs(r,cs)$tmodel_new(t)] = Sw_CO2_spurline_fom * r_cs_distance(r,cs) ; + +cost_co2_pipeline_cap(r,rr,t) = %GSw_CO2_CostAdj% * cost_co2_pipeline_cap(r,rr,t); +cost_co2_pipeline_fom(r,rr,t) = %GSw_CO2_CostAdj% * cost_co2_pipeline_fom(r,rr,t); +cost_co2_stor_bec(cs,t) = %GSw_CO2_CostAdj% * cost_co2_stor_bec(cs,t) ; +cost_co2_spurline_fom(r,cs,t) = %GSw_CO2_CostAdj% * cost_co2_spurline_fom(r,cs,t) ; +cost_co2_spurline_cap(r,cs,t) = %GSw_CO2_CostAdj% * cost_co2_spurline_cap(r,cs,t) ; + + +* Parameter tracking for sequential solve +parameter + m_capacity_exog0(i,v,r,t) "--MW-- original value of m_capacity_exog used to make sure upgraded capacity isnt forced into retirement" + z_rep(t) "--$-- objective function value by year" + z_rep_inv(t) "--$-- investment component of objective function by year" + z_rep_op(t) "--$-- operation component of objective function by year" +; +z_rep_inv(t) = 0 ; +z_rep_op(t) = 0 ; + + +*================================================================================================ +*== h- and szn-dependent sets and parameters (declared here, populated in 2_temporal_params) === +*================================================================================================ + +* allh and allszn need to be populated here so they can be used in c_supplymodel and c_supplyobjective +Set allh "all potentially modeled hours" +/ +$offlisting +$include inputs_case%ds%set_allh.csv +$onlisting +/ ; + +Set allszn "all potentially modeled seasons (used as representative days/weks for hourly resolution)" +/ +$offlisting +$include inputs_case%ds%set_allszn.csv +$onlisting +/ ; + +Set +* Timeslices + h(allh) "representative and stress timeslices" + h_preh(allh, allh) "mapping set between one timeslice and all other timeslices earlier in that period" + h_rep(allh) "representative timeslices" + h_stress(allh) "stress timeslices" + h_t(allh,allt) "representative and stress timeslices by model year" + h_stress_t(allh,allt) "stress timeslices by model year" +* "Seasons" (both seasons and representative days/weks) + szn(allszn) "representative and stress periods" + szn_rep(allszn) "representative periods, or seasons if modeling full year" + szn_stress(allszn) "stress periods" + szn_t(allszn,allt) "representative and stress periods by model year" + szn_stress_t(allszn,allt) "stress periods by model year" + szn_actualszn(allszn,allszn) "mapping from rep periods to actual periods" + actualszn(allszn) "actual periods (each is described by a representative period)" +* Mapping between timeslices and "seasons" + h_szn(allh,allszn) "mapping of hour blocks to seasons" + h_szn_start(allszn,allh) "starting hour of each season" + h_szn_end(allszn,allh) "ending hour of each season" + h_szn_t(allh,allszn,allt) "mapping of hour blocks to seasons by model year" + h_actualszn(allh,allszn) "mapping from rep timeslices to actual periods" + nexth_actualszn(allszn,allh,allszn,allh) "mapping between one timeslice and the next for actual periods (szns)" +* Chronology + nexth(allh,allh) "Mapping set between one timeslice (first) and the following (second)" + starting_hour_nowrap(allh) "Flag for whether allh is the first chronological hour by day type" + final_hour(allh) "Flag for whether allh is the last chronological hour in a day type" + final_hour_nowrap(allh) "Flag for whether allh is the last chronological hour in a day type" + nextszn(allszn,allszn) "Mapping between one actual period (allszn) and the next" + nextpartition(allszn,allszn) "Mapping between one partition (allszn) and the next" +* Peak demand + maxload_szn(r,allh,t,allszn) "hour with highest load within each szn" + h_ccseason_prm(allh,ccseason) "peak-load hour for the entire modeled system by ccseason" +* Operating reserves + opres_periods(allszn) "Periods within which the operating reserve constraint applies" + opres_h(allh) "Timeslices within which the operating reserve constraint applies" + dayhours(allh) "daytime hours, used to limit PV capacity to the daytime hours" +* Demand flexibility + flex_h_corr1(flex_type,allh,allh) "correlation set for hours referenced in flexibility constraints" + flex_h_corr2(flex_type,allh,allh) "correlation set for hours referenced in flexibility constraints" +* Minloading + hour_szn_group(allh,allh) "h and hh in the same season - used in minloading constraint" +; + +Parameter +* Hour/period weighting + hours(allh) "--hours-- number of hours in each time block" + numdays(allszn) "--days-- number of days for each season" + numpartitions(allszn) "--days-- number of partitions for each season in timeseries" + hours_daily(allh) "--hours-- number of hours represented by time-slice 'h' during one day" + numhours_nexth(allh,allh) "--hours-- number of times hh follows h throughout year" +* Mapping to quarters + frac_h_quarter_weights(allh,quarter) "--fraction-- fraction of timeslice associated with each quarter" + frac_h_ccseason_weights(allh,ccseason) "--fraction-- fraction of timeslice associated with each ccseason" + szn_quarter_weights(allszn,quarter) "--fraction-- fraction of season associated with each quarter" + szn_ccseason_weights(allszn,ccseason) "--fraction-- fraction of season associated with each ccseason" +* Capacity factor + cf_rsc(i,v,r,allh,t) "--fraction-- capacity factor for rsc tech - t index included for use in CC/curt calculations" + m_cf(i,v,r,allh,t) "--fraction-- modeled capacity factor" + m_cf_szn(i,v,r,allszn,t) "--fraction-- modeled capacity factor, averaged by season" + cf_in(i,r,allh) "--fraction-- capacity factors for renewable technologies" +* Hydropower + cf_hyd(i,allszn,r,allt) "--fraction-- hydro capacity factors by season and year" + climate_hydro_seasonal(r,allszn,allt) "annual/seasonal nondispatchable hydropower availability" + cap_hyd_szn_adj(i,allszn,r) "--fraction-- seasonal max capacity adjustment for dispatchable hydro" + hydmin(i,r,allszn) "minimum hydro loading factors by season and region" +* Availability (forced and scheduled outage rates) + outage_forced_h(i,r,allh) "--fraction-- forced outage rate" + outage_scheduled_h(i,allh) "--fraction-- scheduled outage rate" + avail(i,r,allh) "--fraction-- fraction of capacity available for generation by hour" + seas_cap_frac_delta(i,v,r,allszn,allt) "--scalar-- fractional change in seasonal capacity compared to summer" +* Demand + load_exog(r,allh,t) "--MW-- busbar load" + load_exog0(r,allh,t) "--MW-- original load by region hour and year - unchanged by demand side" + load_allyear(r,allh,allt) "--MW-- end-use load by region, timeslice, and year" + h2_exogenous_demand_regional(r,p,allh,allt) "--metric tons per hour-- exogenous demand for hydrogen at the BA level" +* Peak demand + peak_static_frac(r,ccseason,t) "--fraction-- fraction of peak demand that is static" + peakdem_static_ccseason(r,ccseason,t) "--MW-- busbar peak demand by ccseason" + peak_ccesason(r,ccseason,allt) "--MW-- end-use peak demand by region, ccseason, year" + peakdem_static_h(r,allh,t) "--MW-- busbar peak demand by timeslice" + peak_h(r,allh,allt) "--MW-- busbar peak demand by timeslice" +* Canada and Mexico demand + canmexload(r,allh) "load for canadian and mexican regions" +* Demand flexibility + flex_frac_load(flex_type,r,allh,allt) + flex_demand_frac(flex_type,r,allh,t) "fraction of load able to be considered flexible" + load_exog_flex(flex_type,r,allh,t) "the amount of exogenous load that is flexible" + load_exog_static(r,allh,t) "the amount of exogenous load that is static" + dr_shed_out(i,r,allh) "--fraction-- dr shed capacity availability" +* EVMC storage + evmc_storage_discharge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be discharged (deferred charging) in each timeslice h" + evmc_storage_charge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be charged (add back deferred charging) in each timeslice h" + evmc_storage_energy_hours(i,r,allh,allt) "--hours-- Allowable EV storage SOC (quantity deferred EV charge) [MWh] divided by nameplate EVMC discharge capacity [MW]" +* EVMC load + evmc_baseline_load(r,allh,allt) "--MW-- baseline electricity load from EV charging by timeslice h and year t" + evmc_shape_load(i,r,allh) "--fraction-- fraction of adopted price-responsive (shaped) EV load added by timeslice" + evmc_shape_gen(i,r,allh) "--fraction-- fraction of adopted price-responsive (shaped) EV load subtracted by timeslice" +* Flexible Canadian imports/exports [Sw_Canada=1] + can_imports_szn(r,allszn,t) "--MWh-- [Sw_Canada=1] seasonal imports from Canada by year" + can_imports_szn_frac(allszn) "--fraction-- [Sw_Canada=1] fraction of annual imports that occur in each season" + can_exports_h(r,allh,t) "--MW-- [Sw_Canada=1] timeslice exports to Canada by year" + can_exports_h_frac(allh) "--fraction-- [Sw_Canada=1] fraction of annual exports by timeslice" +* Resource adequacy + prm_year(r) "--fraction-- planning reserve margin for the current solve year" +* Capacity credit + sdbin_size(ccreg,ccseason,sdbin,t) "--MW-- available power capacity by storage duration bin - used to bin the peaking power capacity contribution of storage by duration" + cc_old(i,r,ccseason,t) "--MW-- capacity credit for existing capacity - used in sequential solve similar to heritage reeds" + cc_mar(i,r,ccseason,t) "--fraction-- cc_mar loading initialized to some reasonable value for the 2010 solve" + cc_int(i,v,r,ccseason,t) "--fraction-- average fractional capacity credit - used in intertemporal solve" + cc_excess(i,r,ccseason,t) "--MW-- this is the excess capacity credit when assuming marginal capacity credit in intertemporal solve" + vre_gen_last_year(r,allh,t) "--MW-- generation from VRE generators in the prior solve year" + hybrid_cc_derate(i,r,ccseason,sdbin,t) "--fraction-- derate factor for hybrid PV+battery storage capacity credit" + m_cc_mar(i,r,ccseason,t) "--fraction-- marginal capacity credit", +* Heuristic climate impacts + trans_cap_delta(allh,allt) "--fraction-- fractional adjustment to transmission capacity from climate heuristics" +* Emissions and policies + h_weight_csapr(allh) "hour weights for CSAPR ozone season constraints" +* Water access + watsa(wst,r,allszn,t) "--fraction-- seasonal distribution factors for new water access by year" + watsa_climate(wst,r,allszn,allt) "--fraction-- time-varying fractional seasonal allocation of water" +* Minloading + minloadfrac(r,i,allh) "--fraction-- minimum loading fraction - final used in model" +* Fossil gas supply curve + gasadder_cd(cendiv,t,allh) "--$/MMbtu-- adder for NG census division" + szn_adj_gas(allh) "--fraction-- seasonal adjustment for gas prices" +; + +alias(allh,allhh,allhhh) ; +alias(h,hh,hhh) ; +alias(allszn,allsznn) ; +alias(actualszn,actualsznn,actualsznnn) ; +alias(szn,sznn) ; + +* Initialize some parameters +sdbin_size(ccreg,ccseason,sdbin,"%startyear%") = 1000 ; +cc_int(i,v,r,ccseason,t) = 0 ; +cc_excess(i,r,ccseason,t) = 0 ; +cc_old(i,r,ccseason,t) = 0 ; +m_cc_mar(i,r,ccseason,t) = 0 ; +hybrid_cc_derate(i,r,ccseason,sdbin,t)$[pvb(i)$valcap_irt(i,r,t)] = 1 ; + +* Trim some of the largest matrices to reduce file sizes +cost_vom(i,v,r,t)$[not valgen(i,v,r,t)] = 0 ; +cost_fom(i,v,r,t)$[not valcap(i,v,r,t)] = 0 ; +heat_rate(i,v,r,t)$[not valgen(i,v,r,t)] = 0 ; +m_capacity_exog(i,v,r,t)$[not valcap(i,v,r,t)] = 0 ; +emit_rate(etype,e,i,v,r,t)$[not valgen(i,v,r,t)] = 0 ; + +*============================================================ +* -- Initial state of parameters that change as model runs -- +*============================================================ + +valinv_init(i,v,r,t) = valinv(i,v,r,t) ; +valcap_init(i,v,r,t) = valcap(i,v,r,t) ; diff --git a/reeds/core/setup/c_model.gms b/reeds/core/setup/c_model.gms new file mode 100644 index 00000000..029d53f1 --- /dev/null +++ b/reeds/core/setup/c_model.gms @@ -0,0 +1,3976 @@ +*Setting the default slash +$setglobal ds \ + +*Change the default slash if in UNIX +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +*======================================== +* -- Supply Side Variable Declaration -- +*======================================== + +positive variables + +* load variable - set equal to load_exog to compute holistic marginal price + LOAD(r,allh,t) "--MW-- busbar load for each balancing region" + FLEX(flex_type,r,allh,t) "--MW-- flexible load shifted to each timeslice" +* PEAK_FLEX(r,ccseason,t) "--MW-- peak busbar load adjustment based on load flexibility" + DROPPED(r,allh,t) "--MW-- dropped load (only allowed before Sw_StartMarkets)" + EXCESS(r,allh,t) "--MW-- excess load (only allowed before Sw_StartMarkets)" + CAP_LOADSITE(r,t) "--MW-- capacity of flexibly sited load" + INV_LOADSITE(r,t) "--MW-- capacity of flexibly sited load installed in model year" + OP_LOADSITE(r,allh,t) "--MW-- operations of flexibly sited load" + +* capacity and investment variables + CAP_SDBIN(i,v,r,ccseason,sdbin,t) "--MW-- generation power capacity by storage duration bin for relevant technologies" + CAP_SDBIN_ENERGY(i,v,r,ccseason,sdbin,t) "--MWh-- generation energy capacity by storage duration bin for relevant technologies" + CAP(i,v,r,t) "--MW-- total generation capacity in MWac (MWdc for PV); PV capacity of hybrid PV+battery; max native, flexible EV load for EVMC" + CAP_ENERGY(i,v,r,t) "--MWh-- battery capacity in terms of energy" + CAP_ABOVE_LIM(tg,r,t) "--MW-- amount of capacity that is deployed above the interconnection queue limits" + CAP_RSC(i,v,r,rscbin,t) "--MW-- total generation capacity in MWac (MWdc for PV) for wind-ons and upv" + GROWTH_BIN(gbin,i,st,t) "--MW-- total new (from INV) generation capacity in each growth bin by state and technology group" + INV(i,v,r,t) "--MW-- generation capacity additions in year t" + INV_ENERGY(i,v,r,t) "--MWh-- generation energy capacity additions in year t" + EXTRA_PRESCRIP(pcat,r,t) "--MW-- builds beyond those prescribed power capacity once allowed in firstyear(pcat) - exceptions for gas-ct, wind-ons, and wind-ofs" + EXTRA_PRESCRIP_ENERGY(pcat,r,t) "--MWh-- builds beyond those prescribed battery energy capacity once allowed in firstyear(pcat)" + INV_CAP_UP(i,v,r,rscbin,t) "--MW-- upsized generation capacity addition in year t" + INV_ENER_UP(i,v,r,rscbin,t) "--MW-- upsized energy addition in year t using capacity factor to convert to capacity units" + INV_REFURB(i,v,r,t) "--MW-- investment in refurbishments of technologies that use a resource supply curve" + INV_RSC(i,v,r,rscbin,t) "--MW-- investment in technologies that use a resource supply curve" + UPGRADES(i,v,r,t) "--MW-- investments in upgraded capacity from ii to i" + UPGRADES_RETIRE(i,v,r,t) "--MW-- upgrades that have been retired - used as a free slack variable in eq_cap_upgrade" + +* The units for all of the operational variables are average MW or MWh/time-slice hours +* generation and storage variables + GEN(i,v,r,allh,t) "--MW-- electricity generation (post-curtailment) in hour h" + GEN_PLANT(i,v,r,allh,t) "--MW-- average plant generation from hybrid generation/storage technologies in hour h" + GEN_STORAGE(i,v,r,allh,t) "--MW-- average generation from hybrid storage technologies in hour h" + STORAGE_IN_PLANT(i,v,r,allh,t) "--MW-- hybrid plant storage charging in hour h that is charging from a coupled technology" + STORAGE_IN_GRID(i,v,r,allh,t) "--MW-- hybrid plant storage charging in hour h that is charging from the grid" + AVAIL_SITE(x,allh,t) "--MW-- available generation from all resources at reV site x" + CURT(r,allh,t) "--MW-- curtailment from vre generators in hour h" + MINGEN(r,allszn,t) "--MW-- minimum generation level in each season" + STORAGE_IN(i,v,r,allh,t) "--MW-- storage charging in hour h that is charging from a given source technology; not used for CSP-TES" + STORAGE_LEVEL(i,v,r,allh,t) "--MWh-- storage level in hour h" + STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) "--MWh-- storage level at hour 0 of the partition" + RAMPUP(i,r,allh,allhh,t) "--MW-- upward change in generation from h to hh" + +* flexible CCS variables + CCSFLEX_POW(i,v,r,allh,t) "--avg MW-- average power consumed for CCS system" + CCSFLEX_POWREQ(i,v,r,allh,t) "--avg MW-- average power requirement for CCS system" + CCSFLEX_STO_STORAGE_LEVEL(i,v,r,allh,t) "--varies-- level of process storage (e.g., chemical solvent) in the CCS system" + CCSFLEX_STO_STORAGE_CAP(i,v,r,t) "--varies-- capacity of process storage (e.g., chemical solvent) in the CCS system" + +* trade variables + FLOW(r,rr,allh,t,trtype) "--MW-- electricity flow on transmission lines in hour h" + OPRES_FLOW(ortype,r,rr,allh,t) "--MW-- interregional trade of operating reserves by operating reserve type" + PRMTRADE(r,rr,trtype,ccseason,t) "--MW-- planning reserve margin capacity traded from r to rr" + +* operating reserve variables + OPRES(ortype,i,v,r,allh,t) "--MW-- operating reserves by type" + +* variable fuel amounts + GASUSED(cendiv,gb,allh,t) "--MMBtu/hour-- total gas used by gas bin", + VGASBINQ_NATIONAL(fuelbin,t) "--MMBtu-- National quantity of gas by bin" + VGASBINQ_REGIONAL(fuelbin,cendiv,t) "--MMBtu-- Regional (census divisions) quantity of gas by bin" + BIOUSED(bioclass,r,t) "--MMBtu-- total biomass used by biomass class" + +* RECS variables + RECS(RPSCat,i,st,ast,t) "--MWh-- renewable energy credits from state st to state ast", + ACP_PURCHASES(RPSCat,st,t) "--MWh-- purchases of ACP credits to meet the RPS constraints", + +* transmission variables + CAPTRAN_ENERGY(r,rr,trtype,t) "--MW-- capacity of transmission for energy trading" + CAPTRAN_PRM(r,rr,trtype,t) "--MW-- capacity of transmission for PRM trading" + CAPTRAN_GRP(transgrp,transgrpp,t) "--MW-- capacity of groups of transmission interfaces" + CAPTRAN_ITL(itlgrp,itlgrpp,t) "--MW-- capacity of groups of transmission interfaces for county and mixed" + INVTRAN(r,rr,trtype,t) "--MW-- investment in transmission capacity (defined for both directions)" + INVTRAN_AC(r,rr,tscbin,t) "--MW-- transmission capacity added to transmission supply curve bin (defined for both directions)" + CAP_CONVERTER(r,t) "--MW-- VSC AC/DC converter capacity" + INV_CONVERTER(r,t) "--MW-- investment in AC/DC converter capacity" + CONVERSION(r,allh,intype,outtype,t) "--MW-- conversion of AC->DC or DC->AC" + CONVERSION_PRM(r,ccseason,intype,outtype,t) "--MW-- planning reserve margin capacity sent through VSC AC/DC converters" + CAP_SPUR(x,t) "--MW-- capacity of spur lines" + INV_SPUR(x,t) "--MW-- investment in spur line capacity" + INV_POI(r,t) "--MW-- investment in new POI capacity (for network reinforcement costs)" + TRAN_CAPEX_BINS(r,rr,tscbin,t) "--$-- transmission capex cost bins (defined for r < rr)" + +* production-, CO2-, and hydrogen-specific variables + PRODUCE(p,i,v,r,allh,t) "--metric tons per hour-- production of hydrogen or DAC capture" + CO2_CAPTURED(r,allh,t) "--metric tons per hour-- amount of CO2 captured from DAC and CCS technologies" + CO2_STORED(r,cs,allh,t) "--metric tons per hour-- amount of CO2 stored underground" + CO2_FLOW(r,rr,allh,t) "--metric tons per hour-- interregional flow of CO2" + CO2_TRANSPORT_INV(r,rr,t) "--metric tons per hour-- investment in interregional CO2 transport capacity" + CO2_SPURLINE_INV(r,cs,t) "--metric tons per hour-- spurline investment from r to carbon storage site (saline storage basin)" + H2_FLOW(r,rr,allh,t) "--metric tons per hour-- interregional flow of hydrogen" + H2_TRANSPORT_INV(r,rr,t) "--metric tons per hour-- investment in interregional hydrogen transmission capacity" + H2_STOR_INV(h2_stor,r,t) "--metric tons-- investment in hydrogen storage capacity" + H2_STOR_CAP(h2_stor,r,t) "--metric tons-- hydrogen storage capacity" + H2_STOR_IN(h2_stor,r,allh,t) "--metric tons per hour-- injection of H2 into storage in a given timeslice" + H2_STOR_OUT(h2_stor,r,allh,t) "--metric tons per hour-- widthdrawal of H2 from storage in a given timeslice" + H2_STOR_LEVEL(h2_stor,r,actualszn,allh,t) "--metric tons-- total storage level of H2 in a timeslice by storage type" + H2_STOR_LEVEL_SZN(h2_stor,r,actualszn,t) "--metric tons-- total storage level of H2 in a period by storage type" + CREDIT_H2PTC(i,v,r,allh,t) "--MW-- generation by resources which qualify for the hydrogen production tax credit, in hour h" + +* water climate variables + WATCAP(i,v,r,t) "--million gallons/year; Mgal/yr-- total water access capacity available in terms of withdraw/consumption per year" + WAT(i,v,w,r,allh,t) "--Mgal-- quantity of water withdrawn or consumed in hour h" + WATER_CAPACITY_LIMIT_SLACK(wst,r,t) "--Mgal/yr-- insufficient water supply in region r, of water type wst, in year t " +; + +Variables +* with negative emissions technologies (e.g. BECCS, DAC) - emissions +* can become negative thus not restricted to the positive domain + EMIT(etype,eall,r,t) "--metric tons-- emissions (broken down to upstream and process) in a region" + +* inter-day storage variables + STORAGE_INTERDAY_DISPATCH(i,v,r,allh,t) "--MW-- net dispatch for storage in hour h" + STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,allszn,t) "--MWh-- maximum relative state of charge on a representative period compared to hour 0 of the rep period" + STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,allszn,t) "--MWh-- minimum relative state of charge on a representative period compared to hour 0 of the rep period" +; + +*======================================== +* -- Supply Side Equation Declaration -- +*======================================== +EQUATION + +* load constraint to compute proper marginal value + eq_loadcon(r,allh,t) "--MW-- load constraint used for computing the marginal energy price" + +* load flexibility constraints + eq_load_flex_day(flex_type,r,allszn,t) "--MWh-- total flexible load in each season is equal to the exogenously-specified flexible load" + eq_load_flex1(flex_type,r,allh,t) "--MWh-- exogenously-specified flexible demand (load_exog_flex) must be served by flexible load (FLEX)" + eq_load_flex2(flex_type,r,allh,t) "--MWh-- flexible load (FLEX) can't exceed exogenously-specified flexible demand (load_exog_flex)" +* eq_load_flex_peak(r,allh,ccseason,t) "--MWh-- adjust peak demand as needed based on the load flexibility (FLEX)" + eq_loadsite_inv(r,t) "--MW-- CAP_LOADSITE accumulates INV_LOADSITE" + eq_loadsite_cap(r,allh,t) "--MW-- Realized load from optimally sited load must be less than optimally sited load capacity" + eq_loadsite_op(r,t) "--MW-- Realized load from optimally sited load must sum to Sw_LoadSiteCF" + eq_loadsite_siting(loadsitereg,t) "--MW-- Optimally sited load capacity must sum to loadsite_annual" + +* capital stock constraints + eq_cap_init_noret(i,v,r,t) "--MW-- Existing capacity that cannot be retired is equal to exogenously-specified amount" + eq_cap_init_retmo(i,v,r,t) "--MW-- Existing capacity that can be retired must be monotonically decreasing" + eq_cap_init_retub(i,v,r,t) "--MW-- Existing capacity that can be retired is less than or equal to exogenously-specified amount" + eq_cap_new_noret(i,v,r,t) "--MW-- New power capacity that cannot be retired is equal to sum of all previous years investment" + eq_cap_energy_new_noret(i,v,r,t) "--MWh-- New energy capacity that cannot be retired is equal to sum of all previous years investment" + eq_cap_new_retmo(i,v,r,t) "--MW-- New capacity that can be retired must be monotonically decreasing unless increased by investment" + eq_cap_new_retub(i,v,r,t) "--MW-- New capacity that can be retired is less than or equal to all previous years investment" + eq_cap_rsc(i,v,r,rscbin,t) "--MW-- Capacity accounting for techs with exogenous capacity tracked by rscbin" + eq_cap_up(i,v,r,rscbin,t) "--MW-- limit on capacity upsizing" + eq_cap_upgrade(i,v,r,t) "--MW-- All purchased upgrades are greater than or equal to the sum of upgraded capacity" + eq_ener_up(i,v,r,rscbin,t) "--MW-- limit on energy upsizing" + eq_forceprescription_power(pcat,r,t) "--MW-- total power investment in prescribed capacity must equal amount from exogenous prescriptions" + eq_forceprescription_energy(pcat,r,t) "--MWh-- total energy investment in prescribed capacity must equal amount from exogenous prescriptions" + eq_refurblim(i,r,t) "--MW-- total refurbishments cannot exceed the amount of capacity that has reached the end of its life" + +* renewable supply curves + eq_rsc_inv_account(i,v,r,t) "--MW-- INV for rsc techs is the sum over all bins of INV_RSC" + eq_rsc_INVlim(r,i,rscbin,t) "--MW-- total investment from each rsc bin cannot exceed the available investment" + +* capacity growth limits + eq_growthlimit_relative(i,st,t) "--MW-- relative growth limit on technologies" + eq_growthbin_limit(gbin,st,tg,t) "--MW-- capacity limit for each growth bin" + eq_growthlimit_absolute(tg,t) "--MW-- absolute growth limit on technologies" + +eq_interconnection_queues(tg,r,t) "--MW-- capacity deployment limit based on interconnection queues" + +* storage capacity credit supply curves + eq_cap_sdbin_balance(i,v,r,ccseason,t) "--MW-- total binned storage power capacity must be greater than total storage capacity" + eq_cap_sdbin_energy_balance(i,v,r,ccseason,t) "--MWh-- total binned storage energy capacity must be greater than total storage capacity" + eq_sdbin_power_energy_link(i,v,r,ccseason,sdbin,t) "--MWh-- binned storage energy capacity equal to binned power storage capacity times bin duration" + eq_sdbin_power_limit(ccreg,ccseason,sdbin,t) "--MW-- binned storage power capacity cannot exceed storage duration bin size" + +* operation and reliability + eq_site_cf(x,allh,t) "--MW-- generation at site x <= CF * capacity of constituent resources" + eq_spurclip(x,allh,t) "--MW-- generation at site x <= spurline capacity to x" + eq_spur_noclip(x,t) "--MW-- spurline capacity to x must equal total generation capacity at x" + eq_capacity_limit(i,v,r,allh,t) "--MW-- generation limited to available capacity" + eq_capacity_limit_hybrid(r,allh,t) "--MW-- generation from hybrid resources limited to available capacity" + eq_capacity_limit_nd(i,v,r,allh,t) "--MW-- generation limited to available capacity for non-dispatchable resources" + eq_curt_gen_balance(r,allh,t) "--MW-- net generation and curtailment must equal gross generation" + eq_dhyd_dispatch(i,v,r,allszn,t) "--MWh-- dispatchable hydro seasonal energy constraint (when not allowing seasonal enregy shifting)" + eq_min_cf(i,r,t) "--MWh-- minimum capacity factor constraint for each generator fleet, applied to (i,r)" + eq_max_daily_cf(i,r,allszn,t) "--MWh-- maximum daily capacity factor constraint for any technology with maxdailycf(i,t) specified" + eq_mingen_fixed(i,v,r,allh,t) "--MW-- Generation in each timeslice must be greater than mingen_fixed * available capacity" + eq_mingen_lb(r,allh,allszn,t) "--MW-- lower bound on minimum generation level" + eq_mingen_ub(r,allh,allszn,t) "--MW-- upper bound on minimum generation level" + eq_minloading(i,v,r,allh,allhh,t) "--MW-- minimum loading across same-season hours" + eq_ramping(i,r,allh,allhh,t) "--MW-- definition of RAMPUP" + eq_reserve_margin(r,ccseason,t) "--MW-- planning reserve margin requirement" + eq_supply_demand_balance(r,allh,t) "--MW-- supply demand balance" + eq_vsc_flow(r,allh,t) "--MW-- DC power flow" + eq_transmission_limit(r,rr,allh,t,trtype) "--MW-- transmission flow limit" + +* operating reserve constraints + eq_OpRes_requirement(ortype,r,allh,t) "--MW-- operating reserve constraint" + eq_ORCap_large_res_frac(ortype,i,v,r,allh,t) "--MW-- operating reserve capacity availability constraint for generators with reserve_frac > 0.5" + eq_ORCap_small_res_frac(ortype,i,v,r,allh,t) "--MW-- operating reserve capacity availability constraint for generators with reserve_frac <= 0.5" + +* regional and national policies + eq_emit_accounting(etype,e,r,t) "--metric tons-- accounting for total emissions in a region" + eq_emit_rate_limit(e,r,t) "--metric tons per MWh-- emission rate limit" + eq_annual_cap(eall,t) "--metric tons-- annual (year-specific) emissions cap", + eq_bankborrowcap(e) "--weighted metric tons-- flexible banking and borrowing cap (to be used w/intertemporal solve only" + eq_RGGI_cap(t) "--metric tons CO2-- RGGI constraint -- Regions' emissions must be less than the RGGI cap" + eq_state_cap(st,t) "--metric tons CO2-- state-level CO2 cap constraint -- used to represent California cap and trade program" + eq_CSAPR_Budget(csapr_group,t) "--metric tons NOx-- CSAPR trading group emissions cannot exceed the budget cap" + eq_CSAPR_Assurance(st,t) "--metric tons NOx-- CSAPR state emissions cannot exceed the assurance cap" + eq_BatteryMandate(st,t) "--MW-- battery storage capacity must be greater than indicated level" + eq_cdr_cap(t) "--metric tons CO2-- CO2 removal (DAC and BECCS) can only offset emissions from fossil+CCS and methane leakage" + eq_caa_max_cf(i,v,r,t) "--MWh-- maximum capacity factors for new gas plants (CCs and CTs) under Clean Air Act Section 111 (BSER)" + eq_caa_rate_standard(st,t) "--metric tons CO2-- maximum coal emissions per state under Clean Air Act Section 111 (rate-based emissions standard)" + +* RPS Policy equations + eq_REC_Generation(RPSCat,i,st,t) "--RECs-- Generation of RECs by state" + eq_REC_Requirement(RPSCat,st,t) "--RECs-- RECs generated plus trade must meet the state's requirement" + eq_REC_ooslim(RPSCat,st,t) "--RECs-- RECs imported cannot exceed a fraction of total requirement for certain states", + eq_REC_launder(RPSCat,st,t) "--RECs-- RECs laundering constraint" + eq_REC_BundleLimit(RPSCat,st,ast,t) "--RECS-- trade in bundle recs must be less than interstate electricity transmission" + eq_REC_unbundledLimit(RPScat,st,t) "--RECS-- unbundled RECS cannot exceed some percentage of total REC requirements" + eq_RPS_OFSWind(st,t) "--MW-- MW of offshore wind capacity must be greater than or equal to RPS amount" + eq_national_gen(t) "--MWh-- e.g. a national RPS or CES. require a certain amount of total generation to be from specified sources." + +* fuel supply curve equations + eq_gasused(cendiv,allh,t) "--MMBtu-- gas used must be from the sum of gas bins" + eq_gasbinlimit(cendiv,gb,t) "--MMBtu-- limit on gas from each bin" + eq_gasbinlimit_nat(gb,t) "--MMBtu-- national limit on gas from each bin" + eq_bioused(r,t) "--MMBtu-- bio used must be from the sum of bio bins" + eq_biousedlimit(bioclass,usda_region,t) "--MMBtu-- limit on bio from each bin in each USDA region" + +* regional natural gas supply curves + eq_gasaccounting_regional(cendiv,t) "--MMBtu-- regional gas consumption cannot exceed the amount used in bins" + eq_gasaccounting_national(t) "--MMBtu-- national gas consumption cannot exceed the amount used in bins" + eq_gasbinlimit_regional(fuelbin,cendiv,t) "--MMBtu-- regional binned gas usage cannot exceed bin capacity" + eq_gasbinlimit_national(fuelbin,t) "--MMBtu-- national binned gas usage cannot exceed bin capacity" + +* hydrogen supply and demand + eq_prod_capacity_limit(i,v,r,allh,t) "--metric tons-- production cannot exceeds its capacity" + eq_h2_demand(p,t) "--metric tons-- production of hydrogen must meet exogenous demand plus H2-CT/CC use" + eq_h2_demand_regional(r,allh,t) "--metric tons per hour-- regional hydrogen supply must equal demand net trade and storage" + eq_h2_transport_caplimit(r,rr,allh,t) "--metric tons per hour-- H2 flow cannot exceed cumulative pipeline investment" + eq_h2_storage_flowlimit(h2_stor,r,allh,t) "--metric tons per hour-- H2 storage injection or withdrawal cannot exceed cumulative storage investment" + eq_h2_storage_capacity(h2_stor,r,t) "--metric tons-- H2 storage capacity is sum of H2 storage investments" + eq_h2_min_storage_cap(r,t) "--metric tons-- H2 storage capacity must be ≥ Sw_H2_MinStorHours * H2 usage capacity" + eq_h2_ptc_region_balance(h2ptcreg,t) "--MWh-- clean generation for hydrogen production must be more than electricity required for electrolytic H2 production in that region and year" + eq_h2_ptc_region_hour_balance(h2ptcreg,allh,t) "--MWh-- clean generation for hydrogen production must be more than electricity required for electrolytic H2 production in that region, hour and year" + eq_h2_ptc_creditgen(i,v,r,allh,t) "--MWh-- total generation must be greater than clean generation for hydrogen production" + eq_h2_storage_caplimit(h2_stor,r,actualszn,allh,t) "--metric tons-- total H2 storage in a storage facility cannot exceed investment capacity" + eq_h2_storage_level(h2_stor,r,actualszn,allh,t) "--metric tons-- tracks H2 storage level by storage type and BA within and across periods" + eq_h2_storage_caplimit_szn(h2_stor,r,actualszn,t) "--metric tons-- total H2 storage in a storage facility cannot exceed investment capacity" + eq_h2_storage_level_szn(h2_stor,r,actualszn,t) "--metric tons-- tracks H2 storage level by storage type and BA within and across periods" + +* CO2 capture and storage + eq_co2_capture(r,allh,t) "--metric tons-- accounting of CO2 captured from DAC and CCS technologies" + eq_co2_injection_limit(cs,allh,t) "--metric tons per hour-- limit on CO2 injection for each carbon site as a rate" + eq_co2_sink(r,allh,t) "--metric tons per hour-- co2 stored or used must exceed co2 captured plus net trade" + eq_co2_transport_caplimit(r,rr,allh,t) "--metric tons-- limit on interregional co2 trade" + eq_co2_spurline_caplimit(r,cs,allh,t) "--metric tons-- limit on transport of CO2 from BA to carbon storage site" + eq_co2_cumul_limit(cs,t) "--cumulative metric tons-- total stored in a reservor cannot exceed capacity" + +* transmission equations + eq_INVTRAN_DC(r,rr,trtype,t) "--MW-- DC transmission additions are assumed to add the same capacity in both directions" + eq_INVTRAN_AC_forward(r,rr,tscbin,t) "--$-- Forward transmission capacity is determined by the cumulative capex invested in the interface" + eq_INVTRAN_AC_reverse(r,rr,tscbin,t) "--$-- Reverse transmission capacity is determined by the cumulative capex invested in the interface" + eq_INVTRAN_AC(r,rr,t) "--MW-- Accumulate investment in tscbins into INVTRAN" + eq_TRAN_CAPEX_BINS(r,rr,tscbin,t) "--$-- Transmission investment bins are limited by the transmission upgrade supply curve" + eq_invtran_exog(r,rr,trtype,t) "--MW-- Exogenous transmission investments are included in INVTRAN" + eq_CAPTRAN_ENERGY(r,rr,trtype,t) "--MW-- capacity accounting for transmission capacity for energy trading" + eq_CAPTRAN_PRM(r,rr,trtype,t) "--MW-- capacity accounting for transmission capacity for PRM trading" + eq_prescribed_transmission(r,rr,trtype,t) "--MW-- investment in transmission up to first year allowed must be less than the exogenous possible transmission", + eq_PRMTRADELimit(r,rr,trtype,ccseason,t) "--MW-- trading of PRM capacity cannot exceed the line's capacity" + eq_transmission_investment_max(t) "--MWmile-- investment in transmission must be <= Sw_TransInvMax" + eq_CAPTRAN_max(r,rr,trtype,t) "--MW-- upper limit for transmission capacity of each trtype across individual interfaces" + eq_CAPTRAN_max_total(r,rr,t) "--MW-- upper limit for transmission capacity of all trtypes across individual interfaces" + eq_CAP_CONVERTER(r,t) "--MW-- capacity accounting for VSC AC/DC converters" + eq_CAP_SPUR(x,t) "--MW-- capacity accounting for spur lines" + eq_converter_max(r,t) "--MW-- upper limit for VSC AC/DC converter capacity in individual BAs" + eq_CONVERSION_limit_energy(r,allh,t) "--MW-- AC/DC energy conversion is limited to converter capacity" + eq_CONVERSION_limit_prm(r,ccseason,t) "--MW-- AC/DC PRM conversion is limited to converter capacity" + eq_PRMTRADE_VSC(r,ccseason,t) "--MW-- PRM capacity can flow through VSC lines but doesn't directly contribute to PRM" + eq_POI_cap(r,t) "--MW-- POI capacity accounting (for network reinforcement costs)" + eq_CAPTRAN_GRP(transgrp,transgrpp,t) "--MW-- combined flow capacity between transmission groups" + eq_transgrp_limit_energy(transgrp,transgrpp,allh,t) "--MW-- limit on combined interface energy flows" + eq_transgrp_limit_prm(transgrp,transgrpp,ccseason,t) "--MW-- limit on combined interface PRM flows" + eq_CAPTRAN_ITL(itlgrp,itlgrpp,t) "--MW-- combined flow capacity between ITL groups" + eq_itlgrp_limit_energy(itlgrp,itlgrpp,allh,t) "--MW-- limit on combined interface energy flows for ITLs" + eq_itlgrp_limit_prm(itlgrp,itlgrpp,ccseason,t) "--MW-- limit on combined interface PRM flows for ITLs" + eq_firm_transfer_limit(nercr,allh,t) "--MW-- limit net firm capacity imports into NERC regions when using stress periods" + eq_firm_transfer_limit_cc(nercr,ccseason,t) "--MW-- limit net firm capacity imports into NERC regions when using capacity credit" + eq_offshore_no_backflow(r,rr,trtype,allh,t) "--MW-- disallow transmission flows from land to offshore zones" + +* storage-specific equations + eq_storage_capacity(i,v,r,allh,t) "--MW-- second storage capacity constraint in addition to eq_capacity_limit" + eq_storage_duration(i,v,r,allh,t) "--MWh-- limit STORAGE_LEVEL based on hours of storage available" + eq_storage_in_cap(i,v,r,allh,t) "--MW-- storage_in must be less than a given fraction of power output capacity" + eq_storage_in_minloading(i,v,r,allh,allhh,t) "--MW-- minimum level for storage_in across same-season hours" + eq_storage_level(i,v,r,allh,t) "--MWh-- storage level inventory balance from one time-slice to the next" + eq_storage_interday_level_max_day(i,v,r,allszn,allh,t) "--MWh-- define the maximum relative SOC on a representative period compared to hour 0 of the rep period" + eq_storage_interday_level_min_day(i,v,r,allszn,allh,t) "--MWh-- define the minimum relative SOC on a representative period compared to hour 0 of the rep period" + eq_storage_interday_min_level_start(i,v,r,allszn,t) "--MWh-- enforce minimun SOC at first period of each partition" + eq_storage_interday_min_level_end(i,v,r,allszn,t) "--MWh-- enforce minimun SOC at last period of each partition" + eq_storage_interday_level(i,v,r,allszn,t) "--MWh-- calculate SOC of each partition at its hour 0" + eq_storage_interday_max_level_start(i,v,r,allszn,t) "--MWh-- enforce maximum SOC at first period of each partition" + eq_storage_interday_max_level_end(i,v,r,allszn,t) "--MWh-- enforce maximum SOC at last period of each partition" + eq_storage_opres(i,v,r,allh,t) "--MWh-- there must be sufficient energy in the storage to be able to provide operating reserves" + eq_storage_thermalres(i,v,r,allh,t) "--MW-- thermal storage contribution to operating reserves is store_in only" + eq_battery_minduration(i,v,r,t) "--MWh-- when power capacity is built, energy capacity should have a minimum capacity" + +* hybrid plant equations + eq_plant_total_gen(i,v,r,allh,t) "--MW-- generation post curtailment = generation from pv (post curtailment) + generation from battery - charging from PV" + eq_hybrid_plant_energy_limit(i,v,r,allh,t) "--MW-- PV energy to storage (no curtailment recovery) + PV energy to inverter <= PV resource" + eq_plant_capacity_limit(i,v,r,allh,t) "--MW-- energy moving through the inverter cannot exceed the inverter capacity" + eq_pvb_itc_charge_reqt(i,v,r,t) "--MWh-- total energy charged from local PV >= ITC qualification fraction * total energy charged" + +* Canadian imports balance + eq_Canadian_Imports(r,allszn,t) "--MWh-- Balance of Canadian imports by season" + +* water usage accounting + eq_water_accounting(i,v,w,r,allh,t) "--Mgal-- water usage accounting" + eq_water_capacity_total(i,v,r,t) "--Mgal-- specify required water access based on generation capacity and water use rate" + eq_water_capacity_limit(wst,r,t) "--Mgal/yr-- total water access must not exceed supply by region and water type" + eq_water_use_limit(i,v,w,r,allszn,t) "--Mgal/yr-- water use must not exceed available access" + +* flexible CCS constraints + eq_ccsflex_byp_ccsenergy_limit(i,v,r,allh,t) "--avg MW-- Limit the CCS power for a bypass system in each time-slice" + eq_ccsflex_sto_ccsenergy_limit_szn(i,v,r,allszn,t) "--MWh-- Limit the CCS power for a storage system across a characteristic day" + eq_ccsflex_sto_ccsenergy_balance(i,v,r,allszn,t) "--MWh-- Total CCS energy requirement can be distributed across a characteristic day" + eq_ccsflex_sto_storage_level(i,v,r,allh,t) "--varies-- Track the level of the CCS storage balance for each time-slice" + eq_ccsflex_sto_storage_level_max(i,v,r,allh,t) "--varies-- Limit the level of the CCS storage system" +; + +*========================== +* --- LOAD CONSTRAINTS --- +*========================== + +* --------------------------------------------------------------------------- + +*the marginal off of this constraint allows you to +*determine the full price of electricity load +*i.e. the price of load with consideration to operating +*reserve and planning reserve margin considered +eq_loadcon(r,h,t)$tmodel(t).. + + LOAD(r,h,t) + + =e= + +*[plus] the static, exogenous load + + load_exog_static(r,h,t) + +*[plus] exogenously defined exports to Canada +* note net canadian load (when Sw_Canada = 2) is included +* in eq_supply_demand since LOAD needs to stay positive +* while net_trade can be negative and cause infeasibilities + + can_exports_h(r,h,t)$[Sw_Canada=1] + +*[plus] load from EV charging (baseline/unmanaged) + + evmc_baseline_load(r,h,t)$Sw_EVMC + +*[plus] shifted load from adopted EVMC shape resources + + sum{(i,v)$[evmc_shape(i)$valcap(i,v,r,t)], evmc_shape_load(i,r,h) * CAP(i,v,r,t)} + +*[plus] load shifted from other timeslices + + sum{flex_type, FLEX(flex_type,r,h,t) }$Sw_EFS_flex + +*[plus] Load created by production activities - only tracked during representative hours +* [metric tons/hour] / [metric tons/MWh] = [MW] + + sum{(p,i,v)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)$(not sameas(i,"dac_gas"))], + PRODUCE(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) }$[Sw_Prod$h_rep(h)] + +*[plus] load for compressors associated with hydrogen storage injections or withdrawals +* metric tons/hour * MWh/metric tons = MW + + sum{h2_stor$h2_stor_r(h2_stor,r), + h2_network_load(h2_stor,t) + * ( H2_STOR_IN(h2_stor,r,h,t) + H2_STOR_OUT(h2_stor,r,h,t) ) }$Sw_H2_CompressorLoad + +* [plus] load for hydrogen pipeline compressors +* metric tons/hour * MWh/metric tons = MW + + sum{rr$h2_routes(r,rr), + h2_network_load("h2_compressor",t) + * (( H2_FLOW(r,rr,h,t) + H2_FLOW(rr,r,h,t) ) / 2) }$Sw_H2_CompressorLoad + +* Operations of flexibly sited load: +* - OP_LOADSITE is used when 0 < Sw_LoadSiteCF < 1 +* - CAP_LOADSITE is used when Sw_LoadSiteCF = 1 because OP_LOADSITE = CAP_LOADSITE +* (the effect is the same but avoiding the h-indexed OP_LOADSITE reduces solve time) + + OP_LOADSITE(r,h,t)$[Sw_LoadSiteCF$(Sw_LoadSiteCF<1)$val_loadsite(r)] + + CAP_LOADSITE(r,t)$[(Sw_LoadSiteCF=1)$val_loadsite(r)] +; + +* --------------------------------------------------------------------------- + +*====================================== +* --- LOAD FLEXIBILITY CONSTRAINTS --- +*====================================== +* CAP_LOADSITE accumulates INV_LOADSITE +eq_loadsite_inv(r,t) + $[tmodel(t) + $Sw_LoadSiteCF + $val_loadsite(r) + $(not Sw_PCM)].. + + CAP_LOADSITE(r,t) + + =e= + + + sum{(tt)$[(yeart(tt) <= yeart(t))$(tmodel(tt) or tfix(tt))], + INV_LOADSITE(r,tt) } +; + +* --------------------------------------------------------------------------- +* Realized load from optimally sited load must be less than optimally sited load capacity +eq_loadsite_cap(r,h,t) + $[tmodel(t) + $Sw_LoadSiteCF + $(Sw_LoadSiteCF<1) + $val_loadsite(r)].. + + CAP_LOADSITE(r,t) + + =g= + + OP_LOADSITE(r,h,t) +; + +* --------------------------------------------------------------------------- +* Realized load from optimally sited load must sum to Sw_LoadSiteCF +eq_loadsite_op(r,t) + $[tmodel(t) + $Sw_LoadSiteCF + $(Sw_LoadSiteCF<1) + $val_loadsite(r)].. + + sum{h, OP_LOADSITE(r,h,t) * hours(h) } + + =g= + + CAP_LOADSITE(r,t) * sum{h, hours(h) } * Sw_LoadSiteCF +; + +* --------------------------------------------------------------------------- +* Optimally sited load capacity must sum to loadsite_annual +eq_loadsite_siting(loadsitereg,t) + $[tmodel(t) + $Sw_LoadSiteCF + $(not Sw_PCM)].. + + sum{r$[r_loadsitereg(r,loadsitereg)$val_loadsite(r)], CAP_LOADSITE(r,t) } + + =e= + + loadsite_annual(loadsitereg,t) +; + +* --------------------------------------------------------------------------- + +*The following 3 equations apply to the flexibility of load in ReEDS, originally developed +*as part of the EFS study in ReEDS heritage and adapted for ReEDS-2.0 here. + +* Additional work has been done to represent flexible load as a generator + storage +* with boundaries on how many timeslices this generator may shift. See equations +* in the DR CONSTRAINTS section for that representation + +* FLEX load in each season equals the total exogenously-specified flexible load in each season +eq_load_flex_day(flex_type,r,szn,t)$[tmodel(t)$Sw_EFS_flex].. + + sum{h$h_szn(h,szn), FLEX(flex_type,r,h,t) * hours(h) } / numdays(szn) + + =e= + + sum{h$h_szn(h,szn), load_exog_flex(flex_type,r,h,t) * hours(h) } / numdays(szn) +; + +* --------------------------------------------------------------------------- + +* for the "previous" flex type: the amount of exogenously-specified load in timeslice "h" +* must be served by FLEX load either in the timeslice h or the timeslice PRECEEDING h +* +* for the "next" flex type: the amount of exogenously-specified load in timeslice "h" +* must be served by FLEX load either in the timeslice h or the timeslice FOLLOWING h +* +* for the "adjacent" flex type: the amount of exogenously-specified load in timeslice "h" +* must be served by FLEX load either in the timeslice h or a timeslice ADJACENT to h + +eq_load_flex1(flex_type,r,h,t)$[tmodel(t)$Sw_EFS_flex].. + + FLEX(flex_type,r,h,t) * hours(h) + + + sum{hh$flex_h_corr1(flex_type,h,hh), FLEX(flex_type,r,hh,t) * hours(hh) } + + =g= + + load_exog_flex(flex_type,r,h,t) * hours(h) +; + +* --------------------------------------------------------------------------- + +* for the "previous" flex type: FLEX load in timeslice "h" cannot exceed the sum of +* exogenously-specified load in timeslice h and the timeslice following h +* +* for the "next" flex type: FLEX load in timeslice "h" cannot exceed the sum of +* exogenously-specified load in timeslice h and the timeslice preceeding h +* +* for the "adjacent" flex type: FLEX load in timeslice "h" cannot exceed the sum of +* exogenously-specified load in timeslice h and the timeslices adjacent to h + +eq_load_flex2(flex_type,r,h,t)$[tmodel(t)$Sw_EFS_flex].. + + load_exog_flex(flex_type,r,h,t) * hours(h) + + + sum{hh$flex_h_corr2(flex_type,h,hh), load_exog_flex(flex_type,r,hh,t) * hours(hh) } + + =g= + + FLEX(flex_type,r,h,t) * hours(h) +; + +* * --------------------------------------------------------------------------- +* This constraint and the associated PEAK_FLEX variable are not currently supported +* but are left in the model in case someone decides to revive them. +* eq_load_flex_peak(r,h,ccseason,t)$[tmodel(t)$Sw_EFS_flex].. +* * peak demand EFS flexibility adjustment is greater than +* PEAK_FLEX(r,ccseason,t)$h_ccseason(h,ccseason) + +* =g= + +* * the static peak in each timeslice +* peakdem_static_h(r,h,t)$h_ccseason(h,ccseason) + +* * PLUS the flexibile load served in each timeslice +* + sum{flex_type, FLEX(flex_type,r,h,t) }$h_ccseason(h,ccseason) + +* * MINUS the static peak demand in the season corresponding to each timeslice +* - peakdem_static_ccseason(r,ccseason,t)$h_ccseason(h,ccseason) +* ; + +* --------------------------------------------------------------------------- + +*========================================================= +* --- EQUATIONS RELATING CAPACITY ACROSS TIME PERIODS --- +*========================================================= + +*==================================== +* -- existing capacity equations -- +*==================================== + +$ontext + +The following six equations dictate how capacity is represented in the model. + +The first three equations handle init-X vintages (those that existed pre-2010) +which are bounded by m_capacity_exog. With retirements (in the second and third +equations), the constraints imply that capacity must be less than or +equal to m_capacity_exog and monotonically decreasing over time - +implying that if endogenous capacity was reduced in the previous year, +it cannot be brought back online. + +New capacity, handled in equations four through six, is the sum of previous +years' greenfield investments and refurbishments. The same logic is present +for retiring capacity, the only difference being that contemporaneous +investment can increase the present-period's capacity. + +Upgraded capacity reduces the total amount of capacity available to the +upgraded-from technology. For example, the model starts with 100MW of +coaloldscr (m_capacity_exog = 100) capacity then upgrades 10MW of that to +coaloldscr_coal-ccs capacity. The remaining amount of available coaloldscr +is thus 90 and coaloldscr_coal-ccs capacity is 10 but both are still less +than the 100 available. As time progresses and exogenous capacity declines, +the model chooses which units to take offline. + +$offtext + +* --------------------------------------------------------------------------- + +eq_cap_init_noret(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$initv(v)$(not upgrade(i)) + $(not retiretech(i,v,r,t))$(not Sw_PCM)].. + + m_capacity_exog(i,v,r,t) + +* Account for capacity upsizing within init vintages + + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], + degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } + + =e= + + CAP(i,v,r,t) + + + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,tt) + - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + +* include contemporaneous upgrades when they are intended to +* persist as new bintages with sw_upgrades = 2 + + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], + UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] + + + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,t) }$[Sw_Upgrades = 2] +; + +* --------------------------------------------------------------------------- + +eq_cap_init_retub(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$initv(v)$(not upgrade(i)) + $retiretech(i,v,r,t)$(not Sw_PCM)].. + + m_capacity_exog(i,v,r,t) + +* Account for capacity upsizing within init vintages + + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], + degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } + + =g= + + CAP(i,v,r,t) + + + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,tt) + - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + +* include contemporaneous upgrades when they are intended to +* persist as new bintages with sw_upgrades = 2 + + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], + UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] + + + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,t) }$[Sw_Upgrades = 2] + +; + +* --------------------------------------------------------------------------- + +eq_cap_init_retmo(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$initv(v)$(not upgrade(i)) + $retiretech(i,v,r,t)$(not Sw_PCM)].. + + sum{tt$[tprev(t,tt)$valcap(i,v,r,tt)], + + CAP(i,v,r,tt) + + + sum{(ii,ttt)$[(tfix(ttt) or tmodel(ttt))$(yeart(ttt)<=yeart(tt)) + $valcap(ii,v,r,ttt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,ttt) + - (UPGRADES_RETIRE(ii,v,r,ttt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + + + sum{ii$[valcap(ii,v,r,tt)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,tt) }$[Sw_Upgrades = 2] + } + +* Account for capacity upsizing within init vintages + + sum{rscbin$allow_cap_up(i,v,r,rscbin,t), INV_CAP_UP(i,v,r,rscbin,t) } + + =g= + + CAP(i,v,r,t) + + + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,tt) + - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + +* include contemporaneous upgrades when they are intended to +* persist as new bintages with sw_upgrades = 2 + + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], + UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] + + + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,t) }$[Sw_Upgrades = 2] +; + +* --------------------------------------------------------------------------- + +*============================== +* -- new capacity equations -- +*============================== + +* --------------------------------------------------------------------------- + +eq_cap_new_noret(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$(not upgrade(i)) + $(not retiretech(i,v,r,t))$(not Sw_PCM)].. + + sum{tt$[inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))$valcap(i,v,r,tt)], + degrade(i,tt,t) * (INV(i,v,r,tt) + INV_REFURB(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]) + } + +* Account for capacity upsizing within new vintages + + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], + degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } + + =e= + + CAP(i,v,r,t) + + + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,tt) + - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + +* include contemporaneous upgrades when they are intended to +* persist as new bintages with sw_upgrades = 2 + + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], + UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] + + + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,t) }$[Sw_Upgrades = 2] + +; + +* --------------------------------------------------------------------------- + +eq_cap_energy_new_noret(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$battery(i)$(not Sw_PCM)].. + + sum{tt$[inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))$valcap(i,v,r,tt)], + degrade(i,tt,t) * INV_ENERGY(i,v,r,tt) + } + + + m_capacity_exog_energy(i,v,r,t) + + =e= + + CAP_ENERGY(i,v,r,t) + +; + +* --------------------------------------------------------------------------- + +eq_cap_new_retub(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$(not upgrade(i)) + $retiretech(i,v,r,t)$(not Sw_PCM)].. + + sum{tt$[inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))$valcap(i,v,r,tt)], + degrade(i,tt,t) * (INV(i,v,r,tt) + INV_REFURB(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]) + } + +* Account for capacity upsizing within new vintages + + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], + degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } + + =g= + + CAP(i,v,r,t) + + + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,tt) + - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + +* include contemporaneous upgrades when they are intended to +* persist as new bintages with sw_upgrades = 2 + + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], + UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] + + + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,t) }$[Sw_Upgrades = 2] +; + +* --------------------------------------------------------------------------- + +eq_cap_new_retmo(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$(not upgrade(i)) + $retiretech(i,v,r,t)$(not Sw_PCM)].. + + sum{tt$[tprev(t,tt)$valcap(i,v,r,tt)], + degrade(i,tt,t) * CAP(i,v,r,tt) + + + sum{(ii,ttt)$[(tfix(ttt) or tmodel(ttt))$(yeart(ttt)<=yeart(tt)) + $valcap(ii,v,r,ttt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,ttt) + - (UPGRADES_RETIRE(ii,v,r,ttt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + + + sum{ii$[valcap(ii,v,r,tt)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,tt) }$[Sw_Upgrades = 2] + + } + + + INV(i,v,r,t)$valinv(i,v,r,t) + + + INV_REFURB(i,v,r,t)$[valinv(i,v,r,t)$refurbtech(i)$Sw_Refurb] + +* Account for capacity upsizing within new vintages + + sum{rscbin$allow_cap_up(i,v,r,rscbin,t), INV_CAP_UP(i,v,r,rscbin,t) } + + =g= + + CAP(i,v,r,t) + + + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], + UPGRADES(ii,v,r,tt) + - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} + $[(Sw_Upgrades = 1)] + +* include contemporaneous upgrades when they are intended to +* persist as new bintages with sw_upgrades = 2 + + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], + UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] + + + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], + CAP(ii,v,r,t) }$[Sw_Upgrades = 2] +; + +* --------------------------------------------------------------------------- +* Capacity accounting for rsc techs +eq_cap_rsc(i,v,r,rscbin,t) + $[tmodel(t) + $rsc_i(i)$(not sccapcosttech(i)) + $valcap(i,v,r,t) + $(not Sw_PCM)].. + + sum{tt$[tfirst(tt)$exog_rsc(i)], + capacity_exog_rsc(i,v,r,rscbin,tt) } + + + sum{tt$[(yeart(tt) <= yeart(t))$(tmodel(tt) or tfix(tt)) + $m_rscfeas(r,i,rscbin) + $valinv(i,v,r,tt)], + INV_RSC(i,v,r,rscbin,tt) + } + + =e= + + CAP_RSC(i,v,r,rscbin,t) +; + +* --------------------------------------------------------------------------- + +eq_cap_upgrade(i,v,r,t)$[valcap(i,v,r,t)$upgrade(i)$Sw_Upgrades$tmodel(t)$(not Sw_PCM)].. + +* without peristent upgrades, all upgrades correspond to their original bintage + sum{(tt)$[(tfix(tt) or tmodel(tt)) + $(yeart(tt)<=yeart(t)) + $(yeart(tt)>=Sw_Upgradeyear) + $valcap(i,v,r,tt) + $sum{ii$upgrade_from(i,ii), valcap(ii,v,r,tt) }], + UPGRADES(i,v,r,tt) * (1 - upgrade_derate(i,v,r,tt)) + }$[(Sw_Upgrades = 1)$(not coal(i))] + +* coal cannot upgrade after the retire year - ie no mothballing + + sum{(tt)$[(tfix(tt) or tmodel(tt)) + $(yeart(tt)<=yeart(t)) + $(yeart(tt)>=Sw_Upgradeyear) + $valcap(i,v,r,tt) + $sum{ii$upgrade_from(i,ii), valcap(ii,v,r,tt) } + $(yeart(tt)<=caa_coal_retire_year)], + UPGRADES(i,v,r,tt) * (1 - upgrade_derate(i,v,r,tt)) + }$[(Sw_Upgrades = 1)$coal(i)] + +* all previous years upgrades converted to new bintages of the present year +* NOTE: the 'v' in ivt(i,v,tt) here is an important distinction - +* although we're summing over 'vv' we still only want upgrades +* to be included for the upgrade tech's vintage combination + + sum{(tt,vv)$[(tfix(tt) or tmodel(tt))$(initv(vv) or sameas(v,vv)) + $(yeart(tt)<=yeart(t))$ivt(i,v,tt) + $(yeart(tt)>=Sw_Upgradeyear) + $valcap(i,v,r,tt)$(not sameas(i,'hydEND_hydED')) + $sum{ii$upgrade_from(i,ii), valcap(ii,vv,r,tt) }], + UPGRADES(i,vv,r,tt) * (1 - upgrade_derate(i,vv,r,tt)) + }$[Sw_Upgrades = 2] + + + sum{(tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))$(yeart(tt)>=Sw_Upgradeyear) + $valcap(i,v,r,tt)$sameas(i,'hydEND_hydED')], + UPGRADES(i,v,r,tt) * (1 - upgrade_derate(i,v,r,tt)) + }$[Sw_Upgrades = 2] + + =e= + + CAP(i,v,r,t) + +* note this is equivalent to the previous version that had a =g= +* sign in the corrolary equation + + sum{tt$[(tfix(tt) or tmodel(tt))$valcap(i,v,r,tt)], + UPGRADES_RETIRE(i,v,r,tt)}$[not noret_upgrade_tech(i)] +; + +* --------------------------------------------------------------------------- + +* Capacity upsizing limit +* This uses rscbin to constrain hydropower upsizing using supply curve data. +eq_cap_up(i,v,r,rscbin,t)$[tmodel(t)$allow_cap_up(i,v,r,rscbin,t)$(not Sw_PCM)].. + + cap_cap_up(i,v,r,rscbin,t) + + =g= + + sum{tt$[(tmodel(tt) or tfix(tt))], INV_CAP_UP(i,v,r,rscbin,tt) } +; + +* --------------------------------------------------------------------------- + +*Energy upsizing limit +* This uses rscbin to constrain hydropower upsizing using supply curve data. +eq_ener_up(i,v,r,rscbin,t)$[tmodel(t)$allow_ener_up(i,v,r,rscbin,t)$(not Sw_PCM)].. + + cap_ener_up(i,v,r,rscbin,t) + + =g= + + sum{tt$[(tmodel(tt) or tfix(tt))], INV_ENER_UP(i,v,r,rscbin,tt) } +; + +* --------------------------------------------------------------------------- + +* Prescribe power capacity +eq_forceprescription_power(pcat,r,t) + $[tmodel(t)$force_pcat(pcat,t)$Sw_ForcePrescription + $sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,t) } + $(not Sw_PCM)].. + +*capacity built in the current period or prior + sum{(i,newv,tt)$[valinv(i,newv,r,tt)$prescriptivelink(pcat,i) + $(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], + INV(i,newv,r,tt) + INV_REFURB(i,newv,r,tt)$[refurbtech(i)$Sw_Refurb]} + + =e= + +*must equal the cumulative prescribed amount + sum{tt$[(yeart(tt)<=yeart(t)) + $(tmodel(tt) or tfix(tt))], + noncumulative_prescriptions(pcat,r,tt)} + +* plus any extra power buildouts (no penalty here - used as free slack) +* only on or after the first year the techs are available + + EXTRA_PRESCRIP(pcat,r,t)$[yeart(t)>=firstyear_pcat(pcat)] + +* or in regions where there is a offshore wind requirement + + EXTRA_PRESCRIP(pcat,r,t)$[r_offshore(r,t)$sameas(pcat,'wind-ofs') + $(yeart(t)>=firstyear_RPS) + $sum{st$r_st(r,st), offshore_cap_req(st,t) }] +; + +* --------------------------------------------------------------------------- + +* Prescribe energy capacity +eq_forceprescription_energy(pcat,r,t) + $[tmodel(t)$force_pcat(pcat,t)$Sw_ForcePrescription + $sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,t) } + $(not Sw_PCM)].. + +*energy capacity built in the current period or prior + sum{(i,newv,tt)$[valinv(i,newv,r,tt)$prescriptivelink(pcat,i) + $(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt)) + $battery(i)], + INV_ENERGY(i,newv,r,tt)} + + =e= + +*must equal the cumulative prescribed energy amount + sum{tt$[(yeart(tt)<=yeart(t)) + $(tmodel(tt) or tfix(tt))], + noncumulative_prescriptions_energy(pcat,r,tt)} + +* plus any extra energy buildouts (no penalty here - used as free slack) +* only on or after the first year the techs are available + + EXTRA_PRESCRIP_ENERGY(pcat,r,t)$[yeart(t)>=firstyear_pcat(pcat)] +; + +* --------------------------------------------------------------------------- + +*limit the amount of refurbishments available in specific year +*this is the sum of all previous year's investment that is now beyond the age +*limit (i.e. it has exited service) plus the amount of retired exogenous capacity +*that we begin with +eq_refurblim(i,r,t)$[tmodel(t)$refurbtech(i)$Sw_Refurb$(not Sw_PCM)].. + +*investments that meet the refurbishment requirement (i.e. they've expired) + sum{(vv,tt)$[m_refurb_cond(i,vv,r,t,tt)$(tmodel(tt) or tfix(tt))$valinv(i,vv,r,tt)], + INV(i,vv,r,tt) } + +*[plus] exogenous decay in capacity +*note here that the tfix or tmodel set does not apply +*since we'd want capital that expires in off-years to +*be included in this calculation as well + + sum{(v,tt)$[yeart(tt)<=yeart(t)], + avail_retire_exog_rsc(i,v,r,tt) } + + =g= + +*must exceed the total sum of investments in refurbishments +*that have yet to expire - implying an investment can be refurbished more than once +*if the first refurbishment has exceed its age limit + sum{(vv,tt)$[inv_cond(i,vv,r,t,tt)$(tmodel(tt) or tfix(tt))$valinv(i,vv,r,tt)], + INV_REFURB(i,vv,r,tt) + } +; + +* --------------------------------------------------------------------------- + +eq_rsc_inv_account(i,v,r,t)$[tmodel(t)$valinv(i,v,r,t)$rsc_i(i)$(not Sw_PCM)].. + + sum{rscbin$m_rscfeas(r,i,rscbin), INV_RSC(i,v,r,rscbin,t) } + + =e= + + INV(i,v,r,t) +; + +* --------------------------------------------------------------------------- + +*note that the following equation only restricts inv_rsc and not inv_refurb +*therefore, the capacity indicated by the supply curve may be limiting +*but the plant can still be refurbished +*Also note the flag_eq_rsc_INVlim--its calculation needs to be updated if this equation +*is changed +eq_rsc_INVlim(r,i,rscbin,t)$[tmodel(t) + $rsc_i(i) + $m_rscfeas(r,i,rscbin) + $m_rsc_con(r,i) + $(not flag_eq_rsc_INVlim(r,i,rscbin,t)) + $(not Sw_PCM)].. +*With water constraints, some RSC techs are expanded to include cooling technologies +*but the combination of m_rsc_con and rsc_agg allows for those investments +*to be limited by the numeraire techs' m_rsc_dat + +*capacity indicated by the resource supply curve (scaled by rsc_capacity_scalar) + m_rsc_dat(r,i,rscbin,"cap")$[not evmc(i)] * ( + 1$[not rsc_capacity_scalar_i(i)] + rsc_capacity_scalar(i,r,t)$rsc_capacity_scalar_i(i)) +* available hydro upgrade capacity + + hyd_add_upg_cap(r,i,rscbin,t)$(Sw_HydroCapEnerUpgradeType=1) + + =g= + +*must exceed the cumulative invested capacity in that region/class/bin... + sum{(ii,v,tt)$[valinv(ii,v,r,tt)$(yeart(tt) <= yeart(t))$rsc_agg(i,ii)], + INV_RSC(ii,v,r,rscbin,tt) * resourcescaler(ii) } + +*plus exogenous (pre-start-year) capacity, using its level in the first year (tfirst) + + sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], + capacity_exog_rsc(ii,v,r,rscbin,tt) } + +; + +* --------------------------------------------------------------------------- + +eq_growthlimit_relative(i,st,t)$[sum{r$[r_st(r,st)], valinv_irt(i,r,t) } + $tmodel(t) + $stfeas(st) + $Sw_GrowthPenalties + $(yeart(t)<=Sw_GrowthPenLastYear) + $(yeart(t)>=model_builds_start_yr) + $(not Sw_PCM)].. + +*the annual growth limit + (yeart(t) - sum{tt$[tprev(t,tt)], yeart(tt) }) + * sum{gbin, GROWTH_BIN(gbin,i,st,t) } + + =g= + +* must exceed the current periods investment + sum{(v,r)$[valinv(i,v,r,t)$r_st(r,st)], + INV(i,v,r,t) } +; + +* --------------------------------------------------------------------------- + +eq_growthbin_limit(gbin,st,tg,t)$[valinv_tg(st,tg,t) + $tmodel(t) + $stfeas(st) + $Sw_GrowthPenalties + $(yeart(t)<=Sw_GrowthPenLastYear) + $(yeart(t)>=model_builds_start_yr) + $(not Sw_PCM)].. + +*the growth bin limit + growth_bin_limit(gbin,st,tg,t) + + =g= + +* must exceed the value in the growth bin + sum{i$tg_i(tg,i), GROWTH_BIN(gbin,i,st,t) } +; + +* --------------------------------------------------------------------------- + +eq_growthlimit_absolute(tg,t)$[growth_limit_absolute(tg)$tmodel(t) + $Sw_GrowthAbsCon$(yeart(t)<=Sw_GrowthConLastYear) + $(yeart(t)>=model_builds_start_yr) + $(not Sw_PCM)].. + +* the absolute limit of growth (in MW) + (sum{tt$[tprev(tt,t)], yeart(tt) } - yeart(t)) + * growth_limit_absolute(tg) + + =g= + +* must exceed the total investment + sum{(i,v,r)$[valinv(i,v,r,t)$tg_i(tg,i)], + INV(i,v,r,t) } +; + +* --------------------------------------------------------------------------- +* If using hybrid generators with endogenous spur lines, the available power from +* a reV site is limited by the CF and capacity of constituent resources at that site +eq_site_cf(x,h,t) + $[tmodel(t) + $Sw_SpurScen + $xfeas(x)].. + + sum{(i,v,r) + $[spur_techs(i) + $x_r(x,r) + $valgen(i,v,r,t)], +* Capacity factor of techs with endogenously-modeled spur lines + m_cf(i,v,r,h,t) +* multiplied by total capacity of those techs + * sum{rscbin + $[valcap(i,v,r,t) + $m_rscfeas(r,i,rscbin) + $spurline_sitemap(i,r,rscbin,x)], + CAP_RSC(i,v,r,rscbin,t) + } + } + + =g= + + AVAIL_SITE(x,h,t) +; + +* --------------------------------------------------------------------------- +* If using hybrid generators with endogenous spur lines, each wind and solar generator +* is associated with a specific reV site x. The available generation from all generators +* at site x is limited to the spur-line capacity built to site x. +eq_spurclip(x,h,t) + $[Sw_SpurScen + $xfeas(x) + $tmodel(t)].. + +* Capacity of spur line to reV site limits the available generation at that site + CAP_SPUR(x,t) + + =g= + + AVAIL_SITE(x,h,t) +; + +* --------------------------------------------------------------------------- +* If spur-line sharing is disabled, the capacity of the spur line for site x +* must be >= the capacity of the hybrid resources (wind and solar) installed at site x +eq_spur_noclip(x,t) + $[Sw_SpurScen + $(not Sw_SpurShare) + $xfeas(x) + $tmodel(t)].. + +* Capacity of spur line to site x + CAP_SPUR(x,t) + + =g= + +* must be >= to the wind/solar capacity installed at x +* (Since PV capacity is in DC, we divide CAP_RSC [DC] by ILR [DC/AC] to get AC spur line capacity. +* ILR is 1 for all non-PV techs.) + sum{(i,v,r,rscbin) + $[spurline_sitemap(i,r,rscbin,x) + $valcap(i,v,r,t)], + CAP_RSC(i,v,r,rscbin,t) / ilr(i) + } +; + +* --------------------------------------------------------------------------- + +*capacity must be greater than supply +*dispatchable hydro is accounted for both in this constraint and in eq_dhyd_dispatch +*this constraint does not apply to storage nor hybrid plant +* limits for storage (including storage of hybrid plants) are tracked in eq_storage_capacity +* limits for plant of Hybrid Plant are tracked in eq_plant_energy_balance +* limits for hybrid techs with shared spur lines are treated in eq_capacity_limit_hybrid +eq_capacity_limit(i,v,r,h,t) + $[tmodel(t)$valgen(i,v,r,t) + $(not spur_techs(i)) + $(not storage_standalone(i))$(not storage_hybrid(i)$(not csp(i)))$(not nondispatch(i))].. + +*total amount of dispatchable, non-hydro capacity + avail(i,r,h)$[dispatchtech(i)$(not hydro_d(i))] + * derate_geo_vintage(i,v) + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + * CAP(i,v,r,t) + +*total amount of dispatchable hydro capacity + + avail(i,r,h)$hydro_d(i) + * CAP(i,v,r,t) + * sum{szn$h_szn(h,szn), cap_hyd_szn_adj(i,szn,r) } + * (1 + hydro_capcredit_delta(i,t)$[h_stress(h)]) + +*sum of non-dispatchable capacity multiplied by its rated capacity factor, +*only vre technologies are curtailable. +* This term accounts for energy-only and capacity-only upsizing, +* which is initially implemented only for hydro. + + (m_cf(i,v,r,h,t) + * (CAP(i,v,r,t) +*add energy embedded in energy-only upsizing + + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))], + INV_ENER_UP(i,v,r,rscbin,tt)$allow_ener_up(i,v,r,rscbin,tt) +*subtract energy that would be embedded in a capacity-only upsizing + - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt)$allow_cap_up(i,v,r,rscbin,tt) }) + )$[not dispatchtech(i)] +*add EVMC shape generation + + (evmc_shape_gen(i,r,h) * CAP(i,v,r,t)) + + =g= + +*must exceed generation + GEN(i,v,r,h,t) + +*[plus] sum of operating reserves by type + + sum{ortype$[Sw_OpRes$reserve_frac(i,ortype)$opres_h(h)$opres_model(ortype)], + OPRES(ortype,i,v,r,h,t) } + +*[plus] power consumed for flexible ccs + + CCSFLEX_POW(i,v,r,h,t) $[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)] +; + +* --------------------------------------------------------------------------- +* For hybrid resources, the sum of generation from constituent resources is +* limited by the available generation at that site, which is defined in +* eq_site_cf and eq_spurclip. +eq_capacity_limit_hybrid(r,h,t) + $[tmodel(t) + $Sw_SpurScen].. + +* Sum of available generation across reV sites in BA + sum{x$[x_r(x,r)$xfeas(x)], AVAIL_SITE(x,h,t)} + + =g= + +* is >= the actual generation and operating reserves from all the hybrid resources in that BA + sum{(i,v) + $[spur_techs(i) + $valgen(i,v,r,t)], + GEN(i,v,r,h,t) + + sum{ortype$[Sw_OpRes$opres_model(ortype)$reserve_frac(i,ortype)$opres_h(h)], + OPRES(ortype,i,v,r,h,t)} + } +; + +* --------------------------------------------------------------------------- + +eq_capacity_limit_nd(i,v,r,h,t)$[tmodel(t)$valgen(i,v,r,t)$nondispatch(i)].. + +*sum of non-dispatchable capacity multiplied by its rated capacity factor, + + m_cf(i,v,r,h,t) * CAP(i,v,r,t) + + =e= + +*must be equal to generation + GEN(i,v,r,h,t) + +*[plus] sum of operating reserves by type + + sum{ortype$[Sw_OpRes$opres_model(ortype)$reserve_frac(i,ortype)$opres_h(h)], + OPRES(ortype,i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +eq_curt_gen_balance(r,h,t)$tmodel(t).. + +*total potential generation + sum{(i,v)$[valcap(i,v,r,t)$(vre(i) or storage_hybrid(i)$(not csp(i)))$(not nondispatch(i))], + m_cf(i,v,r,h,t) * CAP(i,v,r,t) } + +*[minus] curtailed generation + - CURT(r,h,t)$Sw_CurtMarket + + =g= + +*must exceed realized generation; exclude hybrid plants + sum{(i,v)$[valgen(i,v,r,t)$vre(i)$(not nondispatch(i))], GEN(i,v,r,h,t) } + +*[plus] realized generation from hybrid plant + + sum{(i,v)$[valgen(i,v,r,t)$storage_hybrid(i)$(not csp(i))$(not nondispatch(i))], GEN_PLANT(i,v,r,h,t) }$Sw_HybridPlant + +*[plus] sum of operating reserves by type + + sum{(ortype,i,v)$[Sw_OpRes$reserve_frac(i,ortype)$opres_h(h)$valgen(i,v,r,t)$vre(i)$(not nondispatch(i))$opres_model(ortype)], + OPRES(ortype,i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- +* Generation in each timeslice must be greater than mingen_fixed * available capacity +eq_mingen_fixed(i,v,r,h,t) + $[Sw_MingenFixed$tmodel(t)$mingen_fixed(i)$valgen(i,v,r,t) + $(yeart(t)>=Sw_StartMarkets)].. + + GEN(i,v,r,h,t) + + =g= + + mingen_fixed(i) * avail(i,r,h) * CAP(i,v,r,t) +; + +* --------------------------------------------------------------------------- + +eq_mingen_lb(r,h,szn,t)$[h_szn(h,szn)$(yeart(t)>=this_year) + $tmodel(t)$Sw_Mingen].. + +*minimum generation level in a season + MINGEN(r,szn,t) + + =g= + +*must be greater than the minimum generation level in each time slice in that season + sum{(i,v)$[valgen(i,v,r,t)$minloadfrac(r,i,h)], GEN(i,v,r,h,t) * minloadfrac(r,i,h) } +; + +* --------------------------------------------------------------------------- + +eq_mingen_ub(r,h,szn,t)$[h_szn(h,szn)$(yeart(t)>=this_year) + $tmodel(t)$Sw_Mingen].. + +*generation in each timeslice in a season + sum{(i,v)$[valgen(i,v,r,t)$minloadfrac(r,i,h)], GEN(i,v,r,h,t) } + + =g= + +*must be greater than the minimum generation level + MINGEN(r,szn,t) +; + +* --------------------------------------------------------------------------- + +*requirement for fleet of a given tech to have a minimum annual capacity factor +eq_min_cf(i,r,t)$[minCF(i,t)$tmodel(t)$valgen_irt(i,r,t)$Sw_MinCF].. + + sum{(v,h)$[valgen(i,v,r,t)$h_rep(h)], hours(h) * GEN(i,v,r,h,t) } + + =g= + + sum{v$valgen(i,v,r,t), CAP(i,v,r,t) } * sum{h$h_rep(h), hours(h) } * minCF(i,t) +; + +* Maximum allowed daily capacity factor +eq_max_daily_cf(i,r,szn,t)$[maxdailycf(i,t) + $sum{h$h_szn(h,szn), avail(i,r,h)} + $tmodel(t)$valgen_irt(i,r,t)$Sw_MaxDailyCF].. + + sum{v$valgen(i,v,r,t), CAP(i,v,r,t) } * sum{h$h_szn(h,szn), hours(h) * avail(i,r,h) } * maxdailycf(i,t) + + =g= + + sum{(v,h)$[valgen(i,v,r,t)$h_szn(h,szn)], hours(h) * GEN(i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +* Seasonal energy constraint for dispatchable hydropower +eq_dhyd_dispatch(i,v,r,szn,t) + $[tmodel(t)$hydro_d(i)$valgen(i,v,r,t) + ].. + +*seasonal hours [times] seasonal capacity factor [times] total hydro capacity [times] seasonal capacity adjustment + sum{h$[h_szn(h,szn)], hours(h) } + * (CAP(i,v,r,t) + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))], + INV_ENER_UP(i,v,r,rscbin,tt)$allow_ener_up(i,v,r,rscbin,tt) + - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt)$allow_cap_up(i,v,r,rscbin,tt) }) + * m_cf_szn(i,v,r,szn,t) + + =g= + +*total seasonal generation plus fraction of energy for regulation + sum{h$[h_szn(h,szn)], + hours(h) + * (GEN(i,v,r,h,t) + + reg_energy_frac * ( + OPRES("reg",i,v,r,h,t)$[Sw_OpRes=1] + + OPRES("combo",i,v,r,h,t)$[Sw_OpRes=2] + )$[opres_h(h)] + ) + } +; + +* --------------------------------------------------------------------------------------- +* Limit near-term capacity deployments by tech and region based on interconnection queues +eq_interconnection_queues(tg,r,t) + $[tmodel(t)$(yeart(t)>=model_builds_start_yr) + $(sum{(tgg,rr), cap_limit(tgg,rr,t)}) + $sum{(i,newv)$tg_i(tg,i), valinv(i,newv,r,t)} + $(not Sw_PCM)].. + +* the capacity limit from the interconnection queue data +* (with CAP_ABOVE_LIM as a slack variable to address infeasibilities) + cap_limit(tg,r,t) + CAP_ABOVE_LIM(tg,r,t) + + =g= + +* must be greater than the total capacity deployed since the +* start of the interconnection queue data + sum{(i,newv,tt)$[valinv(i,newv,r,tt)$tg_i(tg,i) + $(yeart(tt)>=interconnection_start) + $(tmodel(tt) or tfix(tt))], + INV(i,newv,r,tt) + INV_REFURB(i,newv,r,tt)$[refurbtech(i)$Sw_Refurb] } +; + +*=============================== +* --- SUPPLY DEMAND BALANCE --- +*=============================== + +* --------------------------------------------------------------------------- + +* The treatment of power flow along DC lines depends on the type of AC/DC converter used. +* LCC DC lines are single point-to-point lines connected to the AC grid on either end, and +* as such are treated like AC lines (with different costs/losses). +* VSC DC lines are part of a multi-terminal DC network; DC power can flow through a node +* without converting to AC and incurring DC/AC/DC losses. Power flow along VSC lines is +* therefore treated separately through the CONVERSION variable and eq_vsc_flow equation. +eq_supply_demand_balance(r,h,t)$tmodel(t).. + +* generation from all land-based sources, including storage discharge + sum{(i,v)$[valgen(i,v,r,t)$land(r)], GEN(i,v,r,h,t) } + +* [plus] net AC and LCC DC transmission with imports reduced by losses + + sum{(trtype,rr)$[routes(rr,r,trtype,t)$notvsc(trtype)], + (1-tranloss(rr,r,trtype)) * FLOW(rr,r,h,t,trtype) } + - sum{(trtype,rr)$[routes(r,rr,trtype,t)$notvsc(trtype)], + FLOW(r,rr,h,t,trtype) } + +* [plus] net AC/DC conversion through VSC converter stations +* Note that we only need "AC" in the CONVERSION variable (not LCC, B2B, etc) +* since all it does here is act as a catch-all for "not VSC" + + (CONVERSION(r,h,"VSC","AC",t) * converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] + - (CONVERSION(r,h,"AC","VSC",t) / converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] + +* [minus] storage charging; not hybrid+storage + - sum{(i,v)$[valcap(i,v,r,t)$(storage_standalone(i) or hyd_add_pump(i))], STORAGE_IN(i,v,r,h,t) } + +* [minus] energy into storage for hybrid+storage from grid + - sum{(i,v)$[valcap(i,v,r,t)$storage_hybrid(i)$(not csp(i))], STORAGE_IN_GRID(i,v,r,h,t) }$Sw_HybridPlant + +* [plus] dropped/excess load ONLY if before Sw_StartMarkets + + DROPPED(r,h,t)$[(yeart(t)=model_builds_start_yr) + $tmodel(t)$hour_szn_group(h,hh)$Sw_MinLoading].. + + GEN(i,v,r,h,t) + + =g= + + GEN(i,v,r,hh,t) * minloadfrac(r,i,hh) +; + +* RAMPUP is used in the calculation of startup/ramping costs +* Because RAMPUP has a positive cost, RAMPUP will always either be 0 +* when the RHS is negative, or will be exactly equal to the RHS when +* the RHS is positive. +eq_ramping(i,r,h,hh,t) + $[Sw_StartCost$tmodel(t)$startcost(i)$numhours_nexth(h,hh)$valgen_irt(i,r,t)].. + + RAMPUP(i,r,h,hh,t) + + =g= + + sum{v$valgen(i,v,r,t), GEN(i,v,r,hh,t) - GEN(i,v,r,h,t) } +; + +*======================================= +* --- OPERATING RESERVE CONSTRAINTS --- +*======================================= + +* --------------------------------------------------------------------------- + +*generation must occur at some point during the szn (i.e., day) +*in order to procure operating reserves from that resource +*ORPRES for storage is limited by the storage capacity per the constraint "eq_storage_capacity" +eq_ORCap_large_res_frac(ortype,i,v,r,h,t) + $[tmodel(t)$valgen(i,v,r,t)$Sw_OpRes$opres_model(ortype)$opres_h(h) + $(reserve_frac(i,ortype)>0.5)$(not storage_standalone(i))$(not hyd_add_pump(i))].. + +*the reserve_frac times... + reserve_frac(i,ortype) * ( +* the amount of committed capacity available for a season is assumed to be the amount +* of generation from the timeslice that has the highest demand + sum{(szn,hh)$[h_szn(h,szn)$maxload_szn(r,hh,t,szn)], + GEN(i,v,r,hh,t) }) + + =g= + +*note the reserve_frac applies to each opres by type + OPRES(ortype,i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + +*for plants with reserve_frac <= 0.5 (but nonzero), generation must occur during the timeslice +*in which reserves are provided +eq_ORCap_small_res_frac(ortype,i,v,r,h,t) + $[tmodel(t)$valgen(i,v,r,t)$Sw_OpRes$opres_model(ortype)$opres_h(h) + $(reserve_frac(i,ortype)<=0.5)$reserve_frac(i,ortype)].. + +*generation + GEN(i,v,r,h,t) + + =g= + +*must be greater than the operating reserves procured + OPRES(ortype,i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + +*operating reserves must meet the operating reserves requirement (by ortype) +eq_OpRes_requirement(ortype,r,h,t) + $[tmodel(t)$Sw_OpRes$opres_model(ortype)$opres_h(h)].. + +*operating reserves from technologies that can produce them (i.e. those w/ramp rates) + + sum{(i,v)$[valgen(i,v,r,t)$reserve_frac(i,ortype)], + OPRES(ortype,i,v,r,h,t) } + +*[plus] net transmission of operating reserves (while including losses for imports) + + sum{rr$opres_routes(rr,r,t), (1 - tranloss(rr,r,"AC")) * OPRES_FLOW(ortype,rr,r,h,t) } + - sum{rr$opres_routes(r,rr,t), OPRES_FLOW(ortype,r,rr,h,t) } + +*[plus] dropped load (operating reserves) ONLY if before Sw_StartMarkets + + DROPPED(r,h,t)$[(yeart(t)=model_builds_start_yr) + $Sw_PRM_CapCredit + $(not Sw_PCM)].. + +* forced_retire is used here because forced_retire capacity is removed from valgen +* but not valcap. It remains in valcap to allow for upgrades, but if it is not upgraged +* it should not be allowed to provide capacity to meet the reserve margin constraint. +* Because it can provide no services, it will either upgrade or retire. + +*[plus] sum of all non-rsc and non-storage capacity + + sum{(i,v)$[valcap(i,v,r,t)$(not vre(i))$(not hydro(i))$(not storage(i))$(not consume(i))$(not forced_retire(i,r,t))], + CAP(i,v,r,t) + * (1 + ccseason_cap_frac_delta(i,v,r,ccseason,t)) + } + +*[plus] firm capacity from existing VRE or CSP +*only used in sequential solve case (otherwise cc_old = 0) + + sum{i$[(vre(i) or csp(i) or pvb(i))$(not forced_retire(i,r,t))], + cc_old(i,r,ccseason,t) + } + +*[plus] marginal capacity credit of VRE and csp times new investment +*only used in sequential solve case (otherwise m_cc_mar = 0) +*Note: new distpv is included with cc_old + + sum{(i,v)$[(vre(i) or csp(i) or pvb(i))$valinv(i,v,r,t)$(not forced_retire(i,r,t))], + m_cc_mar(i,r,ccseason,t) * (INV(i,v,r,t) + INV_REFURB(i,v,r,t)$[refurbtech(i)$Sw_Refurb]) + } + +*[plus] firm capacity contribution from all binned storage capacity +*battery and pumped-hydro +*excludes hydro upgraded to add pumps + + sum{(i,v,sdbin)$[(storage_standalone(i) or hyd_add_pump(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], + cc_storage(i,sdbin) * CAP_SDBIN(i,v,r,ccseason,sdbin,t) + } +*hybrid PV+battery + + sum{(i,v,sdbin)$[storage_hybrid(i)$(not csp(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], + cc_storage(i,sdbin) * hybrid_cc_derate(i,r,ccseason,sdbin,t) * CAP_SDBIN(i,v,r,ccseason,sdbin,t) + } + +*[plus] average capacity credit times capacity of VRE and storage +*used in rolling window and full intertemporal solve (otherwise cc_int = 0) + + sum{(i,v)$[(vre(i) or storage(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], + cc_int(i,v,r,ccseason,t) * CAP(i,v,r,t) + } + +*[plus] excess capacity credit +*used in rolling window and full intertemporal solve when using marginals for cc_int (otherwise cc_excess = 0) + + sum{i$[(vre(i) or storage(i))$(not forced_retire(i,r,t))], + cc_excess(i,r,ccseason,t) + } + +*[plus] firm capacity of non-dispatchable hydro +* nb: hydro_nd generation does not fluctuate +* within a seasons set of hours + + sum{(i,v,h)$[hydro_nd(i)$valgen(i,v,r,t)$h_ccseason_prm(h,ccseason)], + GEN(i,v,r,h,t) + } + +*[plus] dispatchable hydro firm capacity +* include hydro upgraded to add pumps + + sum{(i,v)$[(hydro_d(i) or hyd_add_pump(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], + CAP(i,v,r,t) * cap_hyd_ccseason_adj(i,ccseason,r) * (1 + hydro_capcredit_delta(i,t)) + } + +*[plus] imports of firm capacity through AC and LCC DC lines + + sum{(rr,trtype)$[routes(rr,r,trtype,t)$routes_prm(rr,r)$notvsc(trtype)], + (1 - tranloss(rr,r,trtype)) * PRMTRADE(rr,r,trtype,ccseason,t) + } + +*[minus] exports of firm capacity through AC and LCC DC lines + - sum{(rr,trtype)$[routes(r,rr,trtype,t)$routes_prm(r,rr)$notvsc(trtype)], + PRMTRADE(r,rr,trtype,ccseason,t) + } + +*[plus] net AC/DC conversion of firm capacity through VSC converters + + (CONVERSION_PRM(r,ccseason,"VSC","AC",t) * converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] + - (CONVERSION_PRM(r,ccseason,"AC","VSC",t) / converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] + + =g= + +*[plus] the peak demand times the planning reserve margin + + ( + peakdem_static_ccseason(r,ccseason,t) + +* + PEAK_FLEX(r,ccseason,t)$Sw_EFS_flex + +* [plus] only steam methane reforming technologies are assumed to increase peak demand +* contribution to peak demand based on weighted-average across timeslices in each ccseason +* [metric tons/hour] / [metric tons/MWh] * [hours] / [hours] = [MW] + + (sum{(p,i,v,h)$[smr(i)$valcap(i,v,r,t)$frac_h_ccseason_weights(h,ccseason) + $(sameas(p,"H2"))$i_p(i,p)$(not sameas(i,"dac_gas"))$h_rep(h)], + PRODUCE(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) + * hours(h) * frac_h_ccseason_weights(h,ccseason) } + / sum{h$[frac_h_ccseason_weights(h,ccseason)$h_rep(h)], + hours(h) * frac_h_ccseason_weights(h,ccseason) } + )$Sw_Prod + + ) * (1 + prm(r,t)) +; + +* --------------------------------------------------------------------------- + +*================================ +* --- TRANSMISSION CAPACITY --- +*================================ + +* DC transmission additions are assumed to add the same capacity in both directions +eq_INVTRAN_DC(r,rr,trtype,t) + $[routes_inv(r,rr,trtype,t) + $(not aclike(trtype)) + $tmodel(t) + $(not Sw_PCM)].. + + INVTRAN(r,rr,trtype,t) + + =e= + + INVTRAN(rr,r,trtype,t) +; + +* --------------------------------------------------------------------------- +* Added AC transmission capacity (INVTRAN) is determined by the cumulative capex invested +* in the interface (TRAN_CAPEX_BINS). This backwards approach is used because investment +* in the interface (corresponding to a particular upgraded line or transformer) can add a different +* amount of MW to the forward and reverse directions, and because the bins must be filled +* in order. +* This approach is only used for AC capacity; DC and B2B capacity additions are tracked directly. +* Defined only for interfaces (r < rr) +eq_INVTRAN_AC_forward(r,rr,tscbin,t) + $[routes_inv(r,rr,"AC",t) + $tmodel(t) + $tsc_binwidth(r,rr,tscbin) + $(not Sw_PCM)].. +* Transmission capacity additions [MW] times bin cost [$/MW] = [$] + sum{tt + $[(yeart(tt) <= yeart(t)) + $(tmodel(tt) or tfix(tt)) + $routes_inv(r,rr,"AC",tt)], + INVTRAN_AC(r,rr,tscbin,tt) * tsc_forward(r,rr,tscbin) + } + + =e= +* Cumulative transmission capacity investments: [$] + TRAN_CAPEX_BINS(r,rr,tscbin,t) +; + +* --------------------------------------------------------------------------- +* Defined only for interfaces (r < rr) +eq_INVTRAN_AC_reverse(r,rr,tscbin,t) + $[routes_inv(r,rr,"AC",t) + $tmodel(t) + $tsc_binwidth(r,rr,tscbin) + $(not Sw_PCM)].. +* Transmission capacity additions [MW] times bin cost [$/MW] = [$] + sum{tt + $[(yeart(tt) <= yeart(t)) + $(tmodel(tt) or tfix(tt)) + $routes_inv(r,rr,"AC",tt)], + INVTRAN_AC(rr,r,tscbin,tt) * tsc_reverse(r,rr,tscbin) + } + + =e= +* Cumulative transmission capacity investments: [$] + TRAN_CAPEX_BINS(r,rr,tscbin,t) +; + +* --------------------------------------------------------------------------- +* Defined for both directions (r < rr and r > rr) +eq_INVTRAN_AC(r,rr,t) + $[routes_inv(r,rr,"AC",t) + $tmodel(t) + $(not Sw_PCM)].. + + INVTRAN(r,rr,"AC",t) + + =e= + + sum{tscbin, INVTRAN_AC(r,rr,tscbin,t) } +; + +* --------------------------------------------------------------------------- +* AC transmission investment bins are limited by the transmission upgrade supply curve +* Defined only for interfaces (r < rr) +eq_TRAN_CAPEX_BINS(r,rr,tscbin,t) + $[routes_inv(r,rr,"AC",t) + $tmodel(t) + $tsc_binwidth(r,rr,tscbin) + $(not Sw_PCM)].. + + tsc_binwidth(r,rr,tscbin) + + =g= + + TRAN_CAPEX_BINS(r,rr,tscbin,t) +; + +* --------------------------------------------------------------------------- +* Exogenous transmission investments are included in INVTRAN +* Defined for both directions (r < rr and r > rr) +eq_invtran_exog(r,rr,trtype,t) + $[routes_inv(r,rr,trtype,t) + $tmodel(t) + $invtran_exog(r,rr,trtype,t) + $(not Sw_PCM)].. + + INVTRAN(r,rr,trtype,t) + + =g= + + invtran_exog(r,rr,trtype,t) +; + +* --------------------------------------------------------------------------- +* Transmission capacity accumulates capacity investments from years up to present +* Defined for both directions (r < rr and r > rr) +eq_CAPTRAN_ENERGY(r,rr,trtype,t) + $[routes(r,rr,trtype,t) + $tmodel(t) + $(not Sw_PCM)].. + + CAPTRAN_ENERGY(r,rr,trtype,t) + + =e= + +* [plus] initial transmission capacity + + trancap_init_energy(r,rr,trtype) + +* [plus] capacity additions up to and including the present year + + sum{tt + $[(yeart(tt) <= yeart(t)) + $(tmodel(tt) or tfix(tt)) + $routes_inv(r,rr,trtype,tt)], + INVTRAN(r,rr,trtype,tt) + } +; + +* --------------------------------------------------------------------------- +* Transmission capacity for PRM trading (derated by Sw_TransInvPRMderate) +* accumulates capacity investments from years up to present. +* Defined for both directions (r < rr and r > rr) +eq_CAPTRAN_PRM(r,rr,trtype,t) + $[routes(r,rr,trtype,t) + $routes_prm(r,rr) + $tmodel(t) + $(not Sw_PCM)].. + + CAPTRAN_PRM(r,rr,trtype,t) + + =e= + +* [plus] initial transmission capacity + + trancap_init_prm(r,rr,trtype) + +* [plus] capacity additions up to and including the present year, +* derated by Sw_TransInvPRMderate + + sum{tt + $[(yeart(tt) <= yeart(t)) + $(tmodel(tt) or tfix(tt)) + $routes_inv(r,rr,trtype,tt)], + INVTRAN(r,rr,trtype,tt) + * (1 - Sw_TransInvPRMderate) + } +; + +* --------------------------------------------------------------------------- + +eq_prescribed_transmission(r,rr,trtype,t) + $[routes_inv(r,rr,trtype,t) + $tmodel(t)$(yeart(t)=RGGI_start_yr)$Sw_RGGI].. + + RGGI_cap(t) + + =g= + + sum{r$RGGI_r(r), EMIT("process","CO2",r,t) } +; + +* --------------------------------------------------------------------------- + +eq_state_cap(st,t) + $[tmodel(t) + $(yeart(t)>=state_cap_start_yr) + $sum{tt, state_cap(st,tt) } + $Sw_StateCap].. + + state_cap(st,t) + + =g= + + sum{r$r_st(r,st), EMIT("process","CO2",r,t) } + +* Import emissions intensity is taken from the previous solve year. +* Here the receiving regions (r) are the cap regions and the sending +* regions (rr) are those that have connection with cap regions. + + sum{(h,r,rr,trtype) + $[r_st(r,st)$(not r_st(rr,st))$routes(rr,r,trtype,t) + $h_rep(h) +* If there is a national zero-carbon cap in the present year, +* set emissions intensity of imports to zero. + $(not ((Sw_AnnualCap>0) and not emit_cap("CO2",t) and not emit_cap("CO2e",t)))], + hours(h) * FLOW(rr,r,h,t,trtype) + * sum{tt$tprev(t,tt), co2_emit_rate_r(rr,tt) } + } +; + +* --------------------------------------------------------------------------- + +* traded emissions among states in each trading group need +* to be less than the sum of all the state caps within that trading group +eq_CSAPR_Budget(csapr_group,t)$[Sw_CSAPR$tmodel(t)$(yeart(t)>=csapr_startyr)].. + +*the accumulation of states csapr cap for the budget category + sum{st$[stfeas(st)$csapr_group_st(csapr_group,st)], csapr_cap(st,"budget",t) } + + =g= + +*must exceed the summed-over-state hourly-weighted nox emissions by csapr group + sum{st$csapr_group_st(csapr_group,st), + sum{(i,v,h,r)$[r_st(r,st)$valgen(i,v,r,t)$h_rep(h)], + h_weight_csapr(h) * hours(h) * emit_rate("process","NOX",i,v,r,t) * GEN(i,v,r,h,t) + } + } +; + +* --------------------------------------------------------------------------- + +* along with the cap on trading groups, each state has +* a maximum amount of NOX emissions during ozone season +eq_CSAPR_Assurance(st,t)$[stfeas(st)$(yeart(t)>=csapr_startyr) + $csapr_cap(st,"Assurance",t)$tmodel(t)].. + +*the state level assurance cap + csapr_cap(st,"assurance",t) + + =g= + +*must exceed the csapr-hourly-weighted nox emissions by state + sum{(i,v,h,r)$[r_st(r,st)$valgen(i,v,r,t)$h_rep(h)], + h_weight_csapr(h) * hours(h) * emit_rate("process","NOX",i,v,r,t) * GEN(i,v,r,h,t) + } +; + +* --------------------------------------------------------------------------- +* This constraint has no input data and is currently unused +eq_emit_rate_limit(e,r,t)$[emit_rate_con(e,r,t)$tmodel(t)].. + + emit_rate_limit(e,r,t) * ( + sum{(i,v,h)$[valgen(i,v,r,t)$h_rep(h)], hours(h) * GEN(i,v,r,h,t) } + ) + + =g= + + EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream +; + +* --------------------------------------------------------------------------- +* This equation enforces emission caps for both the CO2 (Sw_AnnualCap = 1) +* and CO2e emission (Sw_AnnualCap =2 or Sw_AnnualCap =3) scenarios +eq_annual_cap(eall,t)$[sum{tt, emit_cap(eall,tt) }$tmodel(t)$(Sw_AnnualCap>0)].. + +* exogenous CO2 cap (Sw_AnnualCap = 1) or CO2e cap (Sw_AnnualCap > 1) + emit_cap(eall,t) + + =g= + +* must exceed annual endogenous emissions by CO2 pollutant (when Sw_AnnualCap=1) or CO2e pollutants (CO2, CH4, NO2 pollutants when Sw_AnnualCap=2 and additional H2 leakage when Sw_AnnualCap=3) + sum{(e,r)$emit_capped(e), (EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream) * gwp(e) } +; + +* --------------------------------------------------------------------------- + +eq_bankborrowcap(e)$[Sw_BankBorrowCap$sum{t, emit_cap(e,t) }].. + +*weighted exogenous emissions + sum{t$[tmodel(t)$emit_cap(e,t)], + yearweight(t) * emit_cap(e,t) } + + =g= + +* must exceed weighted endogenous emissions + sum{(r,t)$[tmodel(t)$emit_cap(e,t)], + yearweight(t) * (EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream)} +; + +* --------------------------------------------------------------------------- + +eq_cdr_cap(t) + $[tmodel(t) + $(Sw_AnnualCap>0) + $Sw_NoFossilOffsetCDR].. + +*** GHG emissions from fossil CCS... +* CO2 emissions from fossil CCS... + + sum{(i,v,r,h)$[valgen(i,v,r,t)$ccs(i)$(not beccs(i))$h_rep(h)$(Sw_AnnualCap<2)], + hours(h) * GEN(i,v,r,h,t) * (emit_rate("process","CO2",i,v,r,t) + emit_rate("upstream","CO2",i,v,r,t)$Sw_Upstream) } +* GHG emissions * global warming potential + + sum{(i,v,r,h)$[valgen(i,v,r,t)$ccs(i)$(not beccs(i))$h_rep(h)$(Sw_AnnualCap>=2)], + hours(h) * GEN(i,v,r,h,t) * sum{e, (emit_rate("process",e,i,v,r,t) + emit_rate("upstream",e,i,v,r,t)$Sw_Upstream) * gwp(e) } } + =g= + +*** ...must be greater than emissions offset by CDR (negative emissions so negative signs here) +** DAC + - sum{(p,i,v,r,h)$[valcap(i,v,r,t)$i_p(i,p)$dac(i)$sameas(p,"DAC")$h_rep(h)], + hours(h) * (prod_emit_rate("process","CO2",i,t) + prod_emit_rate("upstream","CO2",i,t)$Sw_Upstream) * PRODUCE(p,i,v,r,h,t) } +** BECCS + - sum{(i,v,r,h)$[valgen(i,v,r,t)$beccs(i)$h_rep(h)], + hours(h) * (emit_rate("process","CO2",i,v,r,t) + emit_rate("upstream","CO2",i,v,r,t)$Sw_Upstream) * GEN(i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +*Under the Clean Air Act Section 111, new gas plants (CCs or CTs) can operate above caa_gas_max_cf percent before caa_coal_retire_year +*During and after caa_coal_retire_year, they need to either A) operate at <= caa_gas_max_cf percent CF, B) upgrade with CCS or C) retire +eq_caa_max_cf(i,v,r,t)$[tmodel(t)$valgen(i,v,r,t) + $gas(i)$(not ccs(i)) + $heat_rate(i,v,r,t) + $(firstyear_v(i,v)>=caa_first_year) + $(yeart(t)>=caa_coal_retire_year) + $Sw_Clean_Air_Act].. + +*fraction of annual generation capacity + sum{h$h_rep(h), hours(h)} * caa_gas_max_cf * CAP(i,v,r,t) + + + =g= + +*must exceed total annual generation + sum{h$h_rep(h), hours(h) * GEN(i,v,r,h,t)} +; + +* --------------------------------------------------------------------------- + +*Under the Clean Air Act Section 111, the emissions from existing coal plants per state must be less than or equal to a rate-based emissions standard + +*The rate is equivalent to average coal CCS emissions assuming 90% capture rate [metric tons CO2 / MWh] +eq_caa_rate_standard(st,t)$[tmodel(t) + $(yeart(t)>=caa_coal_retire_year) + $Sw_Clean_Air_Act].. + +*rate equivalent to average coal CCS emissions assuming 90% capture rate [metric tons CO2 / MWh] + caa_rate_emis_standard + +*coal generation in that state [MWh] + * sum{(i,v,r,h)$[valgen(i,v,r,t)$coal(i)$(not cofire(i))$r_st(r,st)], + GEN(i,v,r,h,t)} + =g= + +*coal emissions in that state [metric tons CO2] + sum{(i,v,r,h)$[valgen(i,v,r,t)$coal(i)$(not cofire(i))$r_st(r,st)], + GEN(i,v,r,h,t) * emit_rate("process","CO2",i,v,r,t)} +; + +*========================== +* --- RPS CONSTRAINTS --- +*========================== + +* --------------------------------------------------------------------------- + +eq_REC_Generation(RPSCat,i,st,t)$[stfeas(st)$(not tfirst(t))$tmodel(t) + $Sw_StateRPS$(yeart(t)>=firstyear_RPS) + $(not sameas(RPSCat,"RPS_Bundled")) + $(not sameas(RPSCat,"CES_Bundled")) + $RecTech(RPSCat,i,st,t) + ].. + +*RECS are computed as the total annual generation from a technology +*hydro is the only technology adjusted by RPSTechMult +*because GEN can only generate a H2 PTC credit or a REC, not both, subtract out the generation which produces a hydrogen PTC credit +*because GEN from pvb(i) includes grid charging, subtract out its grid charging + + sum{(v,r,h)$[valgen(i,v,r,t)$r_st(r,st)$h_rep(h)], + RPSTechMult(RPSCat,i,st) * hours(h) + * (GEN(i,v,r,h,t) + - CREDIT_H2PTC(i,v,r,h,t)$[valgen_h2ptc(i,v,r,t)$Sw_H2_PTC] + - (STORAGE_IN_GRID(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] ) + } + + =g= + +* Generation must be greater than RECS sent to all states that can trade + + sum{ast$[RecMap(i,RPSCat,st,ast,t)$(stfeas(ast) or sameas(ast,"voluntary"))], + RECS(RPSCat,i,st,ast,t) } +* RPS_Bundled RECS and RPS_All RECS can meet the same requirement +* therefore lumping them together to avoid double-counting + + sum{ast$[RecMap(i,"RPS_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("RPS_Bundled",i,st,ast,t) }$[sameas(RPSCat,"RPS_All")] + +*same logic as bundled RPS RECS is applied to the bundled CES RECS + + sum{ast$[RecMap(i,"CES_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("CES_Bundled",i,st,ast,t) }$[sameas(RPSCat,"CES")] +; + +* --------------------------------------------------------------------------- + +* note that the bundled rpscat can be included +* to comply with the RPS_All categeory +* but it is not in itself explicit requirement +eq_REC_Requirement(RPSCat,st,t)$[RecPerc(RPSCat,st,t)$(not tfirst(t)) + $tmodel(t)$Sw_StateRPS$(yeart(t)>=firstyear_RPS) + $(stfeas(st) or sameas(st,"voluntary")) + $(not sameas(RPSCat,"RPS_Bundled")) + $(not sameas(RPSCat,"CES_Bundled"))].. + +* RECs owned (i.e. imported and generated/used in state minus exports) + + sum{(i,ast)$[RecMap(i,RPSCat,ast,st,t)$stfeas(ast)], + RECS(RPSCat,i,ast,st,t) } + - sum{(i,ast)$[RecMap(i,RPSCat,st,ast,t)$stfeas(ast)$(not sameas(st,ast))], + RECS(RPSCat,i,st,ast,t) } + +* bundled RECS can also be used to meet the RPS_All requirements (imports minus exports) + + sum{(i,ast)$[RecMap(i,"RPS_Bundled",ast,st,t)$stfeas(ast)$(not sameas(ast,st))], + RECS("RPS_Bundled",i,ast,st,t) }$[sameas(RPSCat,"RPS_All")] + - sum{(i,ast)$[RecMap(i,"RPS_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("RPS_Bundled",i,st,ast,t) }$[sameas(RPSCat,"RPS_All")] + +* bundled CES credits can also be used to meet the CES requirements (imports minus exports) + + sum{(i,ast)$[RecMap(i,"CES_Bundled",ast,st,t)$stfeas(ast)$(not sameas(ast,st))], + RECS("CES_Bundled",i,ast,st,t) }$[sameas(RPSCat,"CES")] + - sum{(i,ast)$[RecMap(i,"CES_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("CES_Bundled",i,st,ast,t) }$[sameas(RPSCat,"CES")] + +* ACP credits can also be purchased + + ACP_PURCHASES(rpscat,st,t)$(not acp_disallowed(st,RPSCat)) + +* Exports to Canada are assumed to be clean, and therefore consume CES credits + - sum{(r,h)$[r_st(r,st)$h_rep(h)], + can_exports_h(r,h,t) * hours(h) }$[(Sw_Canada=1)$sameas(RPSCat,"CES")] + + =g= + +* note here we do not pre-define the rec requirement since load_exog(r,h,t) +* changes when sent to/from the demand side + RecPerc(RPSCat,st,t) * sum{(r,h)$[r_st_rps(r,st)$h_rep(h)], hours(h) * ( +* RecStyle(st,RPSCat)=0 means end-use sales. + ( (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] + - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) * (1.0 - distloss) + )$(RecStyle(st,RPSCat)=0) + +* RecStyle(st,RPSCat)=1 means bus-bar sales. + + ( LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] + - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) } + )$(RecStyle(st,RPSCat)=1) + +* RecStyle(st,RPSCat)=2 means generation (including distPV), but subtracting canadian exports +*for CES (similar to the left-hand-side). Also, because GEN from pvb(i) includes grid charging, +*subtract out its grid charging (see eq_REC_Generation above). + + ( sum{(i,v)$[valgen(i,v,r,t)$(not storage_standalone(i))], GEN(i,v,r,h,t) + - (distloss * GEN(i,v,r,h,t))$(distpv(i)) + - (STORAGE_IN_GRID(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] } + - can_exports_h(r,h,t)$[(Sw_Canada=1)$sameas(RPSCat,"CES")] + )$(RecStyle(st,RPSCat)=2) + )} +; + +* --------------------------------------------------------------------------- + +eq_REC_BundleLimit(RPSCat,st,ast,t)$[stfeas(st)$stfeas(ast)$tmodel(t) + $(not sameas(st,ast))$Sw_StateRPS + $(sum{i,RecMap(i,RPSCat,st,ast,t) }) + $(sameas(RPSCat,"RPS_Bundled") or sameas(RPSCat,"CES_Bundled")) + $(yeart(t)>=firstyear_RPS)].. + +*amount of net transmission flows from state st to state ast + sum{(h,r,rr,trtype)$[r_st(r,st)$r_st(rr,ast)$routes(r,rr,trtype,t)$h_rep(h)], + hours(h) * FLOW(r,rr,h,t,trtype) + } + + =g= +* must be greater than bundled RECS + sum{i$RecMap(i,RPSCat,st,ast,t), + RECS(RPSCat,i,st,ast,t) } +; + +* --------------------------------------------------------------------------- + +eq_REC_unbundledLimit(RPSCat,st,t)$[st_unbundled_limit(RPScat,st)$tmodel(t)$stfeas(st) + $(yeart(t)>=firstyear_RPS)$Sw_StateRPS + $(sameas(RPSCat,"RPS_All") or sameas(RPSCat,"CES"))].. +*the limit on unbundled RECS times the REC requirement (based on end-use sales) + REC_unbundled_limit(RPSCat,st,t) * RecPerc(RPSCat,st,t) * + sum{(r,h)$[r_st(r,st)$h_rep(h)], + hours(h) * + (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) * (1.0 - distloss) + } + =g= + +*needs to be greater than the unbundled recs +*NB unbundled RECS are computed as all imported RECS minus bundled RECS + sum{(i,ast)$[RecMap(i,RPSCat,ast,st,t)$stfeas(ast)$(not sameas(st,ast))], + RECS(RPSCat,i,ast,st,t) } + + - sum{(i,ast)$[RecMap(i,"RPS_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("RPS_Bundled",i,ast,st,t) }$sameas(RPSCat,"RPS_All") + + - sum{(i,ast)$[RecMap(i,"CES_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("CES_Bundled",i,ast,st,t) }$sameas(RPSCat,"CES") +; + +* --------------------------------------------------------------------------- + +eq_REC_ooslim(RPSCat,st,t)$[RecPerc(RPSCat,st,t)$(yeart(t)>=firstyear_RPS) + $RPS_oosfrac(st)$stfeas(st)$tmodel(t)$Sw_StateRPS + $(not sameas(RPSCat,"RPS_Bundled")) + $(not sameas(RPSCat,"CES_Bundled"))].. + +*the fraction of imported recs times the requirement (based on end-use sales) + RPS_oosfrac(st) * RecPerc(RPSCat,st,t) * + sum{(r,h)$[r_st(r,st)$h_rep(h)], + hours(h) * + (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) * (1.0 - distloss) + } + =g= + +*imported RECs - note that the not sameas(st,ast) indicates they are not generated in-state + sum{(i,ast)$[RecMap(i,RPSCat,ast,st,t)$stfeas(ast)$(not sameas(st,ast))], + RECS(RPSCat,i,ast,st,t) + } + + + sum{(i,ast)$[RecMap(i,"RPS_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("RPS_Bundled",i,ast,st,t) + }$sameas(RPSCat,"RPS_All") + + + sum{(i,ast)$[RecMap(i,"CES_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("CES_Bundled",i,ast,st,t) + }$sameas(RPSCat,"CES") +; + +* --------------------------------------------------------------------------- + +*exports must be less than RECS generated +eq_REC_launder(RPSCat,st,t)$[RecStates(RPSCat,st,t)$(not tfirst(t))$(yeart(t)>=firstyear_RPS) + $tmodel(t)$stfeas(st)$Sw_StateRPS + $(not sameas(RPSCat,"RPS_Bundled")) + $(not sameas(RPSCat,"CES_Bundled"))].. + +*in-state REC generation + + sum{(i,v,r,h)$[valgen(i,v,r,t)$RecTech(RPSCat,i,st,t)$r_st(r,st)$h_rep(h)], + hours(h) * (GEN(i,v,r,h,t) - CREDIT_H2PTC(i,v,r,h,t)$[valgen_h2ptc(i,v,r,t)$Sw_H2_PTC]) + } + +*minus ACP_PURCHASES with a 10x multiplier (the multiplier discourages the model from +*exporting RECs when it is buying ACP credits) + - ACP_PURCHASES(RPSCat,st,t)$(not acp_disallowed(st,RPSCat)) * 10 + + =g= + +*exported RECS - NB the conditional that st!=ast + + sum{(i,ast)$[RecMap(i,RPSCat,st,ast,t)$(stfeas(ast) or sameas(ast,"voluntary"))$(not sameas(st,ast))], + RECS(RPSCat,i,st,ast,t) } + + + sum{(i,ast)$[RecMap(i,"RPS_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("RPS_Bundled",i,st,ast,t) + }$sameas(RPSCat,"RPS_All") + + + sum{(i,ast)$[RecMap(i,"CES_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], + RECS("CES_Bundled",i,st,ast,t) + }$sameas(RPSCat,"CES") + +; + +* --------------------------------------------------------------------------- + +eq_RPS_OFSWind(st,t)$[tmodel(t)$stfeas(st)$offshore_cap_req(st,t)$Sw_StateRPS + $sum{(i,v,r)$[r_st(r,st)$ofswind(i)], valcap(i,v,r,t) } + $(yeart(t)>=firstyear_RPS)$(not Sw_PCM)].. + +* existing capacity of wind + sum{(i,v,r)$[r_st(r,st)$ofswind(i)], m_capacity_exog(i,v,r,t) } + +* investments over time + + sum{(i,v,r,tt)$[r_st(r,st)$ofswind(i)$inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))], + INV(i,v,r,tt) + INV_REFURB(i,v,r,tt)$[refurbtech(i)$Sw_Refurb] } + + =g= + +*exogenously-specified requirement for offshore wind capacity + offshore_cap_req(st,t) +; + +* --------------------------------------------------------------------------- + +eq_batterymandate(st,t) + $[tmodel(t)$batterymandate(st,t)$(yeart(t)>=firstyear_battery) + $Sw_BatteryMandate + $(not Sw_PCM)].. +*battery capacity + sum{(i,v,r)$[r_st(r,st)$valcap(i,v,r,t)$(battery(i) or pvb(i))], bcr(i) * CAP(i,v,r,t) } + + =g= + +*must be greater than the required level + batterymandate(st,t) +; + +* --------------------------------------------------------------------------- + +eq_national_gen(t)$[tmodel(t)$national_gen_frac(t)$Sw_GenMandate].. + +*generation from renewables (already post-curtailment) + sum{(i,v,r,h)$[nat_gen_tech_frac(i)$valgen(i,v,r,t)$h_rep(h)], + GEN(i,v,r,h,t) * hours(h) * nat_gen_tech_frac(i) } + + =g= + +*must exceed the mandated percentage [times] + national_gen_frac(t) * ( + +* if Sw_GenMandate = 1, then apply the fraction to the bus bar load + ( +* load + sum{(r,h)$h_rep(h), LOAD(r,h,t) * hours(h) } +* [plus] transmission losses + + sum{(rr,r,h,trtype)$[routes(rr,r,trtype,t)$h_rep(h)], (tranloss(rr,r,trtype) * FLOW(rr,r,h,t,trtype) * hours(h)) } +* [plus] storage losses + + sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)$h_rep(h)], STORAGE_IN(i,v,r,h,t) * hours(h) } + - sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)$h_rep(h)], GEN(i,v,r,h,t) * hours(h) } + )$[Sw_GenMandate = 1] + +* if Sw_GenMandate = 2, then apply the fraction to the end use load + + (sum{(r,h)$h_rep(h), + hours(h) * + ( (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1]) * (1.0 - distloss) - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) + })$[Sw_GenMandate = 2] + ) +; + +* --------------------------------------------------------------------------- + +*==================================== +* --- FUEL SUPPLY CURVES --- +*==================================== + +* --------------------------------------------------------------------------- + +*gas used from each bin is the sum of all gas used +eq_gasused(cendiv,h,t)$[tmodel(t)$((Sw_GasCurve=0) or (Sw_GasCurve=3))].. + + sum{gb,GASUSED(cendiv,gb,h,t) } + + =e= + + sum{(i,v,r)$[valgen(i,v,r,t)$gas(i)$r_cendiv(r,cendiv)], + heat_rate(i,v,r,t) * (GEN(i,v,r,h,t) + CCSFLEX_POW(i,v,r,h,t)$[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)]) } / gas_scale + + + (sum{(v,r)$[valcap("dac_gas",v,r,t)$r_cendiv(r,cendiv)], + dac_gas_cons_rate("dac_gas",v,t) * PRODUCE("DAC","dac_gas",v,r,h,t) } / gas_scale)$Sw_DAC_Gas + +; + +* --------------------------------------------------------------------------- + +* gas from each bin needs to less than its capacity +eq_gasbinlimit(cendiv,gb,t)$[tmodel(t)$(Sw_GasCurve=0)].. + + gaslimit(cendiv,gb,t) + + =g= + + sum{h, hours(h) * GASUSED(cendiv,gb,h,t) } +; + +* --------------------------------------------------------------------------- + +eq_gasbinlimit_nat(gb,t)$[tmodel(t)$(Sw_GasCurve=3)].. + + gaslimit_nat(gb,t) + + =g= + + sum{(h,cendiv), + hours(h) * GASUSED(cendiv,gb,h,t) + } +; + +* --------------------------------------------------------------------------- + +eq_gasaccounting_regional(cendiv,t)$[tmodel(t)$(Sw_GasCurve=1)].. + + sum{fuelbin, VGASBINQ_REGIONAL(fuelbin,cendiv,t) } + + =e= + + sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)$r_cendiv(r,cendiv)], + hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) + } +; + +* --------------------------------------------------------------------------- + +eq_gasaccounting_national(t)$[tmodel(t)$(Sw_GasCurve=1)].. + + sum{fuelbin,VGASBINQ_NATIONAL(fuelbin,t) } + + =e= + + sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)], + hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) + } +; + +* --------------------------------------------------------------------------- + +eq_gasbinlimit_regional(fuelbin,cendiv,t)$[tmodel(t)$(Sw_GasCurve=1)].. + + Gasbinwidth_regional(fuelbin,cendiv,t) + + =g= + + VGASBINQ_REGIONAL(fuelbin,cendiv,t) +; + +* --------------------------------------------------------------------------- + +eq_gasbinlimit_national(fuelbin,t)$[tmodel(t)$(Sw_GasCurve=1)].. + + Gasbinwidth_national(fuelbin,t) + + =g= + + VGASBINQ_NATIONAL(fuelbin,t) +; + +* --------------------------------------------------------------------------- + +*============================== +* -- Bioenergy Supply Curve -- +*============================== + +* --------------------------------------------------------------------------- + +eq_bioused(r,t)$[sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,t) }$tmodel(t)].. + + sum{bioclass, BIOUSED(bioclass,r,t) } + + =e= + +*biopower generation + + sum{(i,v,h)$[valgen(i,v,r,t)$bio(i)], + hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) } + + +*portion of cofire generation that is from bio resources + + sum{(i,v,h)$[cofire(i)$valgen(i,v,r,t)], + bio_cofire_perc * hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +* biomass consumption limit is annual +eq_biousedlimit(bioclass,usda_region,t)$tmodel(t).. + + biosupply(usda_region,bioclass,"cap") + + =g= + + sum{r$[r_usda(r,usda_region)$sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,t) }], BIOUSED(bioclass,r,t) } +; + +* --------------------------------------------------------------------------- + +*============================ +* --- STORAGE CONSTRAINTS --- +*============================ + +* --------------------------------------------------------------------------- + +*storage use cannot exceed capacity +*this constraint does not apply to CSP+TES or hydro pump upgrades +eq_storage_capacity(i,v,r,h,t)$[valgen(i,v,r,t) + $(storage_standalone(i)$(not evmc_storage(i)) + or evmc_storage(i) + $[evmc_storage_charge_frac(i,r,h,t)$evmc_storage_discharge_frac(i,r,h,t)] + or storage_hybrid(i)$(not csp(i))) + $tmodel(t)].. + +* [plus] Capacity of all storage technologies + (CAP(i,v,r,t) * bcr(i) * avail(i,r,h) + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + )$valcap(i,v,r,t) + + =g= + +* [plus] Generation from storage, excluding hybrid+storage and adjusting evmc_storage for time-varying discharge (deferral) availability + GEN(i,v,r,h,t)$(not storage_hybrid(i)$(not csp(i))) / (1$(not evmc_storage(i)) + evmc_storage_discharge_frac(i,r,h,t)$evmc_storage(i)) + +* [plus] Generation from battery of hybrid+storage + + GEN_STORAGE(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] + +* [plus] Storage charging +* excludes hybrid plant+storage and adjusting evmc_storage for time-varying charge (add back deferred EV load) availability + + STORAGE_IN(i,v,r,h,t)$[not storage_hybrid(i)$(not csp(i))] / (1$(not evmc_storage(i)) + evmc_storage_charge_frac(i,r,h,t)$evmc_storage(i)) + +* hybrid+storage plant: plant generation + + STORAGE_IN_PLANT(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$dayhours(h)$Sw_HybridPlant] +* hybrid+storage plant: Grid generation + + STORAGE_IN_GRID(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] + +* [plus] Operating reserves + + sum{ortype$[Sw_OpRes$opres_model(ortype)$opres_h(h)], + reserve_frac(i,ortype) * OPRES(ortype,i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +* The daily storage level in the next time-slice (h+1) must equal the +* daily storage level in the current time-slice (h) +* plus daily net charging in the current time-slice (accounting for losses). +* CSP with storage energy accounting is also covered by this constraint. +* Does not apply for storage technologies that allow cross-season energy arbitrage. +* When inter-day linkage is used, the nexth will be disabled, and this equation will +* be used to calculate intra-day storage dispatch for further inter-day linkage use. +eq_storage_level(i,v,r,h,t)$[valgen(i,v,r,t)$storage(i)$tmodel(t)].. + +*[plus] storage level in h+1 + sum{(hh)$[nexth(h,hh)], STORAGE_LEVEL(i,v,r,hh,t)}$(not storage_interday(i)) + +*[plus] the net dispatch of inter-day storage technologies + + STORAGE_INTERDAY_DISPATCH(i,v,r,h,t)$(storage_interday(i)) * hours_daily(h) + + =e= + +* only want to include storage_level from periods that have had a previous storage_level +* otherwise it becomes a free variable, implying you can charge storage without bound + STORAGE_LEVEL(i,v,r,h,t)$(not storage_interday(i)) + +*[plus] storage charging + + storage_eff(i,t) * hours_daily(h) * ( +*energy into stand-alone storage (not CSP-TES) and hydropower that adds pumping + STORAGE_IN(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)] + +*energy into storage from CSP field + + (CAP(i,v,r,t) * csp_sm(i) * m_cf(i,v,r,h,t) + )$[CSP_Storage(i)$valcap(i,v,r,t)] + ) +*[plus] water inflow energy available for hydropower that adds pumping + + (CAP(i,v,r,t) * avail(i,r,h) * hours_daily(h) * + sum{szn$h_szn(h,szn), m_cf_szn(i,v,r,szn,t) } + )$hyd_add_pump(i) + +*[plus] energy into hybrid plant storage +*hybrid+storage plant: plant charging + + storage_eff_pvb_p(i,t) * hours_daily(h) + * STORAGE_IN_PLANT(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$dayhours(h)$Sw_HybridPlant] + +*hybrid+storage plant: grid charging + + storage_eff_pvb_g(i,t) * hours_daily(h) + * STORAGE_IN_GRID(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] + +*[minus] generation from stand-alone storage (discharge) and CSP +*exclude hybrid+storage plant because GEN refers to output from both the plant and the battery + - hours_daily(h) * GEN(i,v,r,h,t)$[not storage_hybrid(i)$(not csp(i))] + +*[minus] Generation from Battery (discharge) of hybrid+storage plant + - hours_daily(h) * GEN_STORAGE(i,v,r,h,t) $[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] + +*[minus] losses from reg reserves (only half because only charging half +*the time while providing reg reserves) + - (hours_daily(h) + * (OPRES("reg",i,v,r,h,t)$[Sw_OpRes=1] + OPRES("combo",i,v,r,h,t)$[Sw_OpRes=2]) + * (1 - storage_eff(i,t)) / 2 * reg_energy_frac + )$[opres_h(h)] +; + +* --------------------------------------------------------------------------- + +*there must be sufficient energy in storage to provide operating reserves +eq_storage_opres(i,v,r,h,t) + $[valgen(i,v,r,t)$tmodel(t)$Sw_OpRes$opres_h(h) + $(storage_standalone(i) or storage_hybrid(i)$(not csp(i)) or hyd_add_pump(i))].. + +*[plus] initial storage level + STORAGE_LEVEL(i,v,r,h,t) + +*[minus] generation that occurs during this timeslice + - hours_daily(h) * GEN(i,v,r,h,t) $[not storage_hybrid(i)$(not csp(i))] + +*[minus] generation that occurs during this timeslice + - hours_daily(h) * GEN_STORAGE(i,v,r,h,t) $[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] + +*[minus] losses from reg reserves (only half because only charging half +*the time while providing reg reserves) + - hours_daily(h) * (OPRES("reg",i,v,r,h,t)$[Sw_OpRes = 1] + OPRES("combo",i,v,r,h,t)$[Sw_OpRes = 2]) + * (1 - storage_eff(i,t)) / 2 * reg_energy_frac + + =g= + +*[plus] energy reserved for operating reserves + + hours_daily(h) * sum{ortype$opres_model(ortype), OPRES(ortype,i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +*storage charging must exceed OR contributions for thermal storage +eq_storage_thermalres(i,v,r,h,t) + $[valgen(i,v,r,t)$Thermal_Storage(i) + $tmodel(t)$Sw_OpRes$opres_h(h)].. + + STORAGE_IN(i,v,r,h,t) + + =g= + + sum{ortype$[opres_model(ortype)], + reserve_frac(i,ortype) * OPRES(ortype,i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +*batteries and CSP-TES are limited by their duration for each normalized hour per season +*seas_cap_frac_delta is not applied here because we assume that the storage energy capacity is +*constant across the year. +eq_storage_duration(i,v,r,h,t)$[valgen(i,v,r,t)$valcap(i,v,r,t) + $storage(i) + $tmodel(t) + $(not storage_interday(i))].. + +* [plus] storage duration times storage capacity for fixed-duration techs + storage_duration(i) * CAP(i,v,r,t) * (1$CSP_Storage(i) + 1$psh(i) + bcr(i)$pvb(i)) + +* [plus] EVMC storage has time-varying energy capacity + + evmc_storage_energy_hours(i,r,h,t) * CAP(i,v,r,t) * (bcr(i)$evmc_storage(i)) + +* [plus] battery storage capacity + + CAP_ENERGY(i,v,r,t)$battery(i) + + =g= + + STORAGE_LEVEL(i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + +* Charging power must be less than a specified fraction of power output capacity +* This is required in addition to eq_storage_capacity for facilities where input capacity < output capacity. +* If storinmaxfrac were applied to CAP in eq_storage_capacity, it would also limit output capacity. +eq_storage_in_cap(i,v,r,h,t)$[(storage_standalone(i) or hyd_add_pump(i))$valgen(i,v,r,t)$valcap(i,v,r,t) + $tmodel(t)$(storinmaxfrac(i,v,r) < 1)].. + +*[plus] maximum storage input capacity as a fraction of output capacity and accounting for availability +* for evmc_storage this adjust for time-varying availability of charging (add back deferred EV load) + avail(i,r,h) * storinmaxfrac(i,v,r) + * CAP(i,v,r,t) + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + * (1$(not evmc_storage(i)) + evmc_storage_charge_frac(i,r,h,t)$evmc_storage(i)) + + =g= + +*[plus] storage input + STORAGE_IN(i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + +* the charging power in any time slice cannot exceed the minstorfrac times the +* charging power from any other hour within the same season (via hour_szn_group(h,hh)) +eq_storage_in_minloading(i,v,r,h,hh,t)$[(storage_standalone(i) or hyd_add_pump(i))$tmodel(t) + $minstorfrac(i,v,r)$valgen(i,v,r,t)$hour_szn_group(h,hh)$Sw_MinLoading].. + + STORAGE_IN(i,v,r,h,t) + + =g= + + STORAGE_IN(i,v,r,hh,t) * minstorfrac(i,v,r) +; + +* --------------------------------------------------------------------------- +* for batteries +* when power capacity is built, energy capacity must be greater than the minimum duration +eq_battery_minduration(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$battery(i)].. + + CAP_ENERGY(i,v,r,t) + + =g= + + minbatteryduration * CAP(i,v,r,t) +; + +* --------------------------------------------------------------------------- + +* Constraints for building inter-day storage linkage +* The storage level at hour 0 of next partition +* equals to the storage level at hour 0 of current partition +* plus number of period of current partition multiply by storage net change of the rep period +eq_storage_interday_level(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. + + sum{allsznn$[nextpartition(allszn,allsznn)],STORAGE_INTERDAY_LEVEL(i,v,r,allsznn,t)} + + =e= + + STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) + + + numpartitions(allszn) + * sum{(szn)$[szn_actualszn(szn,allszn)$numpartitions(allszn)], + sum{h$[h_szn(h,szn)], (STORAGE_INTERDAY_DISPATCH(i,v,r,h,t) * hours_daily(h))}} +; + +* --------------------------------------------------------------------------- + +* Constraints for building inter-day storage linkage +* Define the maximum relative soc on a representative period compared to its hour 0 +* It's noted that STORAGE_INTERDAY_LEVEL_MAX_DAY can be nagative since it represent a relative value +eq_storage_interday_level_max_day(i,v,r,szn,h,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$h_szn(h,szn)].. + + STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,szn,t) + + =g= + + sum{(hh)$[h_preh(h,hh)], (STORAGE_INTERDAY_DISPATCH(i,v,r,hh,t) * hours_daily(h))} +; + +* --------------------------------------------------------------------------- + +* Constraints for building inter-day storage linkage +* Define the minimum relative soc on a representative period compared to its hour 0 +* It's noted that STORAGE_INTERDAY_LEVEL_MIN_DAY can be nagative since it represent a relative value +eq_storage_interday_level_min_day(i,v,r,szn,h,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$h_szn(h,szn)].. + + sum{(hh)$[h_preh(h,hh)], (STORAGE_INTERDAY_DISPATCH(i,v,r,hh,t) * hours_daily(h))} + + =g= + + STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,szn,t) +; + +* --------------------------------------------------------------------------- + +* Constraints for building inter-day storage linkage +* For all partitions, the soc at their hour 0 plus minimum soc of first period should greater than 0 +* This is to make sure not only their hour 0 but also the lowest point of the first period of each partition is greater than 0 +eq_storage_interday_min_level_start(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. + + STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) + + + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,szn,t)} + + =g= + + 0 +; + +* --------------------------------------------------------------------------- + +* Constraints for building inter-day storage linkage +* For all partitions, the soc at their hour 0 plus minimum soc of last period should greater than 0 +* This is to make sure not only their hour 0 but also the lowest point of the last period of each partition is greater than 0 +eq_storage_interday_min_level_end(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. + + STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) + + + (numpartitions(allszn) - 1) + * sum{(szn)$[szn_actualszn(szn,allszn)], + sum{h$[h_szn(h,szn)], (STORAGE_INTERDAY_DISPATCH(i,v,r,h,t) * hours_daily(h))}} + + + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,szn,t)} + + =g= + + 0 +; + +* --------------------------------------------------------------------------- + +* Constraints for building inter-day storage linkage +* For all partitions, the soc at their hour 0 plus maximum soc of first period should lower than total storage capacity +* This is to make sure not only their hour 0 but also the highest point of the first period of each partition is lower than maximum capacity +eq_storage_interday_max_level_start(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. + +* Fixed-duration storage + storage_duration(i) * CAP(i,v,r,t)$(not battery(i)) +* Variable-duration storage + + CAP_ENERGY(i,v,r,t)$battery(i) + + =g= + + STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) + + + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,szn,t)} +; + +* --------------------------------------------------------------------------- + +* Constraints for building inter-day storage linkage +* For all partitions, the soc at their hour 0 plus maximum soc of last period should lower than total storage capacity +* This is to make sure not only their hour 0 but also the highest point of the last period of each partition is greater than maximum capacity +eq_storage_interday_max_level_end(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. + + storage_duration(i) * CAP(i,v,r,t)$(not battery(i)) + + + CAP_ENERGY(i,v,r,t)$battery(i) + + =g= + + STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) + + + (numpartitions(allszn) - 1) + * sum{(szn)$[szn_actualszn(szn,allszn)], + sum{h$[h_szn(h,szn)], (STORAGE_INTERDAY_DISPATCH(i,v,r,h,t) * hours_daily(h))}} + + + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,szn,t)} +; + +* --------------------------------------------------------------------------- + +*=============================== +* --- Hybrid Plant --- +*=============================== + +* --------------------------------------------------------------------------- + +*Generation post curtailment = +* + generation from hybrid storage plant + generation from storage - storage charging from hybrid storage plant +eq_plant_total_gen(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$tmodel(t)$valgen(i,v,r,t)$Sw_HybridPlant].. + + + GEN_PLANT(i,v,r,h,t) + + + GEN_STORAGE(i,v,r,h,t) + +*[minus] charging from hybrid storage plant + - STORAGE_IN_PLANT(i,v,r,h,t)$dayhours(h) + + =e= + + GEN(i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + +*Energy to storage from hybrid storage palnt + hybrid storage plant generation <= hybrid storage plant maximum production for a resource +*capacity factor is adjusted to include inverter losses, clipping losses, and low voltage losses +eq_hybrid_plant_energy_limit(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$tmodel(t)$valgen(i,v,r,t)$valcap(i,v,r,t)$Sw_HybridPlant].. + +* [plus] plant output + m_cf(i,v,r,h,t) * CAP(i,v,r,t) + + =g= + +*[plus] charging from hybrid plant + + STORAGE_IN_PLANT(i,v,r,h,t)$dayhours(h) + +*[plus] generation from hybrid plant + + GEN_PLANT(i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + +*Energy moving through the inverter cannot exceed the inverter capacity +eq_plant_capacity_limit(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$tmodel(t)$valgen(i,v,r,t)$valcap(i,v,r,t)$Sw_HybridPlant].. + +*[plus] inverter capacity [AC] = panel capacity [DC] / ILR [DC/AC] + + CAP(i,v,r,t) / ilr(i) + + =g= + +* [plus] Output from plant + + GEN_PLANT(i,v,r,h,t) + +* [plus] Output form storage + + GEN_STORAGE(i,v,r,h,t) + +*[plus] battery charging from grid + + STORAGE_IN_GRID(i,v,r,h,t) + +*[plus] battery operating reserves + + sum{ortype$[Sw_OpRes$opres_h(h)$opres_model(ortype)], OPRES(ortype,i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +*Total energy charged from local PV >= ITC qualification fraction * total energy charged + +eq_pvb_itc_charge_reqt(i,v,r,t)$[pvb(i)$tmodel(t)$valgen(i,v,r,t)$pvb_itc_qual_frac$Sw_PVB].. + +* [plus] battery charging from PV + + sum{h$[dayhours(h)$h_rep(h)], STORAGE_IN_PLANT(i,v,r,h,t) * hours(h) } + + =g= + + + pvb_itc_qual_frac * ( + +* [plus] battery charging from PV + + sum{h$[dayhours(h)$h_rep(h)], STORAGE_IN_PLANT(i,v,r,h,t) * hours(h) } + +* [plus] battery charging from Grid + + sum{h$h_rep(h), STORAGE_IN_GRID(i,v,r,h,t) * hours(h) } + ) +; + +* --------------------------------------------------------------------------- + +*=================================== +* --- CANADIAN IMPORTS EQUATIONS --- +*=================================== + +* --------------------------------------------------------------------------- + +eq_Canadian_Imports(r,szn,t)$[can_imports_szn(r,szn,t)$tmodel(t)$(Sw_Canada=1)].. + + can_imports_szn(r,szn,t) + + =g= + + sum{(i,v,h)$[canada(i)$valgen(i,v,r,t)$h_szn(h,szn)], GEN(i,v,r,h,t) * hours(h) } +; + +* --------------------------------------------------------------------------- + +*========================== +* --- WATER CONSTRAINTS --- +*========================== + +* --------------------------------------------------------------------------- + +*water accounting for all valid power plants for generation where usage is both for cooling and/or non-cooling purposes +eq_water_accounting(i,v,w,r,h,t)$[i_water(i)$valgen(i,v,r,t)$h_rep(h)$tmodel(t)$Sw_WaterMain].. + + WAT(i,v,w,r,h,t) + + =e= + +*division by 1E6 to convert gal of water_rate(i,w) to Mgal + GEN(i,v,r,h,t) * hours(h) * water_rate(i,w) / 1E6 +; + +* --------------------------------------------------------------------------- + +*total water access is determined by total capacity +eq_water_capacity_total(i,v,r,t)$[tmodel(t)$valcap(i,v,r,t) + $(i_water_cooling(i) or psh(i))$Sw_WaterMain$Sw_WaterCapacity].. + + WATCAP(i,v,r,t) + + =e= + +*require enough water capacity to allow 100% capacity factor (8760 hour operation) +*division by 1E6 to convert gal of water_rate(i,w) to Mgal + sum{h$h_rep(h), hours(h) + * sum{w$i_w(i,w), + CAP(i,v,r,t) * water_rate(i,w) } + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + } / 1E6 + +*require enough water capacity to fill PSH reservoir. +*uses investment so that term is only applied in the single investment year +* as a proxy for water needs during construction phase. + + sum{rscbin$[m_rscfeas(r,i,rscbin)$psh(i)], INV_RSC(i,v,r,rscbin,t) * water_req_psh(r,rscbin) }$Sw_PSHwatercon +; + +* --------------------------------------------------------------------------- + +*total water access must not exceed supply +eq_water_capacity_limit(wst,r,t)$[tmodel(t)$Sw_WaterMain$Sw_WaterCapacity].. + + m_watsc_dat(wst,"cap",r,t) + + + WATER_CAPACITY_LIMIT_SLACK(wst,r,t) + + =g= + + sum{(i,v)$[i_wst(i,wst)$valcap(i,v,r,t)], WATCAP(i,v,r,t) } +; + +* --------------------------------------------------------------------------- + +*water use must not exceed available access +eq_water_use_limit(i,v,w,r,szn,t)$[i_water_cooling(i)$valgen(i,v,r,t)$tmodel(t) + $i_w(i,w)$Sw_WaterMain$Sw_WaterCapacity$Sw_WaterUse].. + + WATCAP(i,v,r,t) *sum{wst$i_wst(i,wst), watsa(wst,r,szn,t) } + + =g= + + sum{h$h_szn(h,szn), WAT(i,v,w,r,h,t) } +; + +* --------------------------------------------------------------------------- + +*============================== +* -- H2 and DAC Constraints -- +*============================== + +* --------------------------------------------------------------------------- + +eq_prod_capacity_limit(i,v,r,h,t) + $[tmodel(t) + $consume(i) + $valcap(i,v,r,t) + $Sw_Prod + $h_rep(h)].. + +* available capacity [times] the conversion rate of metric ton / MW + CAP(i,v,r,t) * avail(i,r,h) + * (prod_conversion_rate(i,v,r,t)$[not sameas(i,"dac_gas")] + + 1$sameas(i,"dac_gas")) + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + + =g= + +* production in that timeslice + sum{p$i_p(i,p), PRODUCE(p,i,v,r,h,t) } +; + +* --------------------------------------------------------------------------- + +* H2 demand balance; national and annual. Active only when Sw_H2=1. +eq_h2_demand(p,t)$[(sameas(p,"H2"))$tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2=1)].. + +* annual metric tons of production + sum{(i,v,r,h)$[h2(i)$valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], + PRODUCE(p,i,v,r,h,t) * hours(h) } + + =e= + +* annual demand + h2_exogenous_demand(p,t) + +* assuming here that h2 production and use in H2_COMBUSTION can be temporally asynchronous +* that is, the hydrogen does not need to produced in the same hour it is consumed by h2-ct/cc's + + sum{(i,v,r,h)$[valgen(i,v,r,t)$h2_combustion(i)$h_rep(h)], + GEN(i,v,r,h,t) * hours(h) * h2_combustion_intensity * heat_rate(i,v,r,t) + } +; + +* --------------------------------------------------------------------------- + +* H2 demand balance; regional and by timeslice w/ H2 transport network and storage. +* Active only when Sw_H2=2 [metric tons/hour] +eq_h2_demand_regional(r,h,t) + $[tmodel(t)$(Sw_H2=2)$(yeart(t)>=h2_demand_start)$h_rep(h)].. + +* endogenous supply of hydrogen + sum{(i,v,p)$[h2(i)$valcap(i,v,r,t)$i_p(i,p)], + PRODUCE(p,i,v,r,h,t) } + +* net hydrogen trade with imports reduced by H2 transmission losses + + sum{rr$h2_routes(rr,r), H2_FLOW(rr,r,h,t) } * (1 - h2_tranloss) + - sum{rr$h2_routes(r,rr), H2_FLOW(r,rr,h,t) } + +* net storage injections / withdrawls in a BA + + sum{h2_stor$[h2_stor_r(h2_stor,r)], H2_STOR_OUT(h2_stor,r,h,t)} + - sum{h2_stor$[h2_stor_r(h2_stor,r)], H2_STOR_IN(h2_stor,r,h,t)} + + =e= + +* annual demand in [metric tons/hour] + sum{p, h2_exogenous_demand_regional(r,p,h,t) } + +* region-specific H2 consumption from H2-CT/CCs +* [MW] * [metric ton/MMBtu] * [MMBtu/MWh] = [metric tons/hour] + + sum{(i,v)$[valgen(i,v,r,t)$h2_combustion(i)], + GEN(i,v,r,h,t) * h2_combustion_intensity * heat_rate(i,v,r,t) + } +; + +* --------------------------------------------------------------------------- + +eq_h2_transport_caplimit(r,rr,h,t)$[h2_routes(r,rr)$(Sw_H2=2) + $tmodel(t)$(yeart(t)>=h2_demand_start)].. + +*capacity computed as cumulative investments of h2 pipelines up to the current year + sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt)) + $(yeart(tt)>=h2_demand_start)], + H2_TRANSPORT_INV(r,rr,tt)$h2_routes_inv(r,rr) + + H2_TRANSPORT_INV(rr,r,tt)$h2_routes_inv(rr,r) } + + =g= + +*bi-directional flow of h2 + H2_FLOW(rr,r,h,t) + H2_FLOW(r,rr,h,t) +; + +* --------------------------------------------------------------------------- + +* link H2 storage level between timeslices of actual periods, or hours when running a chronological year +eq_h2_storage_level(h2_stor,r,actualszn,h,t) + $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=2) + $h2_stor_r(h2_stor,r)$(Sw_H2=2)$h_actualszn(h,actualszn)].. + +*[plus] H2 storage level in next timeslice + sum{(hh,actualsznn)$[nexth_actualszn(actualszn,h,actualsznn,hh)], + H2_STOR_LEVEL(h2_stor,r,actualsznn,hh,t) } + + =e= + +* H2 storage level in current timeslice + H2_STOR_LEVEL(h2_stor,r,actualszn,h,t) + +*[plus] h2 storage injection minus withdrawal (currently assumed to be lossless) + + hours_daily(h) * (H2_STOR_IN(h2_stor,r,h,t) - H2_STOR_OUT(h2_stor,r,h,t)) +; + +* --------------------------------------------------------------------------- + +* link H2 storage level between seasons +eq_h2_storage_level_szn(h2_stor,r,actualszn,t) + $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=1) + $h2_stor_r(h2_stor,r)$(Sw_H2=2)].. + +*[plus] H2 storage level at start of next season + sum{actualsznn$[nextszn(actualszn,actualsznn)], + H2_STOR_LEVEL_SZN(h2_stor,r,actualsznn,t) } + + =e= + +* H2 storage level at start of current season + H2_STOR_LEVEL_SZN(h2_stor,r,actualszn,t) + +*[plus] h2 storage injection minus withdrawal (currently assumed to be lossless) + + sum{h$h_actualszn(h,actualszn), + hours_daily(h) * (H2_STOR_IN(h2_stor,r,h,t) - H2_STOR_OUT(h2_stor,r,h,t)) } +; + +* --------------------------------------------------------------------------- + +* H2 storage capacity [metric tons] +eq_h2_storage_capacity(h2_stor,r,t) + $[tmodel(t)$(Sw_H2=2) + $h2_stor_r(h2_stor,r)$(yeart(t)>=h2_demand_start) + $(not Sw_PCM)].. + +* [metric tons] + sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], + H2_STOR_INV(h2_stor,r,tt)} + + =e= + +* [metric tons] + H2_STOR_CAP(h2_stor,r,t) +; + +* --------------------------------------------------------------------------- + +eq_h2_min_storage_cap(r,t)$[tmodel(t)$(Sw_H2=2)$Sw_H2_MinStorHours$(not Sw_PCM)].. + +* [metric tons] + sum{h2_stor$h2_stor_r(h2_stor,r), H2_STOR_CAP(h2_stor,r,t) } + + =g= + +* [MW] * [MMBtu/MWh] * [metric tons/MMBtu] * [hours] = [metric tons] + sum{(i,v)$[h2_combustion(i)$valcap(i,v,r,t)], + CAP(i,v,r,t) * heat_rate(i,v,r,t) * h2_combustion_intensity * Sw_H2_MinStorHours + } +; + +* --------------------------------------------------------------------------- + +* H2 storage investment capacity +* [metric tons/hour] +eq_h2_storage_flowlimit(h2_stor,r,h,t) + $[tmodel(t) + $(Sw_H2=2) + $h2_stor_r(h2_stor,r) + $(yeart(t)>=h2_demand_start) + $h_rep(h)].. + +*storage capacity computed as cumulative investments of H2 storage up to the current year +*H2 storage costs estimated for a fixed duration, so using this to link storage capacity and injection rates +* [metric tons] / [hours] = [metric tons/hour] + H2_STOR_CAP(h2_stor,r,t) / h2_storage_duration + + =g= + +*H2 storage injection [metric tons/hour] + H2_STOR_IN(h2_stor,r,h,t) + +*[plus] H2 storage withdrawal [metric tons/hour] + + H2_STOR_OUT(h2_stor,r,h,t) +; + +* --------------------------------------------------------------------------- + +* total level of H2 storage cannot exceed storage investment for all days +* [metric tons] +eq_h2_storage_caplimit(h2_stor,r,actualszn,h,t) + $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=2) + $h2_stor_r(h2_stor,r)$(Sw_H2=2)$h_actualszn(h,actualszn)].. + +* total storage investment [metric tons] + H2_STOR_CAP(h2_stor,r,t) + + =g= + +* storage level of H2 [metric tons] + H2_STOR_LEVEL(h2_stor,r,actualszn,h,t) +; + +* --------------------------------------------------------------------------- + +* total level of H2 storage at the beginning of the day cannot exceed storage investment +* [metric tons] +eq_h2_storage_caplimit_szn(h2_stor,r,actualszn,t) + $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=1) + $h2_stor_r(h2_stor,r)$(Sw_H2=2)].. + +* total storage investment [metric tons] + H2_STOR_CAP(h2_stor,r,t) + + =g= + +* storage level of H2 [metric tons] + H2_STOR_LEVEL_SZN(h2_stor,r,actualszn,t) +; + +* --------------------------------------------------------------------------- + +* Hydrogen production tax credit - before h2_ptc_temporal_match_year, electric generation must occur in the same +* region ('h2ptcreg' level) and year that the electrolyzer produces the hydrogen, to qualify for the credit +eq_h2_ptc_region_balance(h2ptcreg,t)$[tmodel(t) + $h2_ptc_years(t) + $(yeart(t)>=h2_demand_start) + $(Sw_H2_PTC) + $(yeart(t)=h2_demand_start) + $(Sw_H2_PTC) + $(yeart(t)>=h2_ptc_temporal_match_year) + ].. + +* generation from clean technologies which qualify to receive hydrogen production tax credits [MW] + sum{(i,v,r)$[r_h2ptcreg(r,h2ptcreg)$valgen_h2ptc(i,v,r,t)], CREDIT_H2PTC(i,v,r,h,t)} + + =e= + +* amount of generation needed to produce hydrogen via electrolysis [MW] + sum{(v,r)$[r_h2ptcreg(r,h2ptcreg)$valcap("electrolyzer",v,r,t)$prod_conversion_rate("electrolyzer",v,r,t)], + PRODUCE("H2","electrolyzer",v,r,h,t) / prod_conversion_rate("electrolyzer",v,r,t) } +; + +* --------------------------------------------------------------------------- + +* Hydrogen production tax credit - total generation must be greater than generation going towards electrolyzers to receive the tax credit +eq_h2_ptc_creditgen(i,v,r,h,t)$[valgen_h2ptc(i,v,r,t) + $h_rep(h) + $tmodel(t) + $Sw_H2_PTC + $h2_ptc_years(t) + $(yeart(t)>=h2_demand_start)].. + +* total generation [MW] + GEN(i,v,r,h,t) + + =g= + +* generation going towards electrolyzers to receive the tax credit [MW] + CREDIT_H2PTC(i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + + +*================================= +* -- CO2 transport and storage -- +*================================= + + +eq_co2_capture(r,h,t) + $[tmodel(t) + $Sw_CO2_Detail + $(yeart(t)>=co2_detail_startyr) + $h_rep(h)].. + + CO2_CAPTURED(r,h,t) + + =e= + +*capture from CCS technologies + sum{(i,v)$[capture_rate("CO2",i,v,r,t)$valgen(i,v,r,t)], capture_rate("CO2",i,v,r,t) + * GEN(i,v,r,h,t) } + +*capture from SMR CCS for H2 production + + sum{(p,i,v)$[i_p("smr_ccs",p)$valcap("smr_ccs",v,r,t)], smr_capture_rate * smr_co2_intensity + * PRODUCE(p,"smr_ccs",v,r,h,t) }$Sw_H2 + +* capture from DAC + + sum{(i,v)$[dac(i)$valcap(i,v,r,t)$i_p(i,"DAC")], PRODUCE("DAC",i,v,r,h,t) }$Sw_DAC +; + +* --------------------------------------------------------------------------- + +eq_co2_transport_caplimit(r,rr,h,t)$[co2_routes(r,rr)$Sw_CO2_Detail + $tmodel(t)$(yeart(t)>=co2_detail_startyr)].. + +*capacity computed as cumulative investments of co2 pipelines up to the current year + sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt)) + $(yeart(tt)>=co2_detail_startyr)], + CO2_TRANSPORT_INV(r,rr,tt) + CO2_TRANSPORT_INV(rr,r,tt) } + + =g= + +*bi-directional flow of co2 + CO2_FLOW(rr,r,h,t) + CO2_FLOW(r,rr,h,t) +; + +* --------------------------------------------------------------------------- + +eq_co2_spurline_caplimit(r,cs,h,t)$[Sw_CO2_Detail$r_cs(r,cs)$tmodel(t)$(yeart(t)>=co2_detail_startyr)].. + +*capacity computed as cumulative investments of co2 spurlines up to the current year + sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))$(yeart(tt)>=co2_detail_startyr)], + CO2_SPURLINE_INV(r,cs,tt) } + + =g= + + CO2_STORED(r,cs,h,t) +; + +* --------------------------------------------------------------------------- + +eq_co2_sink(r,h,t)$[tmodel(t)$Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)].. + +*the amount of co2 stored from r in all of its cs sites + sum{cs$r_cs(r,cs), CO2_STORED(r,cs,h,t) } + + =e= + +* local CO2 entering storage +* note we can substitute the equation above into here +* and avoid the creation of a new variable +* -but- it is nice to have this for reporting/tracking + CO2_CAPTURED(r,h,t) + +* net trade + + sum{rr$co2_routes(r,rr), CO2_FLOW(rr,r,h,t) - CO2_FLOW(r,rr,h,t) } +; + +* --------------------------------------------------------------------------- + +eq_co2_injection_limit(cs,h,t)$[Sw_CO2_Detail$tmodel(t)$(yeart(t)>=co2_detail_startyr)$csfeas(cs)].. + +* exogenously defined injection limit + co2_injection_limit(cs) + + =g= + +* must exceed metric tons per hour entering storage + sum{r$r_cs(r,cs), CO2_STORED(r,cs,h,t) } +; + +* --------------------------------------------------------------------------- + +eq_co2_cumul_limit(cs,t)$[tmodel(t)$Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)$csfeas(cs)].. + +*capacity by co2 bin for injections + co2_storage_limit(cs) + + =g= + +*cumulative amount stored over time + sum{(r,h,tt) + $[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))$(yeart(tt)>=co2_detail_startyr) + $r_cs(r,cs)$h_rep(h)], + yearweight(tt) * hours(h) * CO2_STORED(r,cs,h,tt) } +; +* --------------------------------------------------------------------------- + +*=================== +* -- FLEXIBLE CCS -- +*=================== + +* --------------------------------------------------------------------------- + +eq_ccsflex_byp_ccsenergy_limit(i,v,r,h,t)$[tmodel(t)$valgen(i,v,r,t)$ccsflex_byp(i)$Sw_CCSFLEX_BYP].. + CCSFLEX_POW(i,v,r,h,t) =l= ccsflex_powlim(i,t) * (GEN(i,v,r,h,t) + CCSFLEX_POW(i,v,r,h,t)) +; + +* --------------------------------------------------------------------------- + +eq_ccsflex_sto_ccsenergy_limit_szn(i,v,r,szn,t)$[tmodel(t)$valgen(i,v,r,t)$ccsflex_sto(i)$szn_rep(szn)$Sw_CCSFLEX_STO].. + sum{h$h_szn(h,szn), hours(h) * CCSFLEX_POW(i,v,r,h,t)} =l= ccsflex_powlim(i,t) * sum{h$h_szn(h,szn), hours(h) * (GEN(i,v,r,h,t) + CCSFLEX_POW(i,v,r,h,t))} +; + +* --------------------------------------------------------------------------- + +eq_ccsflex_sto_ccsenergy_balance(i,v,r,szn,t)$[valgen(i,v,r,t)$ccsflex_sto(i)$tmodel(t)$szn_rep(szn)$Sw_CCSFLEX_STO$(Sw_CCSFLEX_STO_LEVEL=0)].. + sum{h$h_szn(h,szn), hours(h) * CCSFLEX_POWREQ (i,v,r,h,t) } =e= sum{h$h_szn(h,szn), hours(h) * CCSFLEX_POW(i,v,r,h,t) } ; +; + +* --------------------------------------------------------------------------- + +eq_ccsflex_sto_storage_level(i,v,r,h,t)$[valgen(i,v,r,t)$ccsflex_sto(i)$tmodel(t)$Sw_CCSFLEX_STO$(Sw_CCSFLEX_STO_LEVEL=1)].. + +*[plus] storage level in h+1 + sum{(hh)$[nexth(h,hh)], CCSFLEX_STO_STORAGE_LEVEL(i,v,r,hh,t) } + + =e= + +* only want to include storage_level from periods that have had a previous storage_level +* otherwise it becomes a free variable, implying you can charge storage without bound + CCSFLEX_STO_STORAGE_LEVEL(i,v,r,h,t) + +*[plus] storage charging + + ccsflex_sto_storage_eff(i,t) * hours_daily(h) * CCSFLEX_POWREQ(i,v,r,h,t) + +*[minus] storage discharge +*exclude hybrid PV+battery because GEN refers to output from both the PV and the battery + - hours_daily(h) * CCSFLEX_POW(i,v,r,h,t) +; + +* --------------------------------------------------------------------------- + +eq_ccsflex_sto_storage_level_max(i,v,r,h,t)$[valgen(i,v,r,t)$valcap(i,v,r,t)$ccsflex(i)$tmodel(t)$Sw_CCSFLEX_STO$(Sw_CCSFLEX_STO_LEVEL=1)].. + +* [plus] storage duration times storage capacity + ccsflex_sto_storage_duration(i) * CCSFLEX_STO_STORAGE_CAP(i,v,r,t) + + =g= + + CCSFLEX_STO_STORAGE_LEVEL(i,v,r,h,t) +; + +* --------------------------------------------------------------------------- diff --git a/reeds/core/setup/d_mga.gms b/reeds/core/setup/d_mga.gms new file mode 100644 index 00000000..8bf3841b --- /dev/null +++ b/reeds/core/setup/d_mga.gms @@ -0,0 +1,77 @@ +*============================================== +* -- Modeling to Generate Alternatives (MGA) -- +*============================================== + +Equation eq_MGA_CostEnvelope(t) "--$-- System cost must be within allowed envelope" ; +eq_MGA_CostEnvelope(t)$[tmodel(t)$Sw_MGA].. + (z_rep_inv(t) + z_rep_op(t)) * (1 + Sw_MGA_CostDelta) + =g= + Z_inv(t) + Z_op(t) +; + +* --------------------------------------------------------------------------- + +$ifthen.mgaobj %GSw_MGA_Objective% == 'capacity' +Equation eq_MGA_Objective "--MW-- Defines generation capacity for MGA" ; +Variable MGA_OBJ "--MW-- Capacity of technology to be minimized/maximied" ; +eq_MGA_Objective$Sw_MGA.. + MGA_OBJ + =e= + sum{(i,v,r,t) + $[tmodel(t) + $valcap(i,v,r,t) + $%GSw_MGA_SubObjective%(i)], + CAP(i,v,r,t) + } +; + +* --------------------------------------------------------------------------- + +$elseif.mgaobj %GSw_MGA_Objective% == 'transmission' +Equation eq_MGA_Objective "--MW-- Defines transmission capacity for MGA" ; +Variable MGA_OBJ "--MW-- Transmission capacity of all types" ; +eq_MGA_Objective$Sw_MGA.. + MGA_OBJ + =e= + sum{(r,rr,trtype,t) + $[tmodel(t) + $routes(r,rr,trtype,t)], + CAPTRAN_ENERGY(r,rr,trtype,t) + } +; + +* --------------------------------------------------------------------------- + +$elseif.mgaobj %GSw_MGA_Objective% == 'rasharing' +Equation eq_MGA_Objective "--MWh-- Defines RA flows for MGA" ; +Variable MGA_OBJ "--MWh-- Flows between NERC regions during stress periods" ; +eq_MGA_Objective$Sw_MGA.. + MGA_OBJ + =e= + sum{(r,rr,h,trtype,nercr,nercrr,t) + $[tmodel(t) + $routes(r,rr,trtype,t) + $routes_prm(r,rr) + $routes_nercr(nercr,nercrr,r,rr) + $h_stress(h)], + FLOW(r,rr,h,t,trtype) * hours(h) + } +; + +* --------------------------------------------------------------------------- + +$elseif.mgaobj %GSw_MGA_Objective% == 'co2' +Equation eq_MGA_Objective "--tonne-- Defines CO2 emissions for MGA" ; +Variable MGA_OBJ "--tonne-- Direct (process) CO2 emissions" ; +eq_MGA_Objective$Sw_MGA.. + MGA_OBJ + =e= + sum{(r,t) + $[tmodel(t)], + EMIT("process","CO2",r,t) + } +; + +* --------------------------------------------------------------------------- + +$endif.mgaobj diff --git a/reeds/core/setup/d_objective.gms b/reeds/core/setup/d_objective.gms new file mode 100644 index 00000000..38a5b0b3 --- /dev/null +++ b/reeds/core/setup/d_objective.gms @@ -0,0 +1,369 @@ +$ontext +No globals needed for this file +$offtext + +scalar cost_scale "scaling parameter for the objective function" /1/ ; + +Equation +* objective function calculation + eq_ObjFn "--$s-- Objective function calculation" + eq_ObjFn_inv(t) "--$s-- Calculation of investment component of the objective function" + eq_Objfn_op(t) "--$s-- Calculation of operations component of the objective function" +; + +* note these are not restricited to positive domain +Variable Z "--$-- total cost of operations and investment, scale varies based on cost_scale" + Z_op(t) "--$-- total cost of operations", + Z_inv(t) "--$-- total cost of operations" +; + +* objective function is the sum over modeled years of the investment +* and operations components +eq_ObjFn.. Z =e= cost_scale * sum{t$tmodel(t), Z_inv(t) + Z_op(t) } ; + +*======================================================= +* -- Investment component of the objective function -- +*======================================================= + +eq_ObjFn_inv(t)$tmodel(t).. + + Z_inv(t) + + =e= + + pvf_capital(t) * + + ( +* --- investment costs --- + + sum{(i,v,r)$valinv(i,v,r,t), + cost_cap_fin_mult(i,r,t) * cost_cap(i,t) * INV(i,v,r,t) + } + + + sum{(i,v,r)$[valinv(i,v,r,t)$battery(i)], + cost_cap_fin_mult(i,r,t) * cost_cap_energy(i,t) * INV_ENERGY(i,v,r,t) + } + +* --- penalty for exceeding interconnection queue limit --- + + sum{(tg,r), cap_penalty(tg) * CAP_ABOVE_LIM(tg,r,t) } + +* --- growth penalties --- + + sum{(gbin,i,st)$[sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)], + cost_growth(i,st,t) * growth_penalty(gbin) * (yeart(t) - sum{tt$[tprev(t,tt)], yeart(tt) }) * GROWTH_BIN(gbin,i,st,t) + }$[(yeart(t)>=model_builds_start_yr)$Sw_GrowthPenalties$(yeart(t)<=Sw_GrowthPenLastYear)] + +* --- cost of upgrading--- + + sum{(i,v,r)$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], + cost_upgrade(i,v,r,t) * cost_cap_fin_mult(i,r,t) * UPGRADES(i,v,r,t) } + +* --- costs of resource supply curve spur line investment if not modeling explicitly--- +*Note that cost_cap for hydro, pumped-hydro, and geo techs are zero +*but hydro and geo rsc_fin_mult is equal to the same value as cost_cap_fin_mult +* Note: for OSW, export cable, inter-array and POI/substations are eligible for ITC. The rest are not. +* However we apply the ITC to all transmission costs to be consistent with LBW format + + sum{(i,v,r,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$(not spur_techs(i))], + m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) * sum{ii$rsc_agg(i,ii), INV_RSC(ii,v,r,rscbin,t) } } + +* ---cost of spur lines modeled explicitly--- +* NOTE: no rsc_fin_mult(i,r,t) here, but it's 1 for upv and wind-ons anyway + + sum{x$[Sw_SpurScen$xfeas(x)], + spurline_cost(x) * Sw_SpurCostMult * INV_SPUR(x,t) } + +* --- cost of intra-zone network reinforcement (a.k.a. point-of-interconnection capacity or POI) +* Sw_TransIntraCost is in $/kW, so multiply by 1000 to convert to $/MW + + sum{r$Sw_TransIntraCost, + trans_cost_cap_fin_mult(t) * Sw_TransIntraCost * 1000 * INV_POI(r,t) } + +* --- cost of water access--- + + [ (8760/1E6) * sum{ (i,v,w,r)$[i_w(i,w)$valinv(i,v,r,t)], sum{wst$i_wst(i,wst), + m_watsc_dat(wst,"cost",r,t) } * water_rate(i,w) * + ( INV(i,v,r,t) + INV_REFURB(i,v,r,t)$[refurbtech(i)$Sw_Refurb] ) } + + sum{(rscbin,i,v,r)$[m_rscfeas(r,i,rscbin)$psh(i)], + sum{wst$i_wst(i,wst), m_watsc_dat(wst,"cost",r,t) } * + ( INV_RSC(i,v,r,rscbin,t) * water_req_psh(r,rscbin) ) }$Sw_PSHwatercon + ]$Sw_WaterMain + +*slack variable to update water source type (wst) in the unit database +*Note that existing wst data is not consistent with availability of water source in the region + + sum{(wst,r), 1E6 * WATER_CAPACITY_LIMIT_SLACK(wst,r,t) }$[Sw_WaterMain$Sw_WaterCapacity] + +* --- cost of refurbishments of RSC tech--- + + sum{(i,v,r)$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], + cost_cap_fin_mult(i,r,t) * cost_cap(i,t) * INV_REFURB(i,v,r,t) + } + +* --- cost of interzonal AC transmission--- + + sum{(r,rr,tscbin)$[routes_inv(r,rr,"AC",t)$tsc_binwidth(r,rr,tscbin)], + trans_cost_cap_fin_mult(t) * TRAN_CAPEX_BINS(r,rr,tscbin,t) } + +* --- cost of interzonal HVDC transmission--- +* transmission lines: 1 MW adds 1 MW to both INVTRAN(r,rr) and INVTRAN(rr,r) so divide by 2 + + sum{(r,rr,trtype)$[routes_inv(r,rr,trtype,t)$(not aclike(trtype))], + trans_cost_cap_fin_mult(t) + * transmission_cost_nonac(r,rr,trtype) + * INVTRAN(r,rr,trtype,t) + / 2 } + +* LCC and B2B AC/DC converter stations: each interface has two, one on either side of the interface, +* but each interface shows up in both INVTRAN(r,rr) and INVTRAN(rr,r) so don't multiply by 2 + + sum{(r,rr,trtype)$[lcclike(trtype)$routes_inv(r,rr,trtype,t)], + trans_cost_cap_fin_mult(t) * cost_acdc_lcc * INVTRAN(r,rr,trtype,t) } + +* VSC AC/DC converter stations + + sum{r, + trans_cost_cap_fin_mult(t) * cost_acdc_vsc * INV_CONVERTER(r,t) } + +* --- storage capacity credit--- +*small cost penalty to incentivize solver to fill shorter-duration bins first + + sum{(i,v,r,ccseason,sdbin)$[valcap(i,v,r,t)$(storage(i) or hyd_add_pump(i))$(not csp(i))$Sw_PRM_CapCredit$Sw_StorageBinPenalty], + bin_penalty(sdbin) * CAP_SDBIN(i,v,r,ccseason,sdbin,t) } + +* cost of capacity upsizing + + sum{(i,v,r,rscbin)$allow_cap_up(i,v,r,rscbin,t), + cost_cap_fin_mult(i,r,t) * INV_CAP_UP(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } + +* cost of energy upsizing + + sum{(i,v,r,rscbin)$allow_ener_up(i,v,r,rscbin,t), + cost_cap_fin_mult(i,r,t) * INV_ENER_UP(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } + +* H2 transport network investment costs + + sum{(r,rr)$h2_routes_inv(r,rr), cost_h2_transport_cap(r,rr,t) * H2_TRANSPORT_INV(r,rr,t) }$(Sw_H2 = 2) + +* H2 storage investment costs + + sum{(h2_stor,r)$h2_stor_r(h2_stor,r), cost_h2_storage_cap(h2_stor,t) * H2_STOR_INV(h2_stor,r,t) }$(Sw_H2 = 2) + +* CO2 pipeline investment costs + + sum{(r,rr)$co2_routes(r,rr), cost_co2_pipeline_cap(r,rr,t) * CO2_TRANSPORT_INV(r,rr,t) + }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] + + + sum{(r,cs)$[csfeas(cs)$r_cs(r,cs)], cost_co2_spurline_cap(r,cs,t) * CO2_SPURLINE_INV(r,cs,t) + }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] + +*end to multiplier by pvf_capital + ) +; + +*======================================================= +* -- Operational component of the objective function -- +*======================================================= + +eq_Objfn_op(t)$tmodel(t).. + + Z_op(t) + + =e= + + pvf_onm(t) * ( + +* --- variable O&M costs--- +* all technologies except hybrid plant and DAC + sum{(i,v,r,h)$[valgen(i,v,r,t)$cost_vom(i,v,r,t)$(not storage_hybrid(i)$(not csp(i)))], + hours(h) * cost_vom(i,v,r,t) * GEN(i,v,r,h,t) } + +* hybrid plant (plant) + + sum{(i,v,r,h)$[valgen(i,v,r,t)$cost_vom_pvb_p(i,v,r,t)$storage_hybrid(i)$(not csp(i))], + hours(h) * cost_vom_pvb_p(i,v,r,t) * GEN_PLANT(i,v,r,h,t) }$Sw_HybridPlant + +* hybrid plant (Battery) + + sum{(i,v,r,h)$[valgen(i,v,r,t)$cost_vom_pvb_b(i,v,r,t)$storage_hybrid(i)$(not csp(i))], + hours(h) * cost_vom_pvb_b(i,v,r,t) * GEN_STORAGE(i,v,r,h,t) }$Sw_HybridPlant + +* --- fixed O&M costs--- +* generation + + sum{(i,v,r)$[valcap(i,v,r,t)], + cost_fom(i,v,r,t) * CAP(i,v,r,t) } + + + sum{(i,v,r)$[valcap(i,v,r,t)$battery(i)], + cost_fom_energy(i,v,r,t) * CAP_ENERGY(i,v,r,t) } + +* transmission lines + + sum{(r,rr,trtype)$routes(r,rr,trtype,t), + transmission_line_fom(r,rr,trtype) * CAPTRAN_ENERGY(r,rr,trtype,t) } + +* LCC and B2B AC/DC converter stations + + sum{(r,rr,trtype)$[lcclike(trtype)$routes(r,rr,trtype,t)], + cost_acdc_lcc * 2 * trans_fom_frac * CAPTRAN_ENERGY(r,rr,trtype,t) } + +* VSC AC/DC converter stations + + sum{r, + cost_acdc_vsc * trans_fom_frac * CAP_CONVERTER(r,t) } + +* spur lines modeled as part of supply curve + + sum{(i,v,r,rscbin) + $[m_rscfeas(r,i,rscbin)$valcap(i,v,r,t) + $rsc_i(i)$(not spur_techs(i))$(not sccapcosttech(i))], + m_rsc_dat(r,i,rscbin,"cost_trans") * trans_fom_frac * CAP_RSC(i,v,r,rscbin,t) } + +* spur lines modeled explicitly + + sum{x$[Sw_SpurScen$xfeas(x)], + spurline_cost(x) * trans_fom_frac * CAP_SPUR(x,t) } + +* intra-zone network reinforcement (only for new capacity; don't include it for existing POI +* capacity because it's not a great estimate of the actual FOM cost of all existing transmission) + + sum{r$Sw_TransIntraCost, + Sw_TransIntraCost * 1000 * trans_fom_frac + * sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], INV_POI(r,tt) } } + +* --- penalty for retiring a technology (represents friction in retirements)--- + - sum{(i,v,r)$[valcap(i,v,r,t)$retiretech(i,v,r,t)$Sw_RetirePenalty], + cost_fom(i,v,r,t) * retire_penalty(t) * + (CAP(i,v,r,t) + - INV(i,v,r,t)$valinv(i,v,r,t) + - INV_REFURB(i,v,r,t)$[valinv(i,v,r,t)$refurbtech(i)$Sw_Refurb] + - UPGRADES(i,v,r,t)$[upgrade(i)$Sw_Upgrades] ) + } + +* ---operating reserve costs--- + + sum{(i,v,r,h,ortype)$[Sw_OpRes$valgen(i,v,r,t)$cost_opres(i,ortype,t)$reserve_frac(i,ortype)$opres_model(ortype)$opres_h(h)], + hours(h) * cost_opres(i,ortype,t) * OPRES(ortype,i,v,r,h,t) } + + +* --- cost of coal, nuclear, and other fixed-price fuels (except coal used for cofiring), +* plus cost of H2 fuel when using fixed price (Sw_H2=0) or during stress periods. +* When using endogenous H2 price (Sw_H2=1 or Sw_H2=2), H2 fuel cost is captured elsewhere +* via the capex + opex costs of H2 production and its associated electricity demand. + + sum{(i,v,r,h)$[valgen(i,v,r,t)$heat_rate(i,v,r,t) + $(not gas(i))$(not bio(i))$(not cofire(i)) + $((not h2_combustion(i)) or h2_combustion(i)$[(Sw_H2=0) or h_stress(h)])], + hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN(i,v,r,h,t) } + +* --- startup/ramping costs + + sum{(i,r,h,hh)$[Sw_StartCost$startcost(i)$numhours_nexth(h,hh)$valgen_irt(i,r,t)], + startcost(i) * numhours_nexth(h,hh) * RAMPUP(i,r,h,hh,t) } + +* --cofire coal consumption--- +* cofire bio consumption already accounted for in accounting of BIOUSED + + sum{(i,v,r,h)$[valgen(i,v,r,t)$cofire(i)$heat_rate(i,v,r,t)], + (1-bio_cofire_perc) * hours(h) * heat_rate(i,v,r,t) + * fuel_price("coal-new",r,t) * GEN(i,v,r,h,t) } + +* --- cost of natural gas--- +*Sw_GasCurve = 2 (static natural gas prices) +*first - gas consumed for electricity generation + + sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)$(Sw_GasCurve = 2)], + hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN(i,v,r,h,t) } + +*second - gas consumed by gas-powered DAC + + sum{(v,r,h)$[valcap("dac_gas",v,r,t)$(Sw_GasCurve = 2)], + hours(h) * dac_gas_cons_rate("dac_gas",v,t) * PRODUCE("DAC","dac_gas",v,r,h,t) }$Sw_DAC_Gas + +*Sw_GasCurve = 0 (census division supply curves natural gas prices) + + sum{(cendiv,gb), sum{h, hours(h) * GASUSED(cendiv,gb,h,t) } + * gasprice(cendiv,gb,t) + }$(Sw_GasCurve = 0) + +*Sw_GasCurve = 3 (national supply curve for natural gas prices with census division multipliers) + + sum{(h,cendiv,gb), hours(h) * GASUSED(cendiv,gb,h,t) + * gasadder_cd(cendiv,t,h) + gasprice_nat_bin(gb,t) + }$(Sw_GasCurve = 3) + +*Sw_GasCurve = 1 (national and census division supply curves for natural gas prices) +*first - anticipated costs of gas consumption given last year's amount + + (sum{(i,r,v,cendiv,h)$[valgen(i,v,r,t)$gas(i)], + gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * + hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) } + +*second - adjustments based on changes from last year's consumption at the regional and national level + + sum{(fuelbin,cendiv), + gasbinp_regional(fuelbin,cendiv,t) * VGASBINQ_REGIONAL(fuelbin,cendiv,t) } + + + sum{(fuelbin), + gasbinp_national(fuelbin,t) * VGASBINQ_NATIONAL(fuelbin,t) } + + )$[Sw_GasCurve = 1] + +* ---cost of biofuel consumption and biomass transport--- + + sum{(r,bioclass)$[sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,t) }], + BIOUSED(bioclass,r,t) * + (sum{usda_region$r_usda(r,usda_region), biosupply(usda_region, bioclass, "price") } + bio_transport_cost) } + +* --- hurdle costs for transmission flow --- + + sum{(r,rr,h,trtype)$[routes(r,rr,trtype,t)$cost_hurdle(r,rr,t)], + cost_hurdle(r,rr,t) * FLOW(r,rr,h,t,trtype) * hours(h) } + +* --- taxes on emissions--- + + sum{(e,r), (EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream) * emit_tax(e,r,t) } + +* --cost of CO2 transport and storage from CCS-- + + sum{(i,v,r,h)$[valgen(i,v,r,t)], + hours(h) * capture_rate("CO2",i,v,r,t) * GEN(i,v,r,h,t) * Sw_CO2_Storage }$[not Sw_CO2_Detail] + +* --cost of CO2 transport and storage from SMR CCS-- + + sum{(p,v,r,h)$[i_p("smr_ccs",p)$valcap("smr_ccs",v,r,t)], + hours(h) * smr_capture_rate * smr_co2_intensity * PRODUCE(p,"smr_ccs",v,r,h,t) * Sw_CO2_Storage }$[Sw_H2$(not Sw_CO2_Detail)] + +* --cost of CO2 transport and storage from DAC-- + + sum{(p,i,v,r,h)$[dac(i)$valcap(i,v,r,t)$i_p(i,p)], + hours(h) * PRODUCE(p,i,v,r,h,t) * Sw_CO2_Storage }$[Sw_DAC$(not Sw_CO2_Detail)] + +* ---State RPS alternative compliance payments--- + + sum{(RPSCat,st)$[(stfeas(st) or sameas(st,"voluntary"))$RecPerc(RPSCat,st,t)$(not acp_disallowed(st,RPSCat))], + acp_price(st,t) * ACP_PURCHASES(RPSCat,st,t) + }$[(yeart(t)>=firstyear_RPS)$Sw_StateRPS] + +* --- revenues from purchases of curtailed VRE--- + - sum{(r,h), CURT(r,h,t) * hours(h) * cost_curt(t) }$Sw_CurtMarket + +* --- dropped/excess load (ONLY if before Sw_StartMarkets) + + sum{(r,h)$[(yeart(t)=co2_detail_startyr)] + +* --- CO2 spurline fixed OM costs + + sum{(r,cs)$[csfeas(cs)$r_cs(r,cs)], cost_co2_spurline_fom(r,cs,t) + * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], + CO2_SPURLINE_INV(r,cs,tt) } }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] + +* --- CO2 injection break even costs + + sum{(r,cs,h)$r_cs(r,cs), hours(h) * CO2_STORED(r,cs,h,t) * cost_co2_stor_bec(cs,t) }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] + +* --- Tax credit for CO2 stored --- +* note conversion to 12-year CRF given length of CO2 captured incentive payments + - sum{(i,v,r,h)$[valgen(i,v,r,t)$co2_captured_incentive(i,v,r,t)], + (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * capture_rate("CO2",i,v,r,t) * GEN(i,v,r,h,t)} + +* --- Tax credit for CO2 stored for DAC --- + - sum{(p,i,v,r,h)$[dac(i)$valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], + (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * PRODUCE(p,i,v,r,h,t)} + +* --- PTC value for electric power generation --- + - sum{(i,v,r,h)$[valgen(i,v,r,t)$ptc_value_scaled(i,v,t)], + hours(h) * ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t) * + (GEN(i,v,r,h,t) - (STORAGE_IN_GRID(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[pvb(i)$Sw_PVB]) + } + +* --- PTC value for hydrogen production --- +* Note: all electrolyzers which produce H2 are assuming to be receiving the hydrogen production tax credit during eligible years + - sum{(p,v,r,h)$[valcap("electrolyzer",v,r,t)$(sameas(p,"H2"))$h2_ptc("electrolyzer",v,r,t)$h_rep(h)], + hours(h) * PRODUCE(p,"electrolyzer",v,r,h,t) * + (crf(t) / crf_h2_incentive(t)) * h2_ptc("electrolyzer",v,r,t) * 1e3} + $[Sw_H2_PTC$Sw_H2$h2_ptc_years(t)$(yeart(t) >= h2_demand_start)] + +*end multiplier for pvf_onm + ) +; diff --git a/reeds/core/setup/e_solveprep.gms b/reeds/core/setup/e_solveprep.gms new file mode 100644 index 00000000..dbaa6df9 --- /dev/null +++ b/reeds/core/setup/e_solveprep.gms @@ -0,0 +1,254 @@ +$setglobal ds \ + +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +Model ReEDSmodel /all/ ; + +*================================= +* -- MODEL AND SOLVER OPTIONS -- +*================================= + +OPTION lp = %solver% ; +ReEDSmodel.optfile = %GSw_gopt% ; +*treat fixed variables as parameters +ReEDSmodel.holdfixed = 1 ; + +$ifthen %solver%==CBC +* adjust the GAMS infeasibility tolerance to handle empty rows when using CBC +ReEDSmodel.tolinfeas = 1e-15 ; +$endif + + +$if not set loadgdx $setglobal loadgdx 0 + +$ifthen.gdxin %loadgdx% == 1 +execute_loadpoint "gdxfiles%ds%%gdxfin%.gdx" ; +Option BRatio = 0.0 ; +$endif.gdxin + +*================================================ +* --- Parameters only used when loading data --- +*================================================ + +set tload(t) "years in which data is loaded" ; +tload(t) = no ; + +parameter + cc_old_load(i,r,ccreg,ccseason,t) "--MW-- cc_old loading in from the cc_out gdx file" + sdbin_size_load(ccreg,ccseason,sdbin,t) "--MW-- bin_size power loading in from the cc_out gdx file" + cc_mar_load(i,r,ccreg,ccseason,t) "--fraction-- cc_mar loading in from the cc_out gdx file" + cc_evmc_load(i,r,ccseason,t) "--fraction-- cc_evmc loading in from the cc_out gdx file" +; + + +*============================ +* --- Round parameters --- +*============================ +* As a general rule, costs or prices should be rounded to two decimal places +* and all other parameter should be rounded to no more than 3 decimal places +* Some exceptions might exist due to number scaling (e.g., emission rates) +acp_price(st,t)$acp_price(st,t) = round(acp_price(st,t),2) ; +avail_retire_exog_rsc(i,v,r,t)$valcap(i,v,r,t) = round(avail_retire_exog_rsc(i,v,r,t),3) ; +batterymandate(st,t)$batterymandate(st,t) = round(batterymandate(st,t),2) ; +bcr(i)$bcr(i) = round(bcr(i),4) ; +biosupply(usda_region,bioclass,"price") = round(biosupply(usda_region,bioclass,"price"),2) ; +biosupply(usda_region,bioclass,"cap") = round(biosupply(usda_region,bioclass,"cap"),3) ; +cc_storage(i,sdbin)$cc_storage(i,sdbin) = round(cc_storage(i,sdbin),3) ; +cendiv_weights(r,cendiv)$cendiv_weights(r,cendiv) = round(cendiv_weights(r,cendiv), 3) ; +cost_cap(i,t)$cost_cap(i,t) = round(cost_cap(i,t),2) ; +cost_cap_energy(i,t)$cost_cap_energy(i,t) = round(cost_cap_energy(i,t),2) ; +cost_co2_pipeline_fom(r,rr,t) =round(cost_co2_pipeline_fom(r,rr,t),2) ; +cost_co2_pipeline_cap(r,rr,t) =round(cost_co2_pipeline_cap(r,rr,t),2) ; +cost_co2_spurline_fom(r,cs,t) = round(cost_co2_spurline_fom(r,cs,t),2) ; +cost_co2_spurline_cap(r,cs,t) = round(cost_co2_spurline_cap(r,cs,t),2) ; +cost_co2_stor_bec(cs,t) = round(cost_co2_stor_bec(cs,t),2) ; +cost_fom(i,v,r,t)$cost_fom(i,v,r,t) = round(cost_fom(i,v,r,t),2) ; +cost_fom_energy(i,v,r,t)$cost_fom_energy(i,v,r,t) = round(cost_fom_energy(i,v,r,t),2) ; +cost_h2_storage_cap(h2_stor,t) = round(cost_h2_storage_cap(h2_stor,t), 2) ; +cost_h2_transport_cap(r,rr,t)$cost_h2_transport_cap(r,rr,t) = round(cost_h2_transport_cap(r,rr,t),2) ; +cost_h2_transport_fom(r,rr,t)$cost_h2_transport_fom(r,rr,t) = round(cost_h2_transport_fom(r,rr,t),2) ; +cost_opres(i,ortype,t)$cost_opres(i,ortype,t) = round(cost_opres(i,ortype,t),2) ; +cost_prod(i,v,r,t)$cost_prod(i,v,r,t) = round(cost_prod(i,v,r,t), 2) ; +cost_upgrade(i,v,r,t)$cost_upgrade(i,v,r,t) = round(cost_upgrade(i,v,r,t),2) ; +cost_vom(i,v,r,t)$cost_vom(i,v,r,t) = round(cost_vom(i,v,r,t),2) ; +cost_vom_pvb_b(i,v,r,t)$cost_vom_pvb_b(i,v,r,t) = round(cost_vom_pvb_b(i,v,r,t),2) ; +cost_vom_pvb_p(i,v,r,t)$cost_vom_pvb_p(i,v,r,t) = round(cost_vom_pvb_p(i,v,r,t),2) ; +degrade(i,tt,t)$degrade(i,tt,t) = round(degrade(i,tt,t),3) ; +derate_geo_vintage(i,v)$derate_geo_vintage(i,v) = round(derate_geo_vintage(i,v),3) ; +distance(r,rr,trtype)$distance(r,rr,trtype) = round(distance(r,rr,trtype),3) ; +* non-CO2 emission/capture rates get small, here making sure accounting stays correct +emit_rate(etype,e,i,v,r,t)$valgen(i,v,r,t) = round(emit_rate(etype,e,i,v,r,t),10) ; +capture_rate(e,i,v,r,t)$valgen(i,v,r,t) = round(capture_rate(e,i,v,r,t),6) ; +fuel_price(i,r,t)$fuel_price(i,r,t) = round(fuel_price(i,r,t),2) ; +gasmultterm(cendiv,t)$gasmultterm(cendiv,t) = round(gasmultterm(cendiv,t),3) ; +heat_rate(i,v,r,t)$heat_rate(i,v,r,t) = round(heat_rate(i,v,r,t),2) ; +m_capacity_exog(i,v,r,t)$[valcap(i,v,r,t)$(not sameas(i,"smr"))] = round(m_capacity_exog(i,v,r,t),3) ; +m_capacity_exog_energy(i,v,r,t)$[valcap(i,v,r,t)] = round(m_capacity_exog_energy(i,v,r,t),3) ; +m_rsc_dat(r,i,rscbin,"cap")$m_rsc_dat(r,i,rscbin,"cap") = round(m_rsc_dat(r,i,rscbin,"cap"),3) ; +m_rsc_dat(r,i,rscbin,"cost")$m_rsc_dat(r,i,rscbin,"cost") = round(m_rsc_dat(r,i,rscbin,"cost"),2) ; +m_rsc_dat(r,i,rscbin,"cost_trans")$m_rsc_dat(r,i,rscbin,"cost_trans") = round(m_rsc_dat(r,i,rscbin,"cost_trans"),2) ; +prm(r,t)$prm(r,t) = round(prm(r,t),3) ; +prod_conversion_rate(i,v,r,t)$prod_conversion_rate(i,v,r,t) = round(prod_conversion_rate(i,v,r,t),6) ; +ptc_value_scaled(i,v,t)$ptc_value_scaled(i,v,t) = round(ptc_value_scaled(i,v,t),2) ; +recperc(rpscat,st,t)$recperc(rpscat,st,t) = round(recperc(rpscat,st,t),3) ; +rggi_cap(t)$rggi_cap(t) = round(rggi_cap(t),0) ; +state_cap(st,t)$state_cap(st,t) = round(state_cap(st,t),0) ; +storage_eff_pvb_g(i,t)$storage_eff_pvb_g(i,t) = round(storage_eff_pvb_g(i,t),3) ; +storage_eff_pvb_p(i,t)$storage_eff_pvb_p(i,t) = round(storage_eff_pvb_p(i,t),3) ; +tranloss(r,rr,trtype)$tranloss(r,rr,trtype) = round(tranloss(r,rr,trtype),3) ; +tsc_binwidth(r,rr,tscbin)$tsc_binwidth(r,rr,tscbin) = round(tsc_binwidth(r,rr,tscbin),2) ; +tsc_forward(r,rr,tscbin)$tsc_forward(r,rr,tscbin) = round(tsc_forward(r,rr,tscbin),2) ; +tsc_reverse(r,rr,tscbin)$tsc_reverse(r,rr,tscbin) = round(tsc_reverse(r,rr,tscbin),2) ; +transmission_cost_nonac(r,rr,trtype)$transmission_cost_nonac(r,rr,trtype) = round(transmission_cost_nonac(r,rr,trtype),2) ; +transmission_line_fom(r,rr,trtype)$transmission_line_fom(r,rr,trtype) = round(transmission_line_fom(r,rr,trtype),3) ; +trans_cost_cap_fin_mult(t) = round(trans_cost_cap_fin_mult(t),3) ; +trans_cost_cap_fin_mult_noITC(t) = round(trans_cost_cap_fin_mult_noITC(t),3) ; +upgrade_derate(i,v,r,t)$upgrade_derate(i,v,r,t) = round(upgrade_derate(i,v,r,t),3) ; +winter_cap_frac_delta(i,v,r)$winter_cap_frac_delta(i,v,r) = round(winter_cap_frac_delta(i,v,r),3) ; + + +*================================================ +* --- SEQUENTIAL SETUP --- +*================================================ +$ifthen.seq %timetype%=="seq" + +* -- upgrade capacity tracking -- +m_capacity_exog0(i,v,r,t) = m_capacity_exog(i,v,r,t) ; + +* remove cc_int as it is only used in the intertemporal setting +cc_int(i,v,r,ccseason,t) = 0 ; + +*for the sequential solve, what matters is the relative ratio of the pvf for capital and the pvf for onm +*therefore, we set the pvf capital to one, and then pvf_onm to the relative 20 year present value by using the crf +pvf_capital(t) = 1 ; +pvf_onm(t)$tmodel_new(t) = round(1 / crf(t),6) ; + +$endif.seq + + +*================================================ +* --- INTERTEMPORAL AND WINDOW SETUP --- +*================================================ + +$ifthen.intwin ((%timetype%=="int") or (%timetype%=="win")) + +set + loadset "set used for loading in merged gdx files" / ReEDS_Augur_%startyear%*ReEDS_Augur_%endyear% / +; + +parameter + cc_evmc_load2(loadset,i,r,ccseason,t) "--fraction-- cc_evmc loading in from the cc_out gdx file" + cc_iter(i,v,r,ccseason,t,cciter) "--fraction-- Actual capacity value in iteration cciter" + cc_mar_load2(loadset,i,r,ccseason,t) "--fraction-- cc_mar loading in from the cc_out gdx file" + cc_old_load2(loadset,i,r,ccseason,t) "--MW-- cc_old loading in from the cc_out gdx file" + cc_scale(i,r,ccseason,t) "--unitless-- scaling of marginal capacity value levels in intertemporal runs to equal total capacity value" + cc_totmarg(i,r,ccseason,t) "--MW-- original estimate of total capacity value for intertemporal, based on marginals" + sdbin_size_load2(loadset,ccreg,ccseason,sdbin,t) "--MW-- bin_size power loading in from the cc_out gdx file" +; + +cc_scale(i,r,ccseason,t) = 0 ; +cc_totmarg(i,r,ccseason,t) = 0 ; + +$endif.intwin + + +*================================================ +* --- INTERTEMPORAL SETUP --- +*================================================ + +$ifthen.int %timetype%=="int" + +* Iteration tracking +set cciter "placeholder for iteration number for tracking CC" /0*20/ ; +parameter cap_iter(i,v,r,t,cciter) "--MW-- Power apacity by iteration" + cap_energy_iter(i,v,r,t,cciter) "--MWh-- Energy capacity by iteration" + gen_iter(i,v,r,t,cciter) "--MWh-- Annual uncurtailed generation by iteration" + cap_firm_iter(i,v,r,ccseason,t,cciter) "--MW-- VRE Firm capacity by iteration" + cap_energy_firm_iter(i,v,r,ccseason,t,cciter) "--MWh-- VRE Firm energy capacity by iteration" +; +cap_iter(i,v,r,t,cciter) = 0 ; +cap_energy_iter(i,v,r,t,cciter) = 0 ; +gen_iter(i,v,r,t,cciter) = 0 ; + + +*Assign csp3 and csp4 to use the same initial values as csp2_1 +cc_int(i,v,r,ccseason,t)$[csp3(i) or csp4(i)] = cc_int('csp2_1',v,r,ccseason,t) ; + +tmodel(t) = no ; +tmodel(t)$[tmodel_new(t)$(yeart(t)<=%endyear%)] = yes ; + + +*Cap the maximum CC in the first solve iteration +cc_int(i,v,r,ccseason,t)$[rsc_i(i)$(cc_int(i,v,r,ccseason,t)>0.4)$wind(i)] = 0.4 ; +cc_int(i,v,r,ccseason,t)$[rsc_i(i)$(cc_int(i,v,r,ccseason,t)>0.6)$pv(i)] = 0.6 ; + + +*set objective function to millions of dollars +cost_scale = 1 ; + +*marginal capacity value not used in intertemporal case +m_cc_mar(i,r,ccseason,t) = 0 ; +*static capacity value for existing capacity not used in intertemporal case +cc_old(i,r,ccseason,t) = 0 ; + +*sets needed for the demand side +*following sets are needed for linear interpolation of price +*that determine the year before the non-solved year and the year after +set t_before, t_after ; +alias(t,ttt) ; +t_before(t,tt)$[tprev(t,tt)$(ord(tt) = smax{ttt, ord(ttt)$tprev(t,ttt) })] = yes ; +t_after(t,tt)$tprev(tt,t) = yes ; + +* intentionally not declaring all indices to make these flexibile +* rep only used when running the demand side +parameter rep "reporting for all sectors/timeslices/regions" + repannual(t,*,*) "national and annual reporting" +; + +$endif.int + + +*======================= +* --- WINDOW SETUP --- +*======================= + +$ifthen.win %timetype%=="win" + +parameter pvf_capital0(t) "original pvf_capital used for calculating pvf_capital in window solve", + pvf_onm0(t) "original pvf_onm used for calculating pvf_capital in window solve"; + +pvf_capital0(t) = pvf_capital(t) ; +pvf_onm0(t) = pvf_onm(t) ; + +cost_scale = 1e-3 ; + +*Assign csp3 and csp4 to use the same initial values as csp2_1 +cc_int(i,v,r,ccseason,t)$[csp3(i) or csp4(i)] = cc_int('csp2_1',v,r,ccseason,t) ; + +*marginal capacity value not used in intertemporal case +m_cc_mar(i,r,ccseason,t) = 0 ; +*static capacity value for existing capacity not used in intertemporal case +cc_old(i,r,ccseason,t) = 0 ; + +tmodel(t) = no ; + +set windows /1*40/ ; +set blocks /start,stop/ ; + + +table solvewindows(windows,blocks) +$ondelim +$include inputs_case%ds%windows.csv +$offdelim +; + +$endif.win + + +*====================== +* --- Unload all inputs --- +*====================== + +execute_unload 'inputs_case%ds%inputs.gdx' ; diff --git a/reeds/core/solve/1_tc_phaseout.py b/reeds/core/solve/1_tc_phaseout.py new file mode 100644 index 00000000..812c220e --- /dev/null +++ b/reeds/core/solve/1_tc_phaseout.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 2 16:36:48 2021 + +@author: pgagnon + +This script ingests historical CO2 direct combustion trends, determines if the +trigger for tax credit phasedown has been hit, and if so, outputs a tech-specific +adjustment to tax credit (both PTC and ITC) value. + +Based on the Inflation Reduction Act of 2022 + +""" +########### +#%% IMPORTS +import argparse +import pandas as pd +import numpy as np +import gdxpds +import os +import sys +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent.parent)) +import reeds + +########## +#%% INPUTS +use_historical = True + +############# +#%% FUNCTIONS +def calc_tc_phaseout_mult(year, case, use_historical=use_historical): + ''' + The TC phase down schedule starts the year after the trigger year. + GSw_TCPhaseout_start is the earliest allowed trigger year. + Example: If the conditions are met in 2033, + then the 0th value of the specified tc phaseout schedule applies in 2034 + + tc_phaseout_schedule: dataframe with ['n_yr_after_trigger', 'tc_phaseout_mult'] + Implicitly assumes that the tc value is zero after schedule is complete. + If tc phases to non-zero value, either enter that in the schedule or adjust code + ''' + # #%% Debugging + # year = 2035 + # case = os.path.expanduser('~/github2/ReEDS-2.0/runs/v20230305_reccM0_ref_seq') + + #%% Get switches + sw = reeds.io.get_switches(case) + GSw_TCPhaseout_trigger_f = float(sw.GSw_TCPhaseout_trigger_f) + GSw_TCPhaseout_ref_year = int(sw.GSw_TCPhaseout_ref_year) + GSw_TCPhaseout_start = int(sw.GSw_TCPhaseout_start) + GSw_TCPhaseout_forceyear = int(sw.GSw_TCPhaseout_forceyear) + startyear=int(sw.startyear) + + ### Set input/output path + tc_file_dir = os.path.join(case, 'outputs', 'tc_phaseout_data') + + # Import tech groups. Used to expand const_times + # (e.g., 'UPV' expands to all of the upv subclasses, like upv_1, upv_2, etc) + tech_groups = reeds.techs.import_tech_groups( + os.path.join(case, 'inputs_case', 'tech-subset-table.csv')) + + # The phasedown schedule is defined starting with the first year following the trigger year + # This schedule is for projects "commencing construction" + tc_phaseout = pd.read_csv(os.path.join(case, 'inputs_case', 'tc_phaseout_schedule.csv')) + + # The safe harbor window defines how long a project can be considered under construction. + # Note that even though we can specify incentive-level safe harbors in the inputs, we are + # calculating the single phaseout mult with the maximum safe harbor. This is an expedient for + # lack of time to create a phaseout for each incentive. + safe_harbors = pd.read_csv( + os.path.join(case, 'inputs_case', 'safe_harbor.csv') + ).rename(columns={'*i':'i', 't':'t_online'}) + + const_times = pd.read_csv( + os.path.join(case, 'inputs_case', 'construction_times.csv')) + + yearset = pd.read_csv( + os.path.join(case, 'inputs_case', 'modeledyears.csv') + ).columns.astype(int).values + + # Calc for all years that are covered by this modeled year, then avg the credit + if year==yearset.min(): + covered_years = [year] + else: + covered_years = np.arange(yearset[yearset GSw_TCPhaseout_start: + most_recent_year = max(yearset[yearset= int(sw['GSw_StartMarkets'])) + ].copy() + + # If at least one year fell below the trigger value, + # identify it and find each tech's tc_phaseout_mult + # OR if GSw_TCPhaseout_forceyear is nonzero, use it as the trigger year + if (len(df_qual) > 0) or GSw_TCPhaseout_forceyear: + if GSw_TCPhaseout_forceyear: + trigger_year = GSw_TCPhaseout_forceyear + else: + trigger_year = max([min(df_qual.index), GSw_TCPhaseout_start]) + + print(f'<><><> IRA tax credits start phasing out in {trigger_year} <><><>') + + const_times['n_yr_after_trigger'] = const_times['t_start_build'] - trigger_year + + const_times = const_times.merge( + tc_phaseout[['n_yr_after_trigger', 'tc_phaseout_mult']], + on='n_yr_after_trigger', how='left') + + const_times['tc_phaseout_mult'] = np.where( + const_times['n_yr_after_trigger']<=0, + 1.0, + const_times['tc_phaseout_mult']) + const_times['tc_phaseout_mult'] = np.where( + const_times['n_yr_after_trigger']>tc_phaseout['n_yr_after_trigger'].max(), + 0.0, + const_times['tc_phaseout_mult']) + + tc_phaseout_mult = ( + const_times[['i', 'tc_phaseout_mult']] + .groupby('i', as_index=False).mean() + ) + + # If no years fell below the trigger value, tc phaseout has not begun, + # so just set tc_phaseout_mult to 1.0 for all techs + else: + tc_phaseout_mult = const_times[['i']].copy() + tc_phaseout_mult['tc_phaseout_mult'] = 1.0 + + # If the first allowable trigger year has not yet been reached, tc phaseout has not begun, + # so just set tc_phaseout_mult to 1.0 for all techs + else: + tc_phaseout_mult = const_times[['i']].copy() + tc_phaseout_mult['tc_phaseout_mult'] = 1.0 + + # Round for GAMS + tc_phaseout_mult['tc_phaseout_mult'] = np.round(tc_phaseout_mult['tc_phaseout_mult'], 3) + tc_phaseout_mult['t'] = year + + data = {'tc_phaseout_mult_t':tc_phaseout_mult[['i', 't', 'tc_phaseout_mult']]} + gdxpds.to_gdx(data, os.path.join(tc_file_dir, 'tc_phaseout_mult_%s.gdx' % year)) + + +############# +#%% PROCEDURE +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description="""Running tc_phaseout.py""") + parser.add_argument("year", help="ReEDS solve year", type=int) + parser.add_argument("case", help="filepath for ReEDS case") + args = parser.parse_args() + year = args.year + case = args.case + + ### Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(case,'gamslog.txt'), + ) + + print(f'starting tc_phaseout.py for {year}') + calc_tc_phaseout_mult(year, case, use_historical=use_historical) + print(f'finished tc_phaseout.py for {year}') diff --git a/reeds/core/solve/2_financials.gms b/reeds/core/solve/2_financials.gms new file mode 100644 index 00000000..e6e6815f --- /dev/null +++ b/reeds/core/solve/2_financials.gms @@ -0,0 +1,156 @@ +* --- Ingest tax credit phaseout mult --- * + +$gdxin outputs%ds%tc_phaseout_data%ds%tc_phaseout_mult_%cur_year%.gdx +$loaddcr tc_phaseout_mult_t_load = tc_phaseout_mult_t +$gdxin + +tc_phaseout_mult_t(i,t)$tload(t) = tc_phaseout_mult_t_load(i,t) ; + +* If tcphaseout is enabled, overwrite initialization +* This requires re-calculating cost_cap_fin_mult and its various permutations +if(Sw_TCPhaseout > 0, +* Apply the phaseout multiplier of the current level for current year +* and all future builds. Note that the value will remain the same for +* the cur_year-available vintage but can be updated for vintages whose +* first year hasn't solved yet. i.e. tc_phaseout_mult will remain constant for all +* current and historically-buildable plants but future plants may get updated. +tc_phaseout_mult(i,v,t)$[tload(t)$(firstyear_v(i,v)>=%cur_year%)] = + tc_phaseout_mult_t(i,t) ; +); + + +* --- Start calculations of cost_cap_fin_mult family of parameters --- * + +cost_cap_fin_mult(i,r,t) = ccmult(i,t) / (1.0 - tax_rate(t)) + * (1.0-tax_rate(t) * (1.0 - (itc_frac_monetized(i,t) * itc_energy_comm_bonus(i,r) * tc_phaseout_mult_t(i,t)/2.0) ) + * pv_frac_of_depreciation(i,t) - itc_frac_monetized(i,t) * tc_phaseout_mult_t(i,t)) + * degradation_adj(i,t) * financing_risk_mult(i,t) * (1 + reg_cap_cost_diff(i,r)) + * eval_period_adj_mult(i,t) ; + +cost_cap_fin_mult_noITC(i,r,t) = ccmult(i,t) / (1.0 - tax_rate(t)) + * (1.0-tax_rate(t)*pv_frac_of_depreciation(i,t)) * degradation_adj(i,t) + * financing_risk_mult(i,t) * (1 + reg_cap_cost_diff(i,r)) * eval_period_adj_mult(i,t) ; + +cost_cap_fin_mult_no_credits(i,r,t) = ccmult(i,t) * (1 + reg_cap_cost_diff(i,r)) ; + +* Assign the PV portion of PVB the value of UPV +cost_cap_fin_mult_pvb_p(i,r,t)$pvb(i) = + sum{ii$[upv(ii)$rsc_agg(ii,i)], cost_cap_fin_mult(ii,r,t) } ; + +cost_cap_fin_mult_pvb_p_noITC(i,r,t)$pvb(i) = + sum{ii$[upv(ii)$rsc_agg(ii,i)], cost_cap_fin_mult_noITC(ii,r,t) } ; + +cost_cap_fin_mult_pvb_p_no_credits(i,r,t)$pvb(i) = + sum{ii$[upv(ii)$rsc_agg(ii,i)], cost_cap_fin_mult_no_credits(ii,r,t) } ; + +* In the financing module (python), PVB refers to the battery portion of the hybrid. +* This convention is used to estimate the ITC benefit for the battery. +* Assign the battery portion of PVB the value computed in the financing module for PVB +cost_cap_fin_mult_pvb_b(i,r,t)$pvb(i) = cost_cap_fin_mult(i,r,t) ; +cost_cap_fin_mult_pvb_b_noITC(i,r,t)$pvb(i) = cost_cap_fin_mult_noITC(i,r,t) ; +cost_cap_fin_mult_pvb_b_no_credits(i,r,t)$pvb(i) = cost_cap_fin_mult_no_credits(i,r,t) ; + +* Assign "cost_cap_fin_mult" for PVB to be the weighted average of the PV and battery portions +* The weighting is based on: +* (1) the cost of each portion: PV=cost_cap_pvb_p; Battery=cost_cap_pvb_b +* (2) the relative size of each portion: PV=1; Battery=bcr +* The "-1" and "+1" values are needed because the multipliers are adjustments off of 1.0 +cost_cap_fin_mult(i,r,t)$pvb(i) = + ( (cost_cap_fin_mult_pvb_p(i,r,t) - 1) * cost_cap_pvb_p(i,t) + + bcr(i) * (cost_cap_fin_mult_pvb_b(i,r,t) - 1) * cost_cap_pvb_b(i,t) ) + / (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) + 1 ; + +cost_cap_fin_mult_noITC(i,r,t)$pvb(i) = + ( (cost_cap_fin_mult_pvb_p_noITC(i,r,t) - 1) * cost_cap_pvb_p(i,t) + + bcr(i) * (cost_cap_fin_mult_pvb_b_noITC(i,r,t) - 1) * cost_cap_pvb_b(i,t) ) + / (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) + 1 ; + +cost_cap_fin_mult_no_credits(i,r,t)$pvb(i) = + ((cost_cap_fin_mult_pvb_p_no_credits(i,r,t) - 1) * cost_cap_pvb_p(i,t) + + bcr(i) * (cost_cap_fin_mult_pvb_b_no_credits(i,r,t) - 1) * cost_cap_pvb_b(i,t)) + / (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) + 1 ; + +* --- Upgrades --- +* Assign upgraded techs the same multipliers as the techs they are upgraded from +* This assignment must take place after expanding for water techs, if applicable. + +if(Sw_WaterMain=1, +cost_cap_fin_mult(i,r,t)$i_water_cooling(i) = + sum{(ii)$[ctt_i_ii(i,ii)], cost_cap_fin_mult(ii,r,t) } ; + +cost_cap_fin_mult_noITC(i,r,t)$i_water_cooling(i) = + sum{(ii)$[ctt_i_ii(i,ii)], cost_cap_fin_mult_noITC(ii,r,t) } ; + +cost_cap_fin_mult_no_credits(i,r,t)$i_water_cooling(i) = + sum{(ii)$[ctt_i_ii(i,ii)], cost_cap_fin_mult_no_credits(ii,r,t) } ; +) ; + +cost_cap_fin_mult(i,r,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), cost_cap_fin_mult(ii,r,t) } ; +cost_cap_fin_mult_noITC(i,r,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), cost_cap_fin_mult_noITC(ii,r,t) } ; + +* --- Nuclear Ban --- +* Assign increased cost multipliers to regions with state nuclear bans +if(Sw_NukeStateBan = 2, + cost_cap_fin_mult(i,r,t)$[nuclear(i)$nuclear_ba_ban(r)] = + cost_cap_fin_mult(i,r,t) * nukebancostmult ; + + cost_cap_fin_mult_noITC(i,r,t)$[nuclear(i)$nuclear_ba_ban(r)] = + cost_cap_fin_mult_noITC(i,r,t) * nukebancostmult ; +) ; + +* Start by setting all multipliers to 1 +rsc_fin_mult(i,r,t)$[valcap_irt(i,r,t)$rsc_i(i)] = 1 ; +rsc_fin_mult_noITC(i,r,t)$[valcap_irt(i,r,t)$rsc_i(i)] = 1 ; + +* Hydro, pumped-hydro, and dr-shed have capital costs included in the supply curve, +* so change their multiplier to be the same as cost_cap_fin_mult adjusted by their +* capital cost multipliers. +rsc_fin_mult(i,r,t)$hydro(i) = cost_cap_fin_mult('hydro',r,t) * hydrocapmult(t,i) ; +rsc_fin_mult_noITC(i,r,t)$hydro(i) = cost_cap_fin_mult_noITC('hydro',r,t) * hydrocapmult(t,i) ; +rsc_fin_mult(i,r,t)$psh(i) = cost_cap_fin_mult(i,r,t) * hydrocapmult(t,i) ; +rsc_fin_mult_noITC(i,r,t)$psh(i) = cost_cap_fin_mult_noITC(i,r,t) * hydrocapmult(t,i) ; +rsc_fin_mult(i,r,t)$dr_shed(i) = cost_cap_fin_mult(i,r,t)* dr_shed_capmult(i,r,t) ; +rsc_fin_mult_noITC(i,r,t)$dr_shed(i) = cost_cap_fin_mult_noITC(i,r,t)* dr_shed_capmult(i,r,t) ; + +* Create a new parameter to hold capital financing multipliers with and without ITC for OSW transmission costs inside the resource supply curve cost +* Currently, OSW receives federal incentives in both its capital and transmission costs, hence this custom application for OSW +rsc_fin_mult(i,r,t)$ofswind(i) = cost_cap_fin_mult(i,r,t) * ofswind_rsc_mult(t,i) ; +rsc_fin_mult_noITC(i,r,t)$ofswind(i) = cost_cap_fin_mult_noITC(i,r,t) ; + +* Trim the cost_cap_fin_mult parameters to reduce file sizes +cost_cap_fin_mult(i,r,t)$[(not valinv_irt(i,r,t))$(not upgrade(i)) + $(not sum{(v,rscbin), allow_cap_up(i,v,r,rscbin,t) }) + $(not sum{(v,rscbin), allow_ener_up(i,v,r,rscbin,t) })] = 0 ; + +cost_cap_fin_mult_noITC(i,r,t)$[(not valinv_irt(i,r,t))$(not upgrade(i)) + $(not sum{(v,rscbin), allow_cap_up(i,v,r,rscbin,t) }) + $(not sum{(v,rscbin), allow_ener_up(i,v,r,rscbin,t) })] = 0 ; + +cost_cap_fin_mult_no_credits(i,r,t)$[(not valinv_irt(i,r,t))$(not upgrade(i)) + $(not sum{(v,rscbin), allow_cap_up(i,v,r,rscbin,t) }) + $(not sum{(v,rscbin), allow_ener_up(i,v,r,rscbin,t) })] = 0 ; + +* Round the cost_cap_fin_mult parameters, since they were re-calculated +cost_cap_fin_mult(i,r,t)$cost_cap_fin_mult(i,r,t) + = round(cost_cap_fin_mult(i,r,t),3) ; + +cost_cap_fin_mult_noITC(i,r,t)$cost_cap_fin_mult_noITC(i,r,t) + = round(cost_cap_fin_mult_noITC(i,r,t),3) ; + +cost_cap_fin_mult_no_credits(i,r,t)$cost_cap_fin_mult_no_credits(i,r,t) + = round(cost_cap_fin_mult_no_credits(i,r,t),3) ; + +rsc_fin_mult(i,r,t)$rsc_fin_mult(i,r,t) = round(rsc_fin_mult(i,r,t),3) ; + +* Set cost_cap_fin_mult_out equal to cost_cap_fin_mult before we alter cost_cap_fin_mult +* for state fossil retirement policies and/or a full-region zero-carbon policy. +cost_cap_fin_mult_out(i,r,t) = cost_cap_fin_mult(i,r,t) ; + +* Penalize new gas built within cost recovery period of 20 years for states that require +* fossil retirements if Sw_StateRPS=1 and/or within 20 years of a zero-carbon policy +cost_cap_fin_mult(i,r,t)$[gas(i)$valcap_irt(i,r,t)] = + cost_cap_fin_mult(i,r,t) + * max(sum{st$r_st(r,st), ng_crf_penalty_st(t,st) }$(not ccs(i))$Sw_StateRPS$(yeart(t)>=firstyear_RPS), + ng_crf_penalty_nat(i,t) ) ; + +* --- End calculations of cost_cap_fin_mult family of parameters --- * \ No newline at end of file diff --git a/reeds/core/solve/2_temporal_params.gms b/reeds/core/solve/2_temporal_params.gms new file mode 100644 index 00000000..ecb54ebe --- /dev/null +++ b/reeds/core/solve/2_temporal_params.gms @@ -0,0 +1,910 @@ +*============================================= +* -- Timeslices and seasons -- +*============================================= + +Sets +h_rep(allh) "representative timeslices" +/ +$offlisting +$include inputs_case%ds%%temporal_inputs%%ds%set_h.csv +$onlisting +/ + +$onempty +h_stress(allh) "stress timeslices for the current model year" +/ +$offlisting +$include inputs_case%ds%stress%stress_year%%ds%set_h.csv +$onlisting +/ +$offempty + + +szn_rep(allszn) "representative periods, or seasons if modeling full year" +/ +$offlisting +$include inputs_case%ds%%temporal_inputs%%ds%set_szn.csv +$onlisting +/ + +actualszn(allszn) "actual periods (each is described by a representative period)" +/ +$offlisting +$include inputs_case%ds%%temporal_inputs%%ds%set_actualszn.csv +$onlisting +/ + +$onempty +szn_stress(allszn) "stress periods for the current model year" +/ +$offlisting +$include inputs_case%ds%stress%stress_year%%ds%set_szn.csv +$onlisting +/ +$offempty +; + +* The h set contains h_rep and h_stress; the szn set containts szn_rep and szn_stress +h(allh) = no ; +h(allh)$[h_rep(allh)] = yes ; +h(allh)$[h_stress(allh)] = yes ; + +szn(allszn) = no ; +szn(allszn)$[szn_rep(allszn)] = yes ; +szn(allszn)$[szn_stress(allszn)] = yes ; + +Sets +$ONEMPTY +h_preh(allh, allh) "mapping set between one timeslice and all other timeslices earlier in that period" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%h_preh.csv +$include inputs_case%ds%stress%stress_year%%ds%h_preh.csv +$offdelim +$onlisting +/ +$OFFEMPTY + +h_szn(allh,allszn) "mapping of hour blocks to seasons" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%h_szn.csv +$include inputs_case%ds%stress%stress_year%%ds%h_szn.csv +$offdelim +$onlisting +/ + +h_szn_start(allszn,allh) "starting hour of each season" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%h_szn_start.csv +$include inputs_case%ds%stress%stress_year%%ds%h_szn_start.csv +$offdelim +$onlisting +/ + +h_szn_end(allszn,allh) "ending hour of each season" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%h_szn_end.csv +$include inputs_case%ds%stress%stress_year%%ds%h_szn_end.csv +$offdelim +$onlisting +/ + +* Inter-seasonal storage level (e.g. H2), which uses h_actualszn and nexth_actualszn, +* is only tracked over actual ("energy") periods, not stress periods +h_actualszn(allh,allszn) "mapping from rep timeslices to actual periods" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%h_actualszn.csv +$offdelim +$onlisting +/ +$ONEMPTY +szn_actualszn(allszn,allszn) "mapping from rep timeslices to actual periods" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%szn_actualszn.csv +$offdelim +$onlisting +/ +$OFFEMPTY +nexth_actualszn(allszn,allh,allszn,allh) "Mapping between one timeslice and the next for actual periods (szns)" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%nexth_actualszn.csv +$offdelim +$onlisting +/ + +nexth(allh,allh) "Mapping set between one timeslice (first) and the following (second)" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%nexth.csv +$include inputs_case%ds%stress%stress_year%%ds%nexth.csv +$offdelim +$onlisting +/ +$ONEMPTY +nextpartition(allszn,allszn) "Mapping between one partition (allszn) and the next" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%nextpartition.csv +$offdelim +$onlisting +/ +$OFFEMPTY +; + +* Record the stress periods for each model year +h_t(allh,t)$[tmodel(t)] = no ; +h_t(h,t)$[tmodel(t)] = yes ; + +szn_t(allszn,t)$[tmodel(t)] = no ; +szn_t(szn,t)$[tmodel(t)] = yes ; + +h_stress_t(allh,t)$[h_stress(allh)$tmodel(t)] = no ; +h_stress_t(h,t)$[h_stress(h)$tmodel(t)] = yes ; + +szn_stress_t(allszn,t)$[szn_stress(allszn)$tmodel(t)] = no ; +szn_stress_t(szn,t)$[szn_stress(szn)$tmodel(t)] = yes ; + +h_szn_t(allh,allszn,t)$[h_szn(allh,allszn)$tmodel(t)] = no ; +h_szn_t(h,szn,t)$[h_szn(h,szn)$tmodel(t)] = yes ; + +$offOrder +set starting_hour(allh) "starting hour without tz adjustments" + final_hour(allh) "final hour without tz adjustments" +; + +* find the minimum and maximum ordinal of modeled hours within each season +starting_hour(allh) = no ; +starting_hour(h)$[sum{szn,h_szn(h,szn)$(smin(hh$h_szn(hh,szn),ord(hh))=ord(h)) }] = yes ; + +final_hour(allh) = no ; +final_hour(h)$[sum{szn,h_szn(h,szn)$(smax(hh$h_szn(hh,szn),ord(hh))=ord(h)) }] = yes ; + +* note summing over szn to find the minimum/maximum ordered hour within that season +starting_hour_nowrap(allh) = no ; +starting_hour_nowrap(h)$[sum{szn, h_szn_start(szn,h) }$(not Sw_HourlyWrap)] = yes ; + +final_hour_nowrap(allh) = no ; +final_hour_nowrap(h)$[sum{szn, h_szn_end(szn,h) }$(not Sw_HourlyWrap)] = yes ; + +* Get the order of actual periods +nextszn(actualszn,actualsznn)$[(ord(actualsznn) = ord(actualszn) + 1)] = yes ; +nextszn(actualszn,actualsznn) + $[(ord(actualszn) = smax(actualsznnn, ord(actualsznnn))) + $(ord(actualsznn) = smin(actualsznnn, ord(actualsznnn)))] + = yes ; + +$onOrder + + +parameter hours(allh) "--hours-- number of hours in each time block" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%numhours.csv +$include inputs_case%ds%stress%stress_year%%ds%numhours.csv +$offdelim +$onlisting +/ ; + +parameter numdays(allszn) "--number of days-- number of days for each season" ; +numdays(allszn) = 0 ; +numdays(szn) = sum{h$h_szn(h,szn),hours(h) } / 24 ; +$ONEMPTY +parameter numpartitions(allszn) "--number of periods-- number of partitions for each season in timeseries" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%numpartitions.csv +$offdelim +$onlisting +/ ; +$OFFEMPTY +parameter numhours_nexth(allh,allhh) "--hours-- number of times hh follows h throughout year" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%numhours_nexth.csv +$offdelim +$onlisting +/ ; + +* Written by reeds/inputs/hourly_writetimeseries.py +parameter frac_h_quarter_weights(allh,quarter) "--unitless-- fraction of timeslice associated with each quarter" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%frac_h_quarter_weights.csv +$include inputs_case%ds%stress%stress_year%%ds%frac_h_quarter_weights.csv +$offdelim +$onlisting +/ ; + +parameter frac_h_ccseason_weights(allh,ccseason) "--unitless-- fraction of timeslice associated with each ccseason" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%frac_h_ccseason_weights.csv +$include inputs_case%ds%stress%stress_year%%ds%frac_h_ccseason_weights.csv +$offdelim +$onlisting +/ ; + +szn_quarter_weights(allszn,quarter) = 0 ; +szn_quarter_weights(szn,quarter) = + sum{h$h_szn(h,szn), frac_h_quarter_weights(h,quarter) } + / sum{h$h_szn(h,szn), 1} ; + +szn_ccseason_weights(allszn,ccseason) = 0 ; +szn_ccseason_weights(szn,ccseason) = + sum{h$h_szn(h,szn), frac_h_ccseason_weights(h,ccseason) } + / sum{h$h_szn(h,szn), 1} ; + +hours_daily(allh) = 0 ; +hours_daily(h_rep) = %GSw_HourlyChunkLengthRep% ; +hours_daily(h_stress) = %GSw_HourlyChunkLengthStress% ; + + +*=============================================== +* -- Climate Adjustments to Transmission -- +*=============================================== + +trans_cap_delta(allh,t) = 0 ; +trans_cap_delta(h,t) = + climate_heuristics_finalyear('trans_summer_cap_delta') * climate_heuristics_yearfrac(t) + * sum{quarter$sameas(quarter,"summ"), frac_h_quarter_weights(h,quarter) }; + + +*============================================= +* -- Mexico and Canada -- +*============================================= + +$ifthene.Canada %GSw_Canada% == 1 +parameter can_imports_szn_frac(allszn) "--unitless-- [Sw_Canada=1] fraction of annual imports that occur in each season" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%can_imports_szn_frac.csv +$offdelim +$onlisting +/ ; + +parameter can_exports_h_frac(allh) "--unitless-- [Sw_Canada=1] fraction of annual exports by timeslice" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%can_exports_h_frac.csv +$offdelim +$onlisting +/ ; + +can_imports_szn(r,allszn,t) = 0 ; +can_imports_szn(r,szn,t) = can_imports(r,t) * can_imports_szn_frac(szn) ; +can_exports_h(r,allh,t) = 0 ; +can_exports_h(r,h,t)$[hours(h)] = can_exports(r,t) * can_exports_h_frac(h) / hours(h) ; + +$endif.Canada + +* zero Canada exports when Canada is not modeled +can_imports_szn(r,szn,t)$[Sw_Canada=0] = 0 ; +can_exports_h(r,h,t)$[Sw_Canada=0] = 0 ; + +$onempty +parameter canmexload(r,allh) "load for canadian and mexican regions" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%canmexload.csv +$offdelim +$onlisting +/ ; +$offempty + + +*============================================= +* -- Air quality policies -- +*============================================= + +h_weight_csapr(allh) = 0 ; +h_weight_csapr(h) = + sum{quarter, frac_h_quarter_weights(h,quarter) * quarter_weight_csapr(quarter) } ; + + +*================================================== +* -- Availability (forced and scheduled outages) -- +*================================================== + +parameter outage_forced_h(i,r,allh) "--fraction-- forced outage rate" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%outage_forced_h.csv +$include inputs_case%ds%stress%stress_year%%ds%outage_forced_h.csv +$offdelim +$onlisting +/ ; + +* Infer some forced outage rates from parent techs +outage_forced_h(i,r,h)$pvb(i) = outage_forced_h("battery_li",r,h) ; +outage_forced_h(i,r,h)$geo(i) = outage_forced_h("geothermal",r,h) ; + +outage_forced_h(i,r,h)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), outage_forced_h(ii,r,h) } ; + +* Upgrade plants assume the same forced outage rate as what they're upgraded to +outage_forced_h(i,r,h)$upgrade(i) = sum{ii$upgrade_to(i,ii), outage_forced_h(ii,r,h) } ; + +parameter outage_scheduled_h(i,allh) "--fraction-- scheduled outage rate" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%outage_scheduled_h.csv +$include inputs_case%ds%stress%stress_year%%ds%outage_scheduled_h.csv +$offdelim +$onlisting +/ ; + +* Infer some scheduled outage rates from parent techs +outage_scheduled_h(i,h)$pvb(i) = outage_scheduled_h("battery_li",h) ; +outage_scheduled_h(i,h)$geo(i) = outage_scheduled_h("geothermal",h) ; + +outage_scheduled_h(i,h)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), outage_scheduled_h(ii,h) } ; + +* Upgrade plants assume the same scheduled outage rate as what they're upgraded to +outage_scheduled_h(i,h)$upgrade(i) = sum{ii$upgrade_to(i,ii), outage_scheduled_h(ii,h) } ; + +* Calculate availability (includes forced and scheduled outage rates) +avail(i,r,allh) = 0 ; +avail(i,r,h)$valcap_ir(i,r) = 1 ; + +avail(i,r,h)$[valcap_ir(i,r)] = (1 - outage_forced_h(i,r,h)) * (1 - outage_scheduled_h(i,h)) ; + + +*============================================= +* -- DR Shed -- +*============================================= + +* Written by hourly_writetimeseries.py +$onempty +parameter dr_shed_out(i,r,allh) "--fraction-- fraction of capacity available for DR shed resources" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%dr_shed_out.csv +$include inputs_case%ds%stress%stress_year%%ds%dr_shed_out.csv +$offdelim +$onlisting +/ ; +$offempty + +* DR Shed resources are only available during stress periods +avail(i,r,h)$[dr_shed(i)$h_rep(h)] = 0 ; +avail(i,r,h)$[dr_shed(i)$h_stress(h)] = dr_shed_out(i,r,h) ; + +*upgrade plants assume the same availability of what they are upgraded to +avail(i,r,h)$[upgrade(i)$valcap_i(i)] = sum{ii$upgrade_to(i,ii), avail(ii,r,h) } ; + +* In eq_reserve_margin, thermal outages are captured through the PRM rather than through +* forced/scheduled outages. If GSw_PRM_StressOutages is not true, +* set the availability of thermal generator to 1 during stress periods. +avail(i,r,h) + $[h_stress(h)$valcap_ir(i,r)$(Sw_PRM_StressOutages=0) + $(not vre(i))$(not hydro(i))$(not storage(i))$(not consume(i)) + ] = 1 ; + +* Geothermal is currently the only tech where derate_geo_vintage(i,v) != 1. +* If other techs with a non-unity vintage-dependent derate are added, avail(i,r,h) may need to be +* multiplied by derate_geo_vintage(i,v) in additional locations throughout the model. +* Divide by (1 - outage rate) (i.e. avail) since geothermal_availability is defined +* as the total product of derate_geo_vintage(i,v) * avail(i,r,h). +derate_geo_vintage(i,initv)$[geo(i)$valcap_iv(i,initv)] = + geothermal_availability + / (sum{(r,h)$valcap_ir(i,r), avail(i,r,h) * hours(h) } + / sum{(r,h)$valcap_ir(i,r), hours(h) }) ; + +seas_cap_frac_delta(i,v,r,allszn,t) = 0 ; +seas_cap_frac_delta(i,v,r,szn,t)$valcap(i,v,r,t) = + sum{quarter, szn_quarter_weights(szn,quarter) * quarter_cap_frac_delta(i,v,r,quarter,t) } ; + + +*============================================= +* -- Hydrogen -- +*============================================= + +* assign hydrogen demand by region and timeslice +* we assumed demand is flat, i.e., timeslices w/ more hours +* have more demand in metric tons but the same rate in metric tons/hour +h2_exogenous_demand_regional(r,p,allh,t) = 0 ; +h2_exogenous_demand_regional(r,p,h,t)$[tmodel_new(t)$h2_share(r,t)] + = h2_share(r,t) * h2_exogenous_demand(p,t) / 8760 ; + + +*============================================= +* -- Capacity factor -- +*============================================= + +* Written by cfgather.py, overwritten by hourly_writetimeseries.py +parameter cf_in(i,r,allh) "--fraction-- capacity factors for renewable technologies" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%cf_vre.csv +$include inputs_case%ds%stress%stress_year%%ds%cf_vre.csv +$offdelim +$onlisting +/ ; + +cf_in(i,r,h)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), cf_in(ii,r,h) } ; + +*initial assignment of capacity factors +cf_rsc(i,v,r,allh,t) = 0 ; +cf_rsc(i,v,r,h,t)$[cf_in(i,r,h)$cf_tech(i)$valcap(i,v,r,t)] = cf_in(i,r,h) ; + +* Written by reeds/inputs/hourly_writetimeseries.py +$onempty +parameter cf_hyd(i,allszn,r,allt) "--fraction-- hydro capacity factors by season and year" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%cf_hyd.csv +$include inputs_case%ds%stress%stress_year%%ds%cf_hyd.csv +$offdelim +$onlisting +/ ; +$offempty + +$ifthen.climatehydro %GSw_ClimateHydro% == 1 + +* Written by climateprep.py +table climate_hydro_seasonal(r,allszn,allt) "annual/seasonal nondispatchable hydropower availability" +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%climate_hydadjsea.csv +$offdelim +$onlisting +; + +* adjust cf_hyd based on annual/seasonal climate multipliers +* non-dispatchable hydro gets new seasonal profiles as well as annually-varying CF +* dispatchable hydro keeps the original seasonal profiles; only annual CF changes. Reflects the assumption +* that reservoirs will be utilized in the same seasonal pattern even if seasonal inflows change. +cf_hyd(i,szn,r,t)$[hydro_nd(i)$(yeart(t)>=Sw_ClimateStartYear)] = + sum{allt$att(allt,t), cf_hyd(i,szn,r,t) * climate_hydro_seasonal(r,szn,allt) } ; + +cf_hyd(i,szn,r,t)$[hydro_d(i)$(yeart(t)>=Sw_ClimateStartYear)] = + sum{allt$att(allt,t), cf_hyd(i,szn,r,t) * climate_hydro_annual(r,allt) } ; + +$endif.climatehydro + +*created by /reeds/inputs/writecapdat.py +parameter cap_hyd_szn_adj(i,allszn,r) "--fraction-- seasonal max capacity adjustment for dispatchable hydro" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%cap_hyd_szn_adj.csv +$include inputs_case%ds%stress%stress_year%%ds%cap_hyd_szn_adj.csv +$offdelim +$onlisting +/ ; + +*Upgraded hydro parameters: +* By default, capacity factors for upgraded hydro techs use what we upgraded from. +cf_hyd(i,szn,r,t)$[upgrade(i)$(hydro(i) or psh(i))] = + sum{ii$upgrade_from(i,ii), cf_hyd(ii,szn,r,t) } ; + +* dispatchable hydro has a separate constraint for seasonal generation which uses m_cf_szn +cf_rsc(i,v,r,h,t)$[hydro(i)$valcap(i,v,r,t)] = sum{szn$h_szn(h,szn), cf_hyd(i,szn,r,t) } ; + +cf_rsc(i,v,r,h,t)$[rsc_i(i)$(sum{tt, capacity_exog(i,v,r,tt) })] = + cf_rsc(i,"init-1",r,h,t) ; + +* For cap_hyd_szn_adj, which only applies to dispatchable hydro or upgraded disp hydro with added pumping, we first try using the from-tech, but if that is +* not available we use to to-tech, and if not that either we just use 1. +cap_hyd_szn_adj(i,szn,r)$[upgrade(i)$(hydro_d(i) or psh(i))] = + sum{ii$upgrade_from(i,ii), cap_hyd_szn_adj(ii,szn,r) } ; +cap_hyd_szn_adj(i,szn,r)$[upgrade(i)$hydro_d(i)$(not cap_hyd_szn_adj(i,szn,r))] = + sum{ii$upgrade_to(i,ii), cap_hyd_szn_adj(ii,szn,r) } ; +cap_hyd_szn_adj(i,szn,r)$[upgrade(i)$hydro_d(i)$(not cap_hyd_szn_adj(i,szn,r))] = 1 ; + + +* do not apply "avail" for hybrid PV+battery because "avail" represents the battery availability +m_cf(i,v,r,allh,t) = 0 ; +m_cf(i,v,r,h,t)$[cf_tech(i)$valcap(i,v,r,t)$cf_rsc(i,v,r,h,t)$cf_adj_t(i,v,t)] = + cf_rsc(i,v,r,h,t) + * cf_adj_t(i,v,t) + * (avail(i,r,h)$[not pvb(i) and not hydro(i)] + 1$(pvb(i) or hydro(i)) ); + +* can remove capacity factors for new vintages that have not been introduced yet +m_cf(i,newv,r,h,t)$[not sum{tt$(yeart(tt) <= yeart(t)), ivt(i,newv,tt ) }$valcap(i,newv,r,t)$m_cf(i,newv,r,h,t)] = 0 ; + +* distpv capacity factor is divided by (1.0 - distloss) to provide a busbar equivalent capacity factor +m_cf(i,v,r,h,t)$[distpv(i)$valcap(i,v,r,t)] = m_cf(i,v,r,h,t) / (1.0 - distloss) ; + +* doing this before calculating m_cf_szn to make sure +* m_cf_szn does not get populated with very small values +m_cf(i,v,r,h,t)$[not valcap(i,v,r,t)] = 0 ; +m_cf(i,v,r,h,t)$[(m_cf(i,v,r,h,t)<0.01)$valcap(i,v,r,t)] = 0 ; +m_cf(i,v,r,h,t)$[cf_tech(i)$valcap(i,v,r,t)$m_cf(i,v,r,h,t)] = round(m_cf(i,v,r,h,t),3) ; + +* Remove capacity when there is no corresponding capacity factor +m_capacity_exog(i,v,r,t)$[initv(v)$cf_tech(i)$(not sum{h, m_cf(i,v,r,h,t) })] = 0 ; + +* Average CF by season +m_cf_szn(i,v,r,allszn,t) = 0 ; +m_cf_szn(i,v,r,szn,t)$[cf_tech(i)$valcap(i,v,r,t)$(hydro_d(i) or hyd_add_pump(i))] = + sum{h$h_szn(h,szn), hours(h) * m_cf(i,v,r,h,t) } + / sum{h$h_szn(h,szn), hours(h) } ; + +* adding upgrade techs for hydro +m_cf_szn(i,v,r,szn,t) + $[cf_tech(i)$upgrade(i)$valcap(i,v,r,t)$(hydro_d(i) or psh(i))] + = sum{ii$upgrade_from(i,ii), m_cf_szn(ii,v,r,szn,t) } ; + +m_cf_szn(i,v,r,szn,t) + $[cf_tech(i)$upgrade(i)$valcap(i,v,r,t)$hydro_d(i)$(not m_cf_szn(i,v,r,szn,t))] + = sum{ii$upgrade_to(i,ii), m_cf_szn(ii,v,r,szn,t) } ; + +m_cf_szn(i,v,r,szn,t) + $[cf_tech(i)$upgrade(i)$valcap(i,v,r,t)$hydro_d(i)$(not m_cf_szn(i,v,r,szn,t))] + = 1 ; + +* Calculate daytime hours (for PVB) based on hours with nonzero PV CF +dayhours(allh) = 0 ; +dayhours(h)$[sum{(i,v,r,t)$[pv(i)$valgen(i,v,r,t)], m_cf(i,v,r,h,t) }] = yes ; + + +*===================================================================================== +* -- Cooling Water Initialization, Seasonal Distribution, & Climate Adjustment -- +*===================================================================================== + +* Initialize water capacity based on water requirements of existing fleet in base year. +* We conservatively assume plants have enough water available to operate up to a +* 100% capacity factor, or to operate at full capacity at any time of the year. +if(%cur_year% = sum{t$tfirst(t), yeart(t) }, + wat_supply_init(wst,r) = sum{(i,v,h,t)$[h_rep(h)$valcap(i,v,r,t)$initv(v)$i_wst(i,wst)$tfirst(t)], + hours(h) + * (sum{w$i_w(i,w), m_capacity_exog(i,v,r,t) * water_rate(i,w)}) + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + } / 1E6 ; + + m_watsc_dat(wst,"cap",r,t)$tmodel_new(t) = wat_supply_new(wst,"cap",r) + wat_supply_init(wst,r) ; + +* --- Climate Impacts: Cooling Water Capacity --- +$ifthen.climatewater %GSw_ClimateWater% == 1 +* Update water supply curve with annually-varying water supply. Multiplier is applied to (wat_supply_new + wat_supply_init). +* NOTE: Only the capacity changes, not the cost + m_watsc_dat(wst,"cap",r,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) + ] $= sum{allt$att(allt,t), + m_watsc_dat(wst,"cap",r,t) * wat_supply_climate(wst,r,allt) } ; +* If wst is in wst_climate but does not have data in input file, assign its multiplier to the fsu multiplier + m_watsc_dat(wst,"cap",r,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) + $sum{allt$att(allt,t), (not wat_supply_climate(wst,r,allt)) } + ] $= sum{allt$att(allt,t), + m_watsc_dat(wst,"cap",r,t) * wat_supply_climate('fsu',r,allt) } ; +$endif.climatewater +) ; + +* Initialize seasonal distribution factors for new unappropriated water access +watsa(wst,r,allszn,t) = 0 ; +watsa(wst,r,szn,t)$[tmodel_new(t)$Sw_WaterMain] = + sum{quarter, + szn_quarter_weights(szn,quarter) * watsa_temp(wst,r,quarter) } ; + +* update seasonal distribution factors for water sources other than fresh surface unappropriated +* and also fsu with missing data +watsa(wst,r,szn,t)$[(not sum{sznn, watsa(wst,r,sznn,t)})$tmodel_new(t)$Sw_WaterMain] = + round(numdays(szn)/365 , 4) ; + +* --- Climate Impacts: Cooling Water Seasonal Distribution --- +$ifthen.climatewater %GSw_ClimateWater% == 1 +* Update seasonal distribution factors for fsu; other water types are unchanged +* declared over allt to allow for external data files that extend beyond end_year +* Written by climateprep.py +table watsa_climate(wst,r,allszn,allt) "time-varying fractional seasonal allocation of water" +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%climate_UnappWaterSeaAnnDistr.csv +$offdelim +$onlisting +; +* Use the sparse assignment $= to make sure we don't assign zero to wst's not included in watsa_climate +watsa(wst,r,szn,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) + $Sw_WaterMain + ] $= sum{allt$att(allt,t), watsa_climate(wst,r,szn,allt) }; +* If wst is in wst_climate but does not have data in input file, assign its multiplier to the fsu multiplier +watsa(wst,r,szn,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) + $sum{allt$att(allt,t), (not watsa_climate(wst,r,szn,allt)) } + $Sw_WaterMain + ] $= sum{allt$att(allt,t), watsa_climate('fsu',r,szn,allt) }; +$endif.climatewater + + +*============================================= +* -- Operating reserves and minloading -- +*============================================= + +$onempty +set opres_periods(allszn) "Periods within which the operating reserve constraint applies" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%opres_periods.csv +$offdelim +$onlisting +/ ; +$offempty + +opres_h(allh) = 0 ; +opres_h(h) = sum{szn$opres_periods(szn), h_szn(h,szn) } ; + + +set hour_szn_group(allh,allhh) "h and hh in the same season - used in minloading constraint" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%hour_szn_group.csv +$offdelim +$onlisting +/ ; + +*reducing problem size by removing h-hh combos that are the same +hour_szn_group(h,hh)$sameas(h,hh) = no ; + +hydmin(i,r,allszn) = 0 ; +hydmin(i,r,szn) = round(sum{quarter, szn_quarter_weights(szn,quarter) * hydmin_quarter(i,r,quarter) }, 3) ; + +minloadfrac(r,i,allh) = 0 ; +minloadfrac(r,i,h) = minloadfrac0(i) ; + +* adjust nuclear mingen to minloadfrac_nuclear_flex if running with flexible nuclear +minloadfrac(r,i,h)$[nuclear(i)$Sw_NukeFlex] = minloadfrac_nuclear_flex ; +* CSP and coal use user inputs +minloadfrac(r,i,h)$csp(i) = minloadfrac_csp ; +minloadfrac(r,i,h)$[coal(i)$(not minloadfrac(r,i,h))] = minloadfrac_coal ; +*set seasonal values for minloadfrac for hydro techs +minloadfrac(r,i,h)$[sum{szn$h_szn(h,szn), hydmin(i,r,szn ) }] = + sum{szn$h_szn(h,szn), hydmin(i,r,szn) } ; +*water tech assignment +minloadfrac(r,i,h)$[i_water_cooling(i)$Sw_WaterMain] = + sum{ii$ctt_i_ii(i,ii), minloadfrac(r,ii,h) } ; +*upgrade techs get their corresponding upgraded-to minloadfracs +minloadfrac(r,i,h)$upgrade(i) = sum{ii$upgrade_to(i,ii), minloadfrac(r,ii,h) } ; +*remove minloadfrac for non-viable generators +minloadfrac(r,i,h)$[not sum{(v,t), valcap(i,v,r,t) }] = 0 ; + +*reduced set of minloading constraints and mingen contributors +minloadfrac(r,i,h)$[(Sw_MinLoadTechs=0)] = 0 ; +minloadfrac(r,i,h)$[(Sw_MinLoadTechs=2)$(geo(i) or csp(i) or lfill(i))] = 0 ; +minloadfrac(r,i,h)$[(Sw_MinLoadTechs=3)$(not nuclear(i))$(not hydro(i))] = 0 ; +minloadfrac(r,i,h)$[(Sw_MinLoadTechs=4)$(not boiler(i))$(not hydro(i))] = 0 ; + + +*============================================= +* -- Electricity demand -- +*============================================= + +* Flexible demand +$onempty +parameter flex_frac_load(flex_type,r,allh,allt) +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%flex_frac_all.csv +$offdelim +$onlisting +/ ; + + +* EV adoptable managed charging +parameter evmc_baseline_load(r,allh,allt) "--fraction-- how much adopted shaped EV load is allowed to be shed in each timeslice h" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%evmc_baseline_load.csv +$offdelim +$onlisting +/ ; + +parameter evmc_shape_gen(i,r,allh) "--fraction-- how much adopted shaped EV load is allowed to be shed in each timeslice h" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%evmc_shape_generation.csv +$offdelim +$onlisting +/ ; + +parameter evmc_shape_load(i,r,allh) "--fraction-- how much adopted shaped EV load is added in each timeslice h" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%evmc_shape_load.csv +$offdelim +$onlisting +/ ; + +parameter evmc_storage_discharge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be discharged (deferred charging) in each timeslice h" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%evmc_storage_discharge.csv +$offdelim +$onlisting +/ ; + +parameter evmc_storage_charge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be charged (add back deferred charging) in each timeslice h" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%evmc_storage_charge.csv +$offdelim +$onlisting +/ ; + +parameter evmc_storage_energy_hours(i,r,allh,allt) "--hours-- Allowable EV storage SOC (quantity deferred EV charge) [MWh] divided by nameplate EVMC discharge capacity [MW]" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%evmc_storage_energy.csv +$offdelim +$onlisting +/ ; +$offempty + +* Written by hourly_writetimeseries.py +parameter load_allyear(r,allh,allt) "--MW-- end-use load by region, timeslice, and year" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%load_allyear.csv +$include inputs_case%ds%stress%stress_year%%ds%load_allyear.csv +$offdelim +$onlisting +/ ; +* Dividing by (1-distloss) converts end-use load to busbar load +load_exog(r,allh,t) = 0 ; +load_exog(r,h,t) = load_allyear(r,h,t) / (1.0 - distloss) ; + +parameter prm_year(r) "--fraction-- planning reserve margin for the current solve year" +/ +$offlisting +$ondelim +$include inputs_case%ds%stress%stress_year%%ds%prm.csv +$offdelim +$onlisting +/ ; +prm(r,t)$tmodel(t) = prm_year(r) ; + +* Stress-period load is scaled up by PRM +load_exog(r,h,t)$h_stress(h) = load_exog(r,h,t) * (1 + prm(r,t)) ; + +* first define mexican growth load then replace canadian with +* province-specific growth factors +load_exog(r,h,t)$canmexload(r,h) = mex_growth_rate(t) * canmexload(r,h) ; + +load_exog(r,h,t)$sum{st$r_st(r,st),can_growth_rate(st,t) } = + canmexload(r,h) * sum{st$r_st(r,st),can_growth_rate(st,t) } ; + +* Flexible load doesn't yet work with hourly resolution +flex_h_corr1(flex_type,allh,allh) = no ; +flex_h_corr2(flex_type,allh,allh) = no ; + +* assign zero values to avoid unassigned parameter errors +flex_demand_frac(flex_type,r,allh,t) = 0 ; +flex_demand_frac(flex_type,r,h,t)$Sw_EFS_Flex = flex_frac_load(flex_type,r,h,t) ; + +*initial values are set here (after SwI_Load has been accounted for) +load_exog0(r,allh,t) = 0 ; +load_exog0(r,h,t) = load_exog(r,h,t) ; + + +load_exog_flex(flex_type,r,allh,t) = 0 ; +load_exog_flex(flex_type,r,h,t) = load_exog(r,h,t) * flex_demand_frac(flex_type,r,h,t) ; +load_exog_static(r,allh,t) = 0 ; +load_exog_static(r,h,t) = load_exog(r,h,t) - sum{flex_type, load_exog_flex(flex_type,r,h,t) } ; + + + +set maxload_szn(r,allh,t,allszn) "hour with highest load within each szn" ; +maxload_szn(r,allh,t,allszn) = 0 ; +maxload_szn(r,h,t,szn) + $[(smax(hh$[h_szn(hh,szn)], load_exog_static(r,hh,t)) + = load_exog_static(r,h,t)) + $h_szn(h,szn)$Sw_OpRes] = yes ; + +set h_ccseason_prm(allh,ccseason) "peak-load hour for the entire modeled system by ccseason" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%h_ccseason_prm.csv +$offdelim +$onlisting +/ ; + +peak_static_frac(r,ccseason,t) = 1 - sum{(flex_type,h)$h_ccseason_prm(h,ccseason), flex_demand_frac(flex_type,r,h,t) } ; + + + +* Written by hourly_writetimeseries.py +parameter peak_ccseason(r,ccseason,allt) "--MW-- end-use peak demand by region, season, year" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%peak_ccseason.csv +$offdelim +$onlisting +/ ; +*Dividing by (1-distloss) converts end-use load to busbar load +peakdem_static_ccseason(r,ccseason,t) = peak_ccseason(r,ccseason,t) * peak_static_frac(r,ccseason,t) / (1.0 - distloss) ; + + +$onempty +parameter peak_h(r,allh,allt) "--MW-- busbar peak demand by timeslice" +/ +$offlisting +$ondelim +$include inputs_case%ds%%temporal_inputs%%ds%peak_h.csv +$offdelim +$onlisting +/ ; +$offempty + +peakdem_static_h(r,allh,t) = 0 ; +peakdem_static_h(r,h,t) = peak_h(r,h,t) * (1 - sum{flex_type, flex_demand_frac(flex_type,r,h,t) }) / (1.0 - distloss) ; + + +*============================================= +* -- Fossil gas supply curve -- +*============================================= + +gasadder_cd(cendiv,t,allh) = 0 ; +gasadder_cd(cendiv,t,h) = (gasprice_ref(cendiv,t) - gasprice_nat(t))/2 ; + +*winter gas gets marked up +gasadder_cd(cendiv,t,h) = + gasadder_cd(cendiv,t,h) + + gasprice_ref_frac_adder * frac_h_quarter_weights(h,"wint") * gasprice_ref(cendiv,t) ; + +szn_adj_gas(allh) = 0 ; +szn_adj_gas(h) = 1 ; +szn_adj_gas(h)$frac_h_quarter_weights(h,"wint") = + szn_adj_gas(h) + frac_h_quarter_weights(h,"wint") * szn_adj_gas_winter ; + + +*============================================= +* -- Round parameters for GAMS -- +*============================================= + +avail(i,r,h)$avail(i,r,h) = round(avail(i,r,h),3) ; +can_imports_szn(r,szn,t)$can_imports_szn(r,szn,t) = round(can_imports_szn(r,szn,t),3) ; +can_exports_h(r,h,t)$can_exports_h(r,h,t) = round(can_exports_h(r,h,t),3) ; +h_weight_csapr(h)$h_weight_csapr(h) = round(h_weight_csapr(h),3) ; +load_exog(r,h,t)$load_exog(r,h,t) = round(load_exog(r,h,t),3) ; +load_exog_static(r,h,t)$load_exog_static(r,h,t) = round(load_exog_static(r,h,t),3) ; +minloadfrac(r,i,h)$minloadfrac(r,i,h) = round(minloadfrac(r,i,h),3) ; +szn_adj_gas(h)$szn_adj_gas(h) = round(szn_adj_gas(h), 3) ; +cap_hyd_szn_adj(i,szn,r)$cap_hyd_szn_adj(i,szn,r) = round(cap_hyd_szn_adj(i,szn,r),3) ; +peakdem_static_ccseason(r,ccseason,t)$peakdem_static_ccseason(r,ccseason,t) = round(peakdem_static_ccseason(r,ccseason,t),2) ; +seas_cap_frac_delta(i,v,r,szn,t)$seas_cap_frac_delta(i,v,r,szn,t) = round(seas_cap_frac_delta(i,v,r,szn,t),3) ; + + +* Write the inputs for debugging purposes +$ifthene.write %cur_year% == %startyear% +execute_unload 'inputs_case%ds%inputs.gdx' ; +$endif.write diff --git a/reeds/core/solve/3_solve_allyears.gms b/reeds/core/solve/3_solve_allyears.gms new file mode 100644 index 00000000..204d1bc5 --- /dev/null +++ b/reeds/core/solve/3_solve_allyears.gms @@ -0,0 +1,165 @@ +* global needed for this file: +* case : name of case you're running +* niter : current iteration + +$setglobal ds %ds% + +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +$log 'Running intertemporal solve for...' +$log ' case == %case%' +$log ' iteration == %niter%' + +*remove any load years +tload(t) = no ; + +$if not set niter $setglobal niter 0 +$eval previter %niter%-1 + +*if this isn't the first iteration +$ifthene.notfirstiter %niter%>0 + +$if not set load_ref_dem $setglobal load_ref_dem 0 + +$ifthene.loadref %load_ref_dem% == 0 +* need to load psupply0 and load_exog0... +* should also set load_exog to load_exog0 + + +$endif.loadref + +*============================ +* --- CC and Curtailment --- +*============================ + +*indicate we're loading data +tload(t)$tmodel(t) = yes ; + +$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_merged_%niter%.gdx +$loaddcr cc_old_load2 = cc_old +$loaddcr cc_mar_load2 = cc_mar +$loaddcr cc_evmc_load2 = cc_evmc +$loaddcr sdbin_size_load2 = sdbin_size +$gdxin + +cc_old_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_old_load2(loadset,i,r,ccreg,szn,t) } } ; +cc_mar_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_mar_load2(loadset,i,r,ccreg,szn,t) } } ; + +cc_evmc_load(i,r,szn,t) = sum{loadset, cc_evmc_load2(loadset,i,r,szn,t) } ; + +sdbin_size_load(ccreg,szn,sdbin,t) = sum{loadset, sdbin_size_load2(loadset,ccreg,szn,sdbin,t) } ; + +*=============================== +* --- Begin Capacity Credit --- +*=============================== + +*Clear params before calculation +cc_int(i,v,r,szn,t) = 0 ; +cc_totmarg(i,r,szn,t) = 0 ; +cc_excess(i,r,szn,t) = 0 ; +cc_scale(i,r,szn,t) = 0 ; +sdbin_size(ccreg,szn,sdbin,t) = 0 ; + +*Storage duration bin sizes by year +sdbin_size(ccreg,szn,sdbin,t)$tload(t) = sdbin_size_load(ccreg,szn,sdbin,t) ; + +*Sw_Int_CC=0 means use average capacity credit for each tech, and don't differentiate vintages +*If there is no existing capacity to calculate average, use marginal capacity credit instead. +if(Sw_Int_CC=0, + cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)$sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) }] = + cc_old_load(i,r,szn,t) / sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) } ; + cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)$(cc_old_load(i,r,szn,t)=0)] = m_cc_mar(i,r,szn,t) ; +) ; + +*For the remaining options we initially use marginal values for cc_int, differentiated by vintage based on seasonal capacity factors. +if(Sw_Int_CC=1 or Sw_Int_CC=2, + cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)$sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) }] = + m_cc_mar(i,r,szn,t) * m_cf_szn(i,v,r,szn,t) / sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) } ; + cc_totmarg(i,r,szn,t)$[tload(t)$vre(i)] = sum{v$valcap(i,v,r,t), cc_int(i,v,r,szn,t) * CAP.l(i,v,r,t) } ; +) ; + +*Sw_Int_CC=1 means use average capacity credit for each tech, but differentiate based on vintage. +*Start with marginal capacity credit with seasonal vintage-based capacity factor adjustment, +*and scale with cc_old_load to result in the correct total capacity credit. +if(Sw_Int_CC=1, + cc_scale(i,r,szn,t)$[tload(t)$vre(i)] = 1 ; + cc_scale(i,r,szn,t)$[tload(t)$vre(i)$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) / cc_totmarg(i,r,szn,t) ; + cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)] = cc_int(i,v,r,szn,t) * cc_scale(i,r,szn,t) ; +) ; + +*Sw_Int_CC=2 means use marginal capacity credit, adjusted by seasonal capacity factors by vintage +if(Sw_Int_CC=2, + cc_excess(i,r,szn,t)$[tload(t)$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) - cc_totmarg(i,r,szn,t) ; +) ; + + +*no longer want m_cc_mar since it should not enter the planning reserve margin constraint +m_cc_mar(i,r,szn,t) = 0 ; + +cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) > 1] = 1 ; +cc_int(i,v,r,szn,t)$[tload(t)$csp_storage(i)$valcap(i,v,r,t)] = 1 ; + +*======================================= +* --- Begin Averaging of CC/Curt --- +*======================================= + +$ifthene.afterseconditer %niter%>1 + +*when set to 1 - it will take the average over all previous iterations +if(Sw_AVG_iter=1, + cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)] = + (cc_int(i,v,r,szn,t) + cc_iter(i,v,r,szn,t,"%previter%")) / 2 ; + ) ; + +$endif.afterseconditer + +*Remove very small numbers to make it easier for the solver +cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) < 0.001] = 0 ; + +cc_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) ; + +execute_unload 'ReEDS_Augur%ds%augur_data%ds%curtout_%case%_%niter%.gdx' cc_int ; + +*following line will load in the level values if the switch is enabled +*note that this is still within the conditional that we are now past the first iteration +*and thus a loadpoint is enabled +if(Sw_Loadpoint = 1, +execute_loadpoint 'gdxfiles%ds%%case%_load.gdx' ; +ReEDSmodel.optfile = 8 ; +) ; + +$endif.notfirstiter + + +* rounding of all cc and curt parameters +* used in the intertemporal case + +cc_int(i,v,r,szn,t) = round(cc_int(i,v,r,szn,t), 4) ; +cc_totmarg(i,r,szn,t) = round(cc_totmarg(i,r,szn,t), 4) ; +cc_excess(i,r,szn,t) = round(cc_excess(i,r,szn,t), 4) ; +cc_scale(i,r,szn,t) = round(cc_scale(i,r,szn,t), 4) ; + + +*============================== +* --- Solve Supply Side --- +*============================== + +solve ReEDSmodel using lp minimizing z ; + +if(Sw_Loadpoint = 1, +execute_unload 'gdxfiles%ds%%case%_load.gdx' ; +) ; + +*============================ +* --- Iteration Tracking --- +*============================ + +cap_iter(i,v,r,t,"%niter%")$valcap(i,v,r,t) = CAP.l(i,v,r,t) ; +cap_energy_iter(i,v,r,t,"%niter%")$valcap(i,v,r,t) = CAP_ENERGY.l(i,v,r,t) ; +gen_iter(i,v,r,t,"%niter%")$valcap(i,v,r,t) = sum{h, GEN.l(i,v,r,h,t) * hours(h) } ; +gen_iter(i,v,r,t,"%niter%")$[vre(i)$valcap(i,v,r,t)] = sum{h, m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; +cap_firm_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) * CAP.l(i,v,r,t) ; +cap_firm_iter(i,v,r,szn,t,"%niter%")$storage(i) = sum{sdbin, CAP_SDBIN.l(i,v,r,szn,sdbin,t) * cc_storage(i,sdbin) } ; +cap_energy_firm_iter(i,v,r,szn,t,"%niter%")$storage(i) = sum{sdbin, CAP_SDBIN_ENERGY.l(i,v,r,szn,sdbin,t) } ; \ No newline at end of file diff --git a/reeds/core/solve/3_solve_oneyear.gms b/reeds/core/solve/3_solve_oneyear.gms new file mode 100644 index 00000000..3a4ff1cc --- /dev/null +++ b/reeds/core/solve/3_solve_oneyear.gms @@ -0,0 +1,290 @@ +* Includes these scripts, in order: +* - 2_temporal_params.gms +* - 2_financials.gms +* - * solves the model * +* - 4_post_solve_adjustments.gms +* - 5_varfix.gms +* - 6_data_dump.gms + +$setglobal ds \ + +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +* globals needed for this file: +* case : name of case you're running +* cur_year : current year + +*remove any load years +tload(t) = no ; + +* --- reset tmodel --- +tmodel(t) = no ; +tmodel("%cur_year%") = yes ; + +$log 'Solving sequential case for...' +$log ' Case: %case%' +$log ' Year: %cur_year%' + + +*** Define the h- and szn-dependent parameters +$onMultiR +$include reeds%ds%core%ds%solve%ds%2_temporal_params.gms +$offMulti + + +* need to have values initialized before making adjustments +* thus cannot perform these adjustments until 2010 has solved +$ifthene.post_startyear %cur_year%>%startyear% +* Here we calculate the RHS value of eq_rsc_INVlim because floating point +* differences can cause small number issues that either make the model +* infeasible or result in very tiny number (order 1e-16) in the matrix +rhs_eq_rsc_INVlim(r,i,rscbin,t)$[tmodel(t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i)] = + +*capacity indicated by the resource supply curve (with undiscovered geo available +*at the "discovered" amount and hydro upgrade availability adjusted over time) + m_rsc_dat(r,i,rscbin,"cap") * ( + 1$[not geo_hydro(i)] + geo_discovery(i,r,t)$geo_hydro(i)) + + hyd_add_upg_cap(r,i,rscbin,t)$(Sw_HydroCapEnerUpgradeType=1) +*minus the cumulative invested capacity in that region/class/bin... +*Note that yeart(tt) is stricly < here, while it is <= in eq_rsc_INVlim. That is because +*values where yeart(tt)==yeart(t) are variables rather than parameters because they are not +*values from prior solve years. + - sum{(ii,v,tt)$[valinv(ii,v,r,tt)$(yeart(tt) < yeart(t))$rsc_agg(i,ii)], + INV_RSC.l(ii,v,r,rscbin,tt) * resourcescaler(ii) } +*minus exogenous (pre-start-year) capacity, using its level in the first year (tfirst) + - sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], + capacity_exog_rsc(ii,v,r,rscbin,tt) } +; + + +flag_eq_rsc_INVlim(r,i,rscbin,t)$tmodel(t) = no ; + +* Identify instances when the RHS values are within rhs_tolerance of zero +flag_eq_rsc_INVlim(r,i,rscbin,t)$[tmodel(t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i) + $(rhs_eq_rsc_INVlim(r,i,rscbin,t) > -rhs_tolerance) + $(rhs_eq_rsc_INVlim(r,i,rscbin,t) < rhs_tolerance)] = yes ; + +* When RHS is 0 (or close enough), the eq_rsc_INVlim equation says that all relevant INV_RSC are 0. +* Therefore we can set the INV_RSC variable to zero anywhere the flag_eq_rsc_INVlim is true +loop(i$rsc_i(i), + INV_RSC.fx(ii,v,r,rscbin,t)$[tmodel(t)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i) + $(flag_eq_rsc_INVlim(r,i,rscbin,t))$(valinv(ii,v,r,t)$rsc_agg(i,ii))] = 0 ; +) ; + +* set m_capacity_exog to the maximum of either its original amount +* or the amount of upgraded capacity that has occurred in the past "Sw_UpgradeLifespan" years +* to avoid forcing recently upgraded capacity into retirement +if(Sw_Upgrades = 1, + + m_capacity_exog(i,v,r,t)$[valcap(i,v,r,t)$sameas(t,"%cur_year%") + $(sum{(ii,tt)$[(tt.val <= t.val)$(t.val - tt.val <= Sw_UpgradeLifespan) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], UPGRADES.l(ii,v,r,tt) } ) ] = +* [maximum of] initial capacity recorded in d_solveprep + max( m_capacity_exog0(i,v,r,t), +* -or- capacity of upgrades that have occurred from this i v r t combination + sum{(ii,tt)$[(tt.val <= t.val)$(t.val - tt.val <= Sw_UpgradeLifespan) + $valcap(ii,v,r,tt)$upgrade_from(ii,i)], + UPGRADES.l(ii,v,r,tt) } + ) ; + +) ; + +* if the relative growth constraint is turned on, then calculate the growth +* limits for each growth bin +if(Sw_GrowthPenalties > 0, + +* Calculate the maximum deployment that could have been achieved in the last modeled +* year. For example, if tmodel is 2023 and the prior two solve years were 2020 and +* 2015, then we are calculating the maximum deployment that could have occured in +* 2020 at the growth rate specified in gbin1. This requires looking back to tprev +* and the solve year before tprev, hence the need for the yeart(ttt). +* The denominator is simply a discount term, and the multiplication is an associated +* compounding term. + last_year_max_growth(st,tg,t)$tmodel(t) = + sum{(i,v,r,tt)$[valinv(i,v,r,tt)$r_st(r,st)$tg_i(tg,i)$tprev(t,tt)], + INV.l(i,v,r,tt) } + / sum{allt$[(allt.val>sum{tt$tprev(t,tt), sum{ttt$tprev(tt,ttt), yeart(ttt) } }) + $(allt.val<=sum{tt$tprev(t,tt), yeart(tt) })], + (growth_bin_size_mult("gbin1") ** (allt.val - sum{tt$tprev(t,tt), sum{ttt$tprev(tt,ttt), yeart(ttt) } } - 1)) } + * (growth_bin_size_mult("gbin1") ** ((sum{tt$tprev(t,tt), yeart(tt) - sum{ttt$tprev(tt,ttt), yeart(ttt) } }) - 1)) ; + +* Now calculate the growth bin size for the current solve year, assuming that the +* maximum growth allowed in gbin1 happens each year over the current solve period. + growth_bin_limit("gbin1",st,tg,t)$tmodel(t) = + sum{allt$[(allt.val>sum{tt$tprev(t,tt), yeart(tt) }) + $(allt.val<=yeart(t))], + last_year_max_growth(st,tg,t) * growth_bin_size_mult("gbin1") ** (allt.val - sum{tt$tprev(t,tt), yeart(tt) }) } + / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; + +* Do not allow growth_bin_limit to decline over time (i.e., if a higher growth +* rate was achieved in the past, allow the model to start from that higher level) + growth_bin_limit("gbin1",st,tg,t)$tmodel(t) = smax{tt, growth_bin_limit("gbin1",st,tg,tt) } ; + +* If the calculated gbin1 value is less than the minimum bin size, then set it to the minimum bin size + growth_bin_limit("gbin1",st,tg,t)$[tmodel(t)$(growth_bin_limit("gbin1",st,tg,t) < gbin_min(tg))$stfeas(st)] = gbin_min(tg) ; + +* Now set the size of the remaining bins + growth_bin_limit(gbin,st,tg,t)$[tmodel(t)$(not sameas(gbin,"gbin1"))] = + growth_bin_limit("gbin1",st,tg,t) * (growth_bin_size_mult(gbin) - growth_bin_size_mult("gbin1")) ; + + growth_bin_limit(gbin,st,tg,t)$growth_bin_limit(gbin,st,tg,t) = round(growth_bin_limit(gbin,st,tg,t),0) ; + +) ; + +$endif.post_startyear + +* Load capacity credit results +$ifthene.tcheck %cur_year%>%GSw_SkipAugurYear% + +*indicate we're loading data +tload("%cur_year%") = yes ; + +*file written by ReEDS_Augur.py +* loaddcr = domain check (dc) + overwrite values storage previously (r) +$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_%prev_year%.gdx +$loaddcr cc_old_load = cc_old +$loaddcr cc_mar_load = cc_mar +$loaddcr cc_evmc_load = cc_evmc +$loaddcr sdbin_size_load = sdbin_size +$gdxin + +*Note: these values are rounded before they are written to the gdx file, so no need to round them here + +* assign old and marginal capacity credit parameters to those +* corresponding to each balancing areas cc region +cc_old(i,r,ccseason,t)$[tload(t)$(vre(i) or csp(i) or pvb(i))] = + sum{ccreg$r_ccreg(r,ccreg), cc_old_load(i,r,ccreg,ccseason,t) } ; + +m_cc_mar(i,r,ccseason,t)$[tload(t)$(vre(i) or csp(i) or pvb(i))] = + sum{ccreg$r_ccreg(r,ccreg), cc_mar_load(i,r,ccreg,ccseason,t) } ; + +sdbin_size(ccreg,ccseason,sdbin,t)$tload(t) = sdbin_size_load(ccreg,ccseason,sdbin,t) ; + +* --- Assign hybrid PV+battery capacity credit --- +* Limit the capacity credit of hybrid PV such that the total capacity credit from the +* PV and the battery do not exceed the inverter limit. +* Example: * PV = 130 MWdc, Battery = 65 MW, Inverter = 100 MW (PVdc/Battery=0.5; PVdc/INVac=1.3) +* Assuming the capacity credit of the Battery is 65 MW, then capacity credit of the PV +* is limited to 35 MW or 0.269 (35MW/130MW) on a relative basis. +* Max capacity credit PV [MWac/MWdc] = (Inverter - Battery capacity credit) / PV_dc +* = (PV_dc / ILR - PV_dc * BCR) / PV_dc +* = 1/ILR - BCR +* marginal capacity credit +m_cc_mar(i,r,ccseason,t)$[tload(t)$pvb(i)] = min{ m_cc_mar(i,r,ccseason,t), 1 / ilr(i) - bcr(i) } ; + +* old capacity credit +* (1) convert cc_old from MW to a fractional basis +* (2) adjust the fractional value to be less than 1/ILR - BCR +* (3) multiply by CAP to convert back to MW +cc_old(i,r,ccseason,t)$[tload(t)$pvb(i)$sum{(v,tt)$tprev(t,tt), CAP.l(i,v,r,tt)}] = + min{ cc_old(i,r,ccseason,t) / sum{(v,tt)$tprev(t,tt), CAP.l(i,v,r,tt)}, 1 / ilr(i) - bcr(i) } + * sum{(v,tt)$tprev(t,tt), CAP.l(i,v,r,tt)}; + +$endif.tcheck + + +*** Calculate financial multipliers +* These are calculated here because the ITC phaseout can influence these parameters, +* and the timing of the phaseout is not known beforehand. +$include reeds%ds%core%ds%solve%ds%2_financials.gms + + +$ifthene %cur_year%==%startyear% +*initialize CAP.l for 2010 because it has not been defined yet +CAP.l(i,v,r,"%startyear%")$[m_capacity_exog(i,v,r,"%startyear%")] = m_capacity_exog(i,v,r,"%startyear%") ; +$endif + +$ifthene %cur_year%==%startyear% +*initialize CAP_ENERGY.l for 2010 because it has not been defined yet +CAP_ENERGY.l(i,v,r,"%startyear%")$[m_capacity_exog_energy(i,v,r,"%startyear%")] = m_capacity_exog_energy(i,v,r,"%startyear%") ; +$endif + +* Now that cost_cap_fin_mult is done, calculate cost_growth, which is +* the minimum cost of that technology within a state +if(Sw_GrowthPenalties > 0, +*rsc_fin_mult holds the multipliers for sccapcosttech, so don't include them here + cost_growth(i,st,t)$[tmodel(t)$sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)$(not sccapcosttech(i))] = + smin{r$[valinv_irt(i,r,t)$r_st(r,st)$cost_cap_fin_mult(i,r,t)], + cost_cap_fin_mult(i,r,t) * cost_cap(i,t) } ; + +*rsc_fin_mult holds the capital costs for sccapcosttech + cost_growth(i,st,t)$[tmodel(t)$sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)$sccapcosttech(i)] = + smin{(r,rscbin)$[valinv_irt(i,r,t)$r_st(r,st)$rsc_fin_mult(i,r,t)], + rsc_fin_mult(i,r,t) * m_rsc_dat(r,i,rscbin,"cost") } ; + + cost_growth(i,st,t)$cost_growth(i,st,t) = round(cost_growth(i,st,t),3) ; +) ; + +* Write the inputs for debugging and error checks: +* Always write data for the first solve year (currently always 2010). +* Overwrites the versions written by e_solveprep.gms and 2_temporal_params.gms. +$ifthene.write %cur_year%=%startyear% +execute_unload 'inputs_case%ds%inputs.gdx' ; +$endif.write + +* If using debug mode, write the inputs for every solve year +$ifthene.debug %debug%>0 +execute_unload 'alldata_%stress_year%.gdx' ; +$endif.debug + + +* --- diagnoses gdx dump settings --- +$ifthene.diagnose %diagnose%=1 +$ifthene.diagnose_2 %diagnose_year%<=%cur_year% +$include inputs_case%ds%diagnose.gms +$endif.diagnose_2 +$endif.diagnose + + +* ------------------------------ +* Solve the Model +* ------------------------------ +$ifthen.valstr %GSw_ValStr% == 1 +OPTION lp = convert ; +ReEDSmodel.optfile = 1 ; +$echo dumpgdx ReEDSmodel_jacobian.gdx > convert.opt +solve ReEDSmodel minimizing z using lp ; +OPTION lp = %solver% ; +ReEDSmodel.optfile = %GSw_gopt% ; +OPTION savepoint = 1 ; +$endif.valstr + +solve ReEDSmodel minimizing z using lp ; +tsolved(t)$tmodel(t) = yes ; + +* record objective function values right after solve +z_rep(t)$tmodel(t) = Z.l ; +z_rep_inv(t)$tmodel(t) = Z_inv.l(t) ; +z_rep_op(t)$tmodel(t) = Z_op.l(t) ; + + +* --------------------------------- +* Modeling to Generate Alternatives +* --------------------------------- +$ifthene.mga %GSw_MGA_CostDelta%>0 +$ifthene.mga1 %cur_year%>=%GSw_StartMarkets% +*## Activate MGA mode +Sw_MGA = 1 ; +solve ReEDSmodel %GSw_MGA_Direction%imizing MGA_OBJ using lp ; +*## Deactivate MGA mode +Sw_MGA = 0 ; +$endif.mga1 +$endif.mga + + +*** Adjust some parameters based on the solution for this solve year +$include reeds%ds%core%ds%solve%ds%4_post_solve_adjustments.gms + +*** Fix decision variables to their optimized levels for this solve year +tfix("%cur_year%") = yes ; +$include reeds%ds%core%ds%solve%ds%5_varfix.gms + +*** Dump data used in calculations between solve years +$include reeds%ds%core%ds%solve%ds%6_data_dump.gms + +*** Abort if the solver returns an error +if (ReEDSmodel.modelStat > 1, + abort "Model did not solve to optimality", + ReEDSmodel.modelStat) ; diff --git a/reeds/core/solve/3_solve_window.gms b/reeds/core/solve/3_solve_window.gms new file mode 100644 index 00000000..505675ac --- /dev/null +++ b/reeds/core/solve/3_solve_window.gms @@ -0,0 +1,154 @@ +* global needed for this file: +* case : name of case you're running +* niter : current iteration + +$setglobal ds \ + +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +$log 'Running window solve for...' +$log ' case == %case%' +$log ' iteration == %niter%' +$log ' window == %window%' + +*remove any load years +tload(t) = no ; + +$if not set niter $setglobal niter 0 +$eval previter %niter%-1 + +tmodel(t) = no ; +*enable years that fall within the window range +tmodel(t)$[tmodel_new(t)$(yeart(t)>=solvewindows("%window%","start")) + $(yeart(t)<=solvewindows("%window%","stop"))] = yes ; + + +*reset tlast to the final modeled period for this window +*then re-compute the financial multiplier for pv +tlast(t) = no ; +tlast(t)$[ord(t)=smax(tt$tmodel(tt),ord(tt))] = yes ; + +pvf_capital(t)$tmodel(t) = pvf_capital0(t) ; +pvf_onm(t) = pvf_onm0(t) ; +pvf_onm(t)$tlast(t) = round(pvf_capital0(t) / crf(t), 6) ; + +*if this isn't the first iteration +$ifthene.notfirstiter %niter%>0 + +*============================ +* --- CC and Curtailment --- +*============================ + +*indicate we're loading data +tload(t)$tmodel(t) = yes ; + +$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_merged_%niter%.gdx +$loaddcr loadset = merged_set_1 +$loaddcr cc_old_load2 = cc_old +$loaddcr cc_mar_load2 = cc_mar +$loaddcr sdbin_size_load2 = sdbin_size +$gdxin + +*collapse the set that came from merging the gdx files + +cc_old_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_old_load2(loadset,i,r,ccreg,szn,t) } } ; +cc_mar_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_mar_load2(loadset,i,r,ccreg,szn,t) } } ; + +sdbin_size_load(ccreg,szn,sdbin,t) = sum{loadset, sdbin_size_load2(loadset,ccreg,szn,sdbin,t) } ; + +*=============================== +* --- Begin Capacity Credit --- +*=============================== + +*Clear params before calculation +cc_int(i,v,r,szn,t) = 0 ; +cc_totmarg(i,r,szn,t) = 0 ; +cc_excess(i,r,szn,t) = 0 ; +cc_scale(i,r,szn,t) = 0 ; +sdbin_size(ccreg,szn,sdbin,t)$tload(t) = 0 ; + +*Storage duration bin sizes by year +sdbin_size(ccreg,szn,sdbin,t)$tload(t) = sdbin_size_load(ccreg,szn,sdbin,t) ; + +*Sw_Int_CC=0 means use average capacity credit for each tech, and don't differentiate vintages +*If there is no existing capacity to calculate average, use marginal capacity credit instead. +if(Sw_Int_CC=0, + cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)$sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) }] = + cc_old_load(i,r,szn,t) / sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) } ; + cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)$(cc_old_load(i,r,szn,t)=0)] = m_cc_mar(i,r,szn,t) ; +) ; + +*For the remaining options we initially use marginal values for cc_int, differentiated by vintage based on seasonal capacity factors. +if(Sw_Int_CC=1 or Sw_Int_CC=2, + cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)$sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) }] = + m_cc_mar(i,r,szn,t) * m_cf_szn(i,v,r,szn,t) / sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) } ; + cc_totmarg(i,r,szn,t)$[tload(t)$(vre(i) or storage(i))] = sum{v$valcap(i,v,r,t), cc_int(i,v,r,szn,t) * CAP.l(i,v,r,t) } ; +) ; + +*Sw_Int_CC=1 means use average capacity credit for each tech, but differentiate based on vintage. +*Start with marginal capacity credit with seasonal vintage-based capacity factor adjustment, +*and scale with cc_old_load to result in the correct total capacity credit. +if(Sw_Int_CC=1, + cc_scale(i,r,szn,t)$[tload(t)$(vre(i) or storage(i))] = 1 ; + cc_scale(i,r,szn,t)$[tload(t)$(vre(i) or storage(i))$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) / cc_totmarg(i,r,szn,t) ; + cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)] = cc_int(i,v,r,szn,t) * cc_scale(i,r,szn,t) ; +) ; + +*Sw_Int_CC=2 means use marginal capacity credit, adjusted by seasonal capacity factors by vintage +if(Sw_Int_CC=2, + cc_excess(i,r,szn,t)$[tload(t)$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) - cc_totmarg(i,r,szn,t) ; +) ; + + +*no longer want m_cc_mar since it should not enter the planning reserve margin constraint +m_cc_mar(i,r,szn,t) = 0 ; + +cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) > 1] = 1 ; +cc_int(i,v,r,szn,t)$[tload(t)$csp_storage(i)$valcap(i,v,r,t)] = 1 ; + +*======================================= +* --- Begin Averaging of CC/Curt --- +*======================================= + +$ifthene.afterseconditer %niter%>1 + +*when set to 1 - it will take the average over all previous iterations +if(Sw_AVG_iter=1, + cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)] = round((cc_int(i,v,r,szn,t) + cc_iter(i,v,r,szn,t,"%previter%")) / 2 ,4) ; + ) ; + +$endif.afterseconditer + +*Remove very small numbers to make it easier for the solver +cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) < 0.001] = 0 ; + +cc_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) ; + +execute_unload 'ReEDS_Augur%ds%augur_data%ds%curtout_%case%_%niter%.gdx' cc_int ; + +*following line will load in the level values if the switch is enabled +*note that this is still within the conditional that we are now past the first iteration +*and thus a loadpoint is enabled +if(Sw_Loadpoint = 1, +execute_loadpoint 'gdxfiles%ds%%case%_load.gdx' ; +%case%.optfile = 8 ; +) ; + +$endif.notfirstiter + +*============================== +* --- Solve Supply Side --- +*============================== + +solve ReEDSmodel using lp minimizing z ; + +*add years to tfix(t) if this is the last iteration +$ifthene.lastiter %niter%=%maxiter% + +$eval nextwindow %window% + 1 +tfix(t)$(tmodel(t)$(yeart(t)%cur_year%],ivt(i,v,tt)}) + $(not sum(tt$[valinv(i,v,r,tt)$(yeart(tt)>%cur_year%)],1)) +* if it has not been upgraded.. +* note the newv condition above allows for the capacity equations +* of motion to still function - this would/does not work for initv vintanges without additional work + $(not sum{(tt,ii)$[tsolved(tt)$upgrade_from(ii,i)$valcap(ii,v,r,tt)], + UPGRADES.l(ii,v,r,tt)}) + ] = yes ; + valcap(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; + valcap_h2ptc(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; + valgen(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; + valgen_h2ptc(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no; + valinv(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; + inv_cond(i,v,r,t,"%cur_year%")$valcap_remove(i,v,r,t,"%cur_year%") = no ; + valcap_irt(i,r,t) = sum{v, valcap(i,v,r,t) } ; + valcap_iv(i,v)$sum{(r,t)$tmodel_new(t), valcap(i,v,r,t) } = yes ; + valcap_i(i)$sum{v, valcap_iv(i,v) } = yes ; + valcap_ivr(i,v,r)$sum{t, valcap(i,v,r,t) } = yes ; + valgen_irt(i,r,t) = sum{v, valgen(i,v,r,t) } ; + valinv_irt(i,r,t) = sum{v, valinv(i,v,r,t) } ; + valinv_tg(st,tg,t)$sum{(i,r)$[tg_i(tg,i)$r_st(r,st)], valinv_irt(i,r,t) } = yes ; + +) ; + +*** Adjust CCS incentives for upgrades +if(Sw_Upgrades = 1, +* note sum over tt required here as we want to only remove the incentive +* from years beyond the current year if upgrades occurred in this solve year + +* extend the current-year incentive beyond current date to expiration date - only needed +* when needing to specify beyond current amounts + co2_captured_incentive(i,v,r,t)$[sum{tt$tmodel(tt),upgrades.l(i,v,r,tt) } + $(not sum{tt$tfix(tt),upgrades.l(i,v,r,tt)}) + $(year(t) < %cur_year% + co2_capture_incentive_length) + $(yeart(t) >= %cur_year%) + $valcap(i,v,r,t) ] = co2_captured_incentive(i,v,r,"%cur_year%") ; + +* remove co2 captured incentive after the length of time if upgrades occurred in this year + co2_captured_incentive(i,v,r,t)$[sum{tt$tmodel(tt),upgrades.l(i,v,r,tt) } + $(year(t) >= %cur_year% + co2_capture_incentive_length) + $valcap(i,v,r,t) ] = 0 ; + +* adjust fom of upgraded-from plant to updated cost for maintaining the CCS equipment + cost_fom(i,v,r,t)$[sum{(ii,tt)$[tmodel(tt)$upgrade_from(ii,i)],upgrades.l(ii,v,r,tt) } + $(year(t) >= %cur_year%) + $valcap(i,v,r,t) ] = + max(cost_fom(i,v,r,t), + sum{ii$upgrade_from(ii,i),cost_fom(ii,v,r,t) } + ) ; +) ; + + +*** Regional emissions for tax credit phaseout +* emit_r_tc is calculated the same as the EMIT variable in the model. We do not use +* EMIT.l here because the emissions are only modeled for those in the emit_modeled set. +emit_r_tc(r,t)$tmodel_new(t) = + +* Emissions from generation + sum{(i,v,h)$[valgen(i,v,r,t)$h_rep(h)], + hours(h) * emit_rate("process","CO2",i,v,r,t) + * (GEN.l(i,v,r,h,t) + + CCSFLEX_POW.l(i,v,r,h,t)$[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)]) + } + +* Plus emissions produced via production activities (SMR, SMR-CCS, DAC) +* The "production" of negative CO2 emissions via DAC is also included here + + sum{(p,i,v,h)$[valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], + hours(h) * prod_emit_rate("process","CO2",i,t) + * PRODUCE.l(p,i,v,r,h,t) + } + +*[minus] co2 reduce from flexible CCS capture +*capture = capture per energy used by the ccs system * CCS energy + +* Flexible CCS - bypass + - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_byp(i)$h_rep(h)], + ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POW.l(i,v,r,h,t) })$Sw_CCSFLEX_BYP + +* Flexible CCS - storage + - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_sto(i)$h_rep(h)], + ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POWREQ.l(i,v,r,h,t) })$Sw_CCSFLEX_STO +; + +emit_nat_tc(t)$tmodel_new(t) = sum{r, emit_r_tc(r,t) } ; + + +*** Recalculate regional CO2 emissions rate for use in state CO2 cap import accounting +* [metric kiloton] * [1000 metric ton / metric kiloton] / ([MW] * [hours]) = [metric ton/MWh] +$ifthen.stateco2 %GSw_StateCO2ImportLevel% == 'r' + co2_emit_rate_r(r,t)$tmodel(t) = ( + emit_r_tc(r,t) + / sum{(i,v,h)$[valgen(i,v,r,t)], hours(h) * GEN.l(i,v,r,h,t) } +* Avoid division-by-zero errors + )$sum{(i,v,h)$[valgen(i,v,r,t)], hours(h) * GEN.l(i,v,r,h,t) } ; +$else.stateco2 +* sum emissions and generation within the region + co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t)$tmodel(t) = ( + sum{rr$r_%GSw_StateCO2ImportLevel%(rr,%GSw_StateCO2ImportLevel%), + emit_r_tc(rr,t) } + / sum{(i,v,rr,h)$[valgen(i,v,rr,t) + $r_%GSw_StateCO2ImportLevel%(rr,%GSw_StateCO2ImportLevel%)], + hours(h) * GEN.l(i,v,rr,h,t) } +* Avoid division-by-zero errors + )$sum{(i,v,rr,h)$[valgen(i,v,rr,t) + $r_%GSw_StateCO2ImportLevel%(rr,%GSw_StateCO2ImportLevel%)], + hours(h) * GEN.l(i,v,rr,h,t) } + ; +* broadcast the regional emissions rate to each r in the region + co2_emit_rate_r(r,t)$tmodel(t) = + sum{%GSw_StateCO2ImportLevel% + $r_%GSw_StateCO2ImportLevel%(r,%GSw_StateCO2ImportLevel%), + co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t) } + ; +$endif.stateco2 diff --git a/reeds/core/solve/5_varfix.gms b/reeds/core/solve/5_varfix.gms new file mode 100644 index 00000000..85db2442 --- /dev/null +++ b/reeds/core/solve/5_varfix.gms @@ -0,0 +1,125 @@ +* Round problematic variables +* Non-rounded parameters can sometimes cause numerical issues when summing over tfix in model equations +if(Sw_RemoveSmallNumbers = 1, + CAP.l(i,v,r,tfix)$[abs(CAP.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; + CAP_ENERGY.l(i,v,r,tfix)$[abs(CAP_ENERGY.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; + UPGRADES.l(i,v,r,tfix)$[abs(UPGRADES.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; + CAP_ABOVE_LIM.l(tg,r,tfix)$[abs(CAP_ABOVE_LIM.l(tg,r,tfix)) < rhs_tolerance] = 0 ; + INV.l(i,v,r,tfix)$[abs(INV.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; + INV_ENERGY.l(i,v,r,tfix)$[abs(INV_ENERGY.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; + INV_RSC.l(i,v,r,rscbin,tfix)$[abs(INV_RSC.l(i,v,r,rscbin,tfix)) < rhs_tolerance] = 0 ; + INV_POI.l(r,tfix)$[abs(INV_POI.l(r,tfix)) < rhs_tolerance] = 0 ; + H2_STOR_INV.l(h2_stor,r,tfix)$[abs(H2_STOR_INV.l(h2_stor,r,tfix)) < rhs_tolerance] = 0 ; + H2_TRANSPORT_INV.l(r,rr,tfix) $[abs(H2_TRANSPORT_INV.l(r,rr,tfix) ) < rhs_tolerance] = 0 ; +); + +*load variable +LOAD.fx(r,h,tfix) = LOAD.l(r,h,tfix) ; +FLEX.fx(flex_type,r,h,tfix)$Sw_EFS_flex = FLEX.l(flex_type,r,h,tfix) ; +* PEAK_FLEX.fx(r,ccseason,tfix)$Sw_EFS_flex = PEAK_FLEX.l(r,ccseason,tfix) ; +DROPPED.fx(r,h,tfix)$[(yeart(tfix)=model_builds_start_yr) + $(sum{(tgg,rr), cap_limit(tgg,rr,tfix)}) + $sum{(i,newv)$tg_i(tg,i), valinv(i,newv,r,tfix)}] = CAP_ABOVE_LIM.l(tg,r,tfix) ; +CAP_SDBIN.fx(i,v,r,ccseason,sdbin,tfix)$[valcap(i,v,r,tfix)$(storage(i) or hyd_add_pump(i))$(not csp(i))$Sw_PRM_CapCredit] = CAP_SDBIN.l(i,v,r,ccseason,sdbin,tfix) ; +CAP_SDBIN_ENERGY.fx(i,v,r,ccseason,sdbin,tfix)$[valcap(i,v,r,tfix)$battery(i)$Sw_PRM_CapCredit] = CAP_SDBIN_ENERGY.l(i,v,r,ccseason,sdbin,tfix) ; +GROWTH_BIN.fx(gbin,i,st,tfix)$[sum{r$[r_st(r,st)], valinv_irt(i,r,tfix) }$stfeas(st)$Sw_GrowthPenalties$(yeart(tfix)<=Sw_GrowthPenLastYear)] = GROWTH_BIN.l(gbin,i,st,tfix) ; +INV.fx(i,v,r,tfix)$[valinv(i,v,r,tfix)] = INV.l(i,v,r,tfix) ; +INV_ENERGY.fx(i,v,r,tfix)$[valinv(i,v,r,tfix)$battery(i)] = INV_ENERGY.l(i,v,r,tfix) ; +INV_REFURB.fx(i,v,r,tfix)$[valinv(i,v,r,tfix)$refurbtech(i)] = INV_REFURB.l(i,v,r,tfix) ; +INV_RSC.fx(i,v,r,rscbin,tfix)$[valinv(i,v,r,tfix)$rsc_i(i)$m_rscfeas(r,i,rscbin)] = INV_RSC.l(i,v,r,rscbin,tfix) ; +CAP_RSC.fx(i,v,r,rscbin,tfix)$[valcap(i,v,r,tfix)$rsc_i(i)$m_rscfeas(r,i,rscbin)] = CAP_RSC.l(i,v,r,rscbin,tfix) ; +INV_CAP_UP.fx(i,v,r,rscbin,tfix)$[allow_cap_up(i,v,r,rscbin,tfix)] = INV_CAP_UP.l(i,v,r,rscbin,tfix) ; +INV_ENER_UP.fx(i,v,r,rscbin,tfix)$[allow_ener_up(i,v,r,rscbin,tfix)] = INV_ENER_UP.l(i,v,r,rscbin,tfix) ; +UPGRADES.fx(i,v,r,tfix)$[valcap(i,v,r,tfix)$upgrade(i)] = UPGRADES.l(i,v,r,tfix) ; +UPGRADES_RETIRE.fx(i,v,r,tfix)$[valcap(i,v,r,tfix)$upgrade(i)] = UPGRADES_RETIRE.l(i,v,r,tfix) ; +EXTRA_PRESCRIP.fx(pcat,r,tfix)$[force_pcat(pcat,tfix)$sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,tfix) }] = EXTRA_PRESCRIP.l(pcat,r,tfix) ; +EXTRA_PRESCRIP_ENERGY.fx(pcat,r,tfix)$[force_pcat(pcat,tfix)$sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,tfix) }] = EXTRA_PRESCRIP_ENERGY.l(pcat,r,tfix) ; + +* generation and storage variables +GEN.fx(i,v,r,h,tfix)$valgen(i,v,r,tfix) = GEN.l(i,v,r,h,tfix) ; +GEN_PLANT.fx(i,v,r,h,tfix)$[storage_hybrid(i)$(not csp(i))$valgen(i,v,r,tfix)$Sw_HybridPlant] = GEN_PLANT.l(i,v,r,h,tfix) ; +GEN_STORAGE.fx(i,v,r,h,tfix)$[storage_hybrid(i)$(not csp(i))$valgen(i,v,r,tfix)$Sw_HybridPlant] = GEN_STORAGE.l(i,v,r,h,tfix) ; +CURT.fx(r,h,tfix)$Sw_CurtMarket = CURT.l(r,h,tfix) ; +MINGEN.fx(r,szn,tfix)$Sw_Mingen = MINGEN.l(r,szn,tfix) ; +STORAGE_IN.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$(storage_standalone(i) or hyd_add_pump(i))] = STORAGE_IN.l(i,v,r,h,tfix) ; +STORAGE_IN_PLANT.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] = STORAGE_IN_PLANT.l(i,v,r,h,tfix) ; +STORAGE_IN_GRID.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] = STORAGE_IN_GRID.l(i,v,r,h,tfix) ; +STORAGE_LEVEL.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$storage(i)$(not storage_interday(i))] = STORAGE_LEVEL.l(i,v,r,h,tfix) ; +STORAGE_INTERDAY_LEVEL.fx(i,v,r,allszn,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL.l(i,v,r,allszn,tfix) ; +STORAGE_INTERDAY_DISPATCH.fx(i,v,r,allh,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_DISPATCH.l(i,v,r,allh,tfix) ; +STORAGE_INTERDAY_LEVEL_MAX_DAY.fx(i,v,r,allszn,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL_MAX_DAY.l(i,v,r,allszn,tfix) ; +STORAGE_INTERDAY_LEVEL_MIN_DAY.fx(i,v,r,allszn,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL_MIN_DAY.l(i,v,r,allszn,tfix) ; +AVAIL_SITE.fx(x,h,tfix)$[Sw_SpurScen$xfeas(x)] = AVAIL_SITE.l(x,h,tfix) ; +RAMPUP.fx(i,r,h,hh,tfix)$[Sw_StartCost$startcost(i)$numhours_nexth(h,hh)$valgen_irt(i,r,tfix)] = RAMPUP.l(i,r,h,hh,tfix) ; + +* flexible CCS variables +CCSFLEX_POW.fx(i,v,r,h,tfix)$[ccsflex(i)$valgen(i,v,r,tfix)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)] = CCSFLEX_POW.l(i,v,r,h,tfix) ; +CCSFLEX_POWREQ.fx(i,v,r,h,tfix)$[ccsflex_sto(i)$valgen(i,v,r,tfix)$Sw_CCSFLEX_STO] = CCSFLEX_POWREQ.l(i,v,r,h,tfix) ; +CCSFLEX_STO_STORAGE_LEVEL.fx(i,v,r,h,tfix)$[ccsflex_sto(i)$valgen(i,v,r,tfix)$Sw_CCSFLEX_STO] = CCSFLEX_STO_STORAGE_LEVEL.l(i,v,r,h,tfix) ; +CCSFLEX_STO_STORAGE_CAP.fx(i,v,r,tfix)$[ccsflex_sto(i)$valcap(i,v,r,tfix)$Sw_CCSFLEX_STO] = CCSFLEX_STO_STORAGE_CAP.l(i,v,r,tfix) ; + +* trade variables +FLOW.fx(r,rr,h,tfix,trtype)$routes(r,rr,trtype,tfix) = FLOW.l(r,rr,h,tfix,trtype) ; +OPRES_FLOW.fx(ortype,r,rr,h,tfix)$[Sw_OpRes$opres_model(ortype)$opres_routes(r,rr,tfix)$opres_h(h)] = OPRES_FLOW.l(ortype,r,rr,h,tfix) ; +PRMTRADE.fx(r,rr,trtype,ccseason,tfix)$[routes(r,rr,trtype,tfix)$routes_prm(r,rr)] = PRMTRADE.l(r,rr,trtype,ccseason,tfix) ; + +* operating reserve variables +OPRES.fx(ortype,i,v,r,h,tfix)$[Sw_OpRes$valgen(i,v,r,tfix)$reserve_frac(i,ortype)$opres_h(h)] = OPRES.l(ortype,i,v,r,h,tfix) ; + +* variable fuel amounts +GASUSED.fx(cendiv,gb,h,tfix)$[(Sw_GasCurve=0)$h_rep(h)] = GASUSED.l(cendiv,gb,h,tfix) ; +VGASBINQ_NATIONAL.fx(fuelbin,tfix)$[Sw_GasCurve=1] = VGASBINQ_NATIONAL.l(fuelbin,tfix) ; +VGASBINQ_REGIONAL.fx(fuelbin,cendiv,tfix)$[Sw_GasCurve=1] = VGASBINQ_REGIONAL.l(fuelbin,cendiv,tfix) ; +BIOUSED.fx(bioclass,r,tfix)$[sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,tfix) }] = BIOUSED.l(bioclass,r,tfix) ; + +* RECS variables +RECS.fx(RPSCat,i,st,ast,tfix)$[stfeas(st)$RecMap(i,RPSCat,st,ast,tfix)$(stfeas(ast) or sameas(ast,"voluntary"))$Sw_StateRPS] = RECS.l(RPSCat,i,st,ast,tfix) ; +ACP_Purchases.fx(RPSCat,st,tfix)$[(stfeas(st) or sameas(st,"voluntary"))$Sw_StateRPS] = ACP_Purchases.l(RPSCat,st,tfix) ; +EMIT.fx(etype,e,r,tfix)$emit_modeled(e,r,tfix) = EMIT.l(etype,e,r,tfix) ; + +* transmission variables +CAPTRAN_ENERGY.fx(r,rr,trtype,tfix)$routes(r,rr,trtype,tfix) = CAPTRAN_ENERGY.l(r,rr,trtype,tfix) ; +CAPTRAN_PRM.fx(r,rr,trtype,tfix)$[routes(r,rr,trtype,tfix)$routes_prm(r,rr)] = CAPTRAN_PRM.l(r,rr,trtype,tfix) ; +CAPTRAN_GRP.fx(transgrp,transgrpp,tfix)$trancap_init_transgroup(transgrp,transgrpp,"AC") = CAPTRAN_GRP.l(transgrp,transgrpp,tfix) ; +INVTRAN.fx(r,rr,trtype,tfix)$routes_inv(r,rr,trtype,tfix) = INVTRAN.l(r,rr,trtype,tfix) ; +INVTRAN_AC.fx(r,rr,tscbin,tfix)$routes_inv(r,rr,"AC",tfix) = INVTRAN_AC.l(r,rr,tscbin,tfix) ; +INV_CONVERTER.fx(r,tfix)$Sw_VSC = INV_CONVERTER.l(r,tfix) ; +CAP_CONVERTER.fx(r,tfix)$Sw_VSC = CAP_CONVERTER.l(r,tfix) ; +CONVERSION.fx(r,h,intype,outtype,tfix)$Sw_VSC = CONVERSION.l(r,h,intype,outtype,tfix) ; +CONVERSION_PRM.fx(r,ccseason,intype,outtype,tfix)$Sw_VSC = CONVERSION_PRM.l(r,ccseason,intype,outtype,tfix) ; +CAP_SPUR.fx(x,tfix)$[Sw_SpurScen$xfeas(x)] = CAP_SPUR.l(x,tfix) ; +INV_SPUR.fx(x,tfix)$[Sw_SpurScen$xfeas(x)] = INV_SPUR.l(x,tfix) ; +INV_POI.fx(r,tfix)$Sw_TransIntraCost = INV_POI.l(r,tfix) ; +TRAN_CAPEX_BINS.fx(r,rr,tscbin,tfix)$[routes_inv(r,rr,"AC",tfix)$tsc_binwidth(r,rr,tscbin)] = TRAN_CAPEX_BINS.l(r,rr,tscbin,tfix) ; + +* water climate variables +WATCAP.fx(i,v,r,tfix)$[valcap(i,v,r,tfix)$Sw_WaterMain$Sw_WaterCapacity] = WATCAP.l(i,v,r,tfix) ; +WAT.fx(i,v,w,r,h,tfix)$[i_water(i)$valgen(i,v,r,tfix)$Sw_WaterMain] = WAT.l(i,v,w,r,h,tfix) ; +WATER_CAPACITY_LIMIT_SLACK.fx(wst,r,tfix)$[Sw_WaterMain$Sw_WaterCapacity] = WATER_CAPACITY_LIMIT_SLACK.l(wst,r,tfix) ; + +*H2 and DAC production variables +PRODUCE.fx(p,i,v,r,h,tfix)$[consume(i)$i_p(i,p)$valcap(i,v,r,tfix)$h_rep(h)$Sw_Prod] = PRODUCE.l(p,i,v,r,h,tfix) ; +H2_FLOW.fx(r,rr,h,tfix)$[h2_routes(r,rr)$(Sw_H2 = 2)] = H2_FLOW.l(r,rr,h,tfix) ; +H2_TRANSPORT_INV.fx(r,rr,tfix)$[h2_routes(r,rr)$(Sw_H2 = 2)] = H2_TRANSPORT_INV.l(r,rr,tfix) ; +H2_STOR_INV.fx(h2_stor,r,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_INV.l(h2_stor,r,tfix) ; +H2_STOR_CAP.fx(h2_stor,r,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_CAP.l(h2_stor,r,tfix) ; +H2_STOR_IN.fx(h2_stor,r,h,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_IN.l(h2_stor,r,h,tfix) ; +H2_STOR_OUT.fx(h2_stor,r,h,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_OUT.l(h2_stor,r,h,tfix) ; +H2_STOR_LEVEL.fx(h2_stor,r,actualszn,h,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)$(Sw_H2_StorTimestep=2)] = H2_STOR_LEVEL.l(h2_stor,r,actualszn,h,tfix) ; +H2_STOR_LEVEL_SZN.fx(h2_stor,r,actualszn,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)$(Sw_H2_StorTimestep=1)] = H2_STOR_LEVEL_SZN.l(h2_stor,r,actualszn,tfix) ; + +*CO2-related variables +CO2_CAPTURED.fx(r,h,tfix)$Sw_CO2_Detail = CO2_CAPTURED.l(r,h,tfix) ; +CO2_STORED.fx(r,cs,h,tfix)$[Sw_CO2_Detail$r_cs(r,cs)] = CO2_STORED.l(r,cs,h,tfix) ; +CO2_FLOW.fx(r,rr,h,tfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = CO2_FLOW.l(r,rr,h,tfix) ; +CO2_TRANSPORT_INV.fx(r,rr,tfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = CO2_TRANSPORT_INV.l(r,rr,tfix) ; +CO2_SPURLINE_INV.fx(r,cs,tfix)$[Sw_CO2_Detail$r_cs(r,cs)] = CO2_SPURLINE_INV.l(r,cs,tfix) ; diff --git a/reeds/core/solve/6_data_dump.gms b/reeds/core/solve/6_data_dump.gms new file mode 100644 index 00000000..29c97036 --- /dev/null +++ b/reeds/core/solve/6_data_dump.gms @@ -0,0 +1,424 @@ +$ontext +This file creates a gdx file with all of the data necessary for the Augur module to solve. This includes: + - Generator capacities + - Exogenous retirments (sequential solves only) + - Wind capacity by build year (because wind CFs change by build year) + - heat rates, fuel costs, and vom costs + - capacity factors (hydro, wind) + - availability rates (1 - outage rates) + - transmission capacities and loss rates + - technology sets +$offtext + +$if not set start_year $setglobal start_year %startyear% + +*=============================== +* Set and parameter definitions +*=============================== + +set rfeas(r) "list of feasible r regions - for use in Augur only" + trange(t) "range from first year to current year" + tcur(t) "current year" + tnext(t) "next year" + valcap_i_filt(i) "subset of valcap" + valcap_ir_filt(i,r) "subset of valcap" + valcap_iv_filt(i,v) "subset of valcap" + routes_filt(r,rr,trtype) "set of transmission connections" +; + +parameter +avail_filt(i,v,r,allszn) "--fraction-- fraction of capacity available for generation by season" +can_exports_h_filt(r,allh) "--MW-- Canada exports by region and timeslice filtered for the previous solve year" +can_imports_cap(i,v,r) "--MW-- Canadian import max capacity" +can_imports_szn_filt(r,allszn) "--MWh-- Canada imports by region and season filtered for the previous solve year" +cap_converter_filt(r) "--MW-- VSC AC/DC converter capacity" +cap_exist_i(i) "--MW-- technologies with existing capacity in the current solve year" +cap_exist_ir(i,r) "--MW-- technology-region combinations with existing capacity in the current solve year" +cap_exist_iv(i,v) "--MW-- technology-vintage combinations with existing capacity in the current solve year" +cap_exist(i,v,r) "--MW-- capacity that exists in the current solve year" +cap_exog_filt(i,v,r) "--MW-- exogenous capacity" +cap_hyd_szn_adj_filt(i,allszn,r) "--fraction-- seasonal hydro capacity adjustment filtered for the previous solve year" +cap_init(i,v,r) "--MW-- initial capacity" +cap_ivrt(i,v,r,t) "--MW-- generation power capacity" +cap_energy_ivrt(i,v,r,t) "--MWh-- generation energy capacity" +cap_pvb(i,v,r) "--MW-- Hybrid PV+battery capacity (PV)" +cap_trans_energy(r,rr,trtype) "--MW-- transmission capacity for energy trading" +cap_trans_prm(r,rr,trtype) "--MW-- transmission capacity for PRM trading" +cf_adj_t_filt(i,v,t) "--fraction-- capacity factor adjustment for wind" +cost_cap_filt(i,t) "--2004$/MW-- technology capital costs" +cost_cap_fin_mult_filt(i,r,t) "--unitless-- capital cost financial multipliers" +cost_vom_filt(i,v,r) "--$/MWh-- VO&M costs filtered for the previous solve year and existing capacity" +ctt_i_ii_filt(i,ii) "--set-- set linking watercooling techs i to numeraire techs ii filtered for existing watercooling techs" +ctt_i_ii_psh(i,ii) "--set-- set linking PSH techs with water i to numeraire techs ii filtered for valid capacity techs" +emissions_price(e,r) "--2004$/metric ton-- combined emissions taxes and marginal prices for emissions caps" +emit_rate_filt(e,i,v,r) "--metric tons/MWh-- emission rate for the previous solve year" +energy_price(r,allh) "--2004$/MWh-- energy price from the previous solve year" +flex_load_opt(r,allh) "--MW-- model results for optimizing flexible load" +flex_load(r,allh) "--MW-- total exogenously defined flexible load" +fuel_price_filt(i,r) "--$/mmBTU-- fuel prices filtered for the previous solve year and existing capacity" +gen_h_stress_filt(i,r,allh,t) "--MW-- generation by stress timeslice with charge and production load as negative generation" +heat_rate_filt(i,v,r) "--MMBtu/MWh-- heat rate" +h2_usage_regional(r,allh,t) "--metric tons-- H2 usage by region" +inv_cond_filt(i,v,t) "--set-- vintage-year mapping for investments by technology" +inv_ivrt(i,v,r,t) "--MW-- investments in power generation capacity" +inv_energy_ivrt(i,v,r,t) "--MWh-- investments in energy generation capacity" +m_cf_filt(i,v,r,allh) "--fraction-- capacity factor used in the model" +m_cf_szn_filt(i,v,r,allszn) "--fraction-- modelled capacity factors filtered for hydro resources to set seasonal energy constraints" +minloadfrac_filt(r,i,allszn) "--fraction-- modelled mingen fraction filtered for hydro resources to set mingen constraints" +prod_filt(i,v,r,allh) "--MW-- power consumed for PRODUCE.l" +ra_cap_loadsite(r,t) "--MW-- capacity of flexibly sited load" +repbioprice_filt(r) "--2004$/MWh-- marginal price for biofuel in region where biofuel was used" +repgasprice_filt(r) "--$/mmBTU-- NG prices in ReEDS filtered for the previous solve year" +repgasprice_r(r,t) "--$/mmBTU-- NG prices in ReEDS, switch-dependent, at the BA level" +repgasprice(cendiv,t) "--$/mmBTU-- NG prices in ReEDS, the calculation of which depends on what switch is used" +repgasquant(cendiv,t) "--mmBTU-- NG fuel usage in ReEDS - used to determine NG price" +ret_ivrt(i,v,r,t) "--MW-- retirements of generation capacity" +ret(i,v,r) "--MW-- retirements of generation capacity" +rsc_dat_filt(i,r,sc_cat,rscbin) "--$/MW-- capital costs filtered for pumped-hydro so arbitrage value doesn't exceed capital costs" +storage_eff_filt(i) "--fraction-- storage efficiency filtered for the next solve year" +upgrade_to_filt(i,ii) "--set-- set linking upgrade techs to the tech the upgraded from filtered for existing upgrades" +; + +rfeas(r) = yes ; + +trange(t) = no ; +loop(t$[(yeart(t)>%start_year%)$(yeart(t)<=%next_year%)], +trange(t) = yes ; +) ; +trange("%next_year%") = no ; +trange("%cur_year%") = yes ; + +tcur(t) = no ; +tcur("%cur_year%") = yes ; + +tnext(t) = no ; +tnext("%next_year%") = yes ; + +*populate reduced-form sets +valcap_iv_filt(i,v) = sum{(r,t)$tcur(t), valcap(i,v,r,t)} ; +valcap_i_filt(i) = sum{v, valcap_iv_filt(i,v)} ; +valcap_ir_filt(i,r) = sum{t$tcur(t), valcap_irt(i,r,t)} ; + +*======================================= +* Removing banned technologies from sets +*======================================= + +csp_sm(i) = csp_sm(i)$(not ban(i)) ; +geo(i) = geo(i)$(not ban(i)) ; +hydro_d(i) = hydro_d(i)$(not ban(i)) ; +hydro_nd(i) = hydro_nd(i)$(not ban(i)) ; +nuclear(i) = nuclear(i)$(not ban(i)) ; +storage_duration(i) = storage_duration(i)$(not ban(i)) ; +storage_eff(i,t) = storage_eff(i,t)$(not ban(i)) ; +storage_standalone(i) = storage_standalone(i)$(not ban(i)) ; + +*============================== +* Get ReEDS generation capacity +*============================== + +cap_exist(i,v,r)$valcap_ivr(i,v,r) = sum{t$tcur(t), CAP.l(i,v,r,t) } ; +cap_exist_ir(i,r)$valcap_ir_filt(i,r) = sum{v, cap_exist(i,v,r) } ; +cap_exist_iv(i,v)$valcap_iv_filt(i,v) = sum{r, cap_exist(i,v,r) } ; +cap_exist_i(i)$valcap_i_filt(i) = sum{(r,v), cap_exist(i,v,r) } ; + +cap_ivrt(i,v,r,t)$([not (upv(i) or wind(i))]$valcap(i,v,r,t)$trange(t)) = CAP.l(i,v,r,t) ; +cap_energy_ivrt(i,v,r,t)$[valcap(i,v,r,t)$trange(t)$battery(i)] = CAP_ENERGY.l(i,v,r,t) ; +cap_ivrt(i,v,r,t)$([upv(i) or wind(i)]$valcap(i,v,r,t)) = + m_capacity_exog(i,v,r,t)$trange(t) + + sum{tt$[inv_cond(i,v,r,t,tt)$trange(tt)], + INV.l(i,v,r,tt) + INV_REFURB.l(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]} ; +cap_init(i,v,r)$([not distpv(i)]$valcap_ivr(i,v,r)) = sum{t$tcur(t), cap_ivrt(i,v,r,t)$initv(v) } ; +cap_init(i,v,r)$(distpv(i)$valcap_ivr(i,v,r)) = sum{t$tfirst(t), cap_ivrt(i,v,r,t) } ; +inv_ivrt(i,v,r,t)$[valcap(i,v,r,t)$trange(t)] = [INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)]$valinv(i,v,r,t) + UPGRADES.l(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades] ; +inv_energy_ivrt(i,v,r,t)$[valcap(i,v,r,t)$trange(t)$battery(i)] = INV_ENERGY.l(i,v,r,t); +inv_ivrt("distpv",v,r,t)$([trange(t)$(not tfirst(t))]$valcap("distpv",v,r,t)) = cap_ivrt("distpv",v,r,t) - sum{tt$tprev(t,tt), cap_ivrt("distpv",v,r,tt) } ; +inv_ivrt("distpv","init-1",r,"%next_year%") = inv_distpv(r,"%next_year%") ; + +ret_ivrt(i,v,r,t)$([trange(t)$(not tfirst(t))$newv(v)]$valcap(i,v,r,t)) = sum{tt$tprev(t,tt), cap_ivrt(i,v,r,tt)} - cap_ivrt(i,v,r,t) + inv_ivrt(i,v,r,t) ; +ret_ivrt(i,v,r,t)$([abs(ret_ivrt(i,v,r,t) < 1e-6)]$valcap(i,v,r,t)) = 0 ; + +ret(i,v,r)$valcap_ivr(i,v,r) = sum{t, ret_ivrt(i,v,r,t) } ; + +cap_exog_filt(i,v,r)$([not canada(i)]$valcap_ivr(i,v,r)) = sum{t$tnext(t), m_capacity_exog(i,v,r,t) } ; + +gen_h_stress_filt(i,r,allh,t)$[tcur(t)$valgen_irt(i,r,t)$h_stress_t(allh,t)] = + sum{v$valgen(i,v,r,t), GEN.l(i,v,r,allh,t)} +; +*============================ +* Fuel prices +*============================ + +fuel_price_filt(i,r)$cap_exist_ir(i,r) = sum{t$tcur(t), fuel_price(i,r,t) } ; + +* populate the fuel price for H2-CT/CC techs as the marginal off the +* hydrogen demand constraint (in $/[metric tons/hour]) divided by hours and +* times h2_combustion_intensity (metric tons / mmbtu) to get $ / mmbtu -- note there should +* always be a positive value here since if an H2-CT/CC is built it consumes hydrogen +* the equation from which we extract the marginal depends on whether +* we have the national (Sw_H2 = 1) or regional (Sw_H2 = 2) constraint +h2_usage_regional(r,h,t)$tcur(t) = + hours(h) * ( + h2_exogenous_demand_regional(r,'h2',h,t) + + sum{(i,v)$[valgen(i,v,r,t)$h2_combustion(i)], + GEN.l(i,v,r,h,t) * h2_combustion_intensity * heat_rate(i,v,r,t)} + ) +; + +fuel_price_filt(i,r)$[Sw_H2$h2_combustion(i)$(sum{t$tcur(t),yeart(t) } >= h2_demand_start)$cap_exist_ir(i,r)] = + sum{t$tcur(t), + (1 / cost_scale) * (1 / pvf_onm(t)) * h2_combustion_intensity * ( + eq_h2_demand.m('h2',t)$[Sw_H2=1] +* regional demand is now by hour, so calculate annual price as the weighted average of demand across hours + + (sum{h, eq_h2_demand_regional.m(r,h,t) / hours(h) * h2_usage_regional(r,h,t) } + / sum{h, h2_usage_regional(r,h,t) } + )$[(Sw_H2=2)$(sum{h, h2_usage_regional(r,h,t) })] + ) + } +; + +* for regions that consumed biomass, use the cost of the last supply curve bin consumed +repbioprice_filt(r)$[sum{(t, bioclass), bioused.l(bioclass,r,t) }] = + sum{t$tcur(t), smax{bioclass$[bioused.l(bioclass,r,t)], + sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"price")} } + bio_transport_cost } ; + +* for regions with no biomass, assign biomass price as the cost of the cheapest available supply curve bin for that region +* also safeguard against outlying values (for some reason smax sometimes returns -INF for regions w/o biomass consumption) +repbioprice_filt(r)$[(repbioprice_filt(r) <= 0)] = rep_bio_price_unused(r) ; + +repgasquant(cendiv,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 3)$tcur(t)] = + sum{(gb,h), GASUSED.l(cendiv,gb,h,t) * hours(h) } ; + +repgasquant(cendiv,t)$[(Sw_GasCurve = 1 or Sw_GasCurve = 2)$tcur(t)] = + sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], + hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) + } ; + +repgasprice(cendiv,t)$[(Sw_GasCurve = 0)$tcur(t)] = + smax{gb$[sum{h, GASUSED.l(cendiv,gb,h,t) }], gasprice(cendiv,gb,t) } ; + +repgasprice(cendiv,t)$[(Sw_GasCurve = 2)$tcur(t)$repgasquant(cendiv,t)] = + sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], + hours(h)*heat_rate(i,v,r,t)*fuel_price(i,r,t)*GEN.l(i,v,r,h,t) + } / (repgasquant(cendiv,t)) ; + +repgasprice_r(r,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 2)$tcur(t)] = sum{cendiv$r_cendiv(r,cendiv), repgasprice(cendiv,t) } ; + +repgasprice_r(r,t)$[(Sw_GasCurve = 1)$tcur(t)] = + ( sum{(h,cendiv), + gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * + hours(h) } / sum{h, hours(h) } + + + smax((fuelbin,cendiv)$[VGASBINQ_REGIONAL.l(fuelbin,cendiv,t)$r_cendiv(r,cendiv)], gasbinp_regional(fuelbin,cendiv,t) ) + + + smax(fuelbin$VGASBINQ_NATIONAL.l(fuelbin,t), gasbinp_national(fuelbin,t) ) + ) ; + +* catch any infinite values, assign to reference gas price +repgasprice_r(r,t)$[(repgasprice_r(r,t) = -inf or repgasprice_r(r,t) = inf)$tcur(t)] = + smax{cendiv$r_cendiv(r,cendiv), gasprice_ref(cendiv,t) } ; + +repgasprice_filt(r) = sum{t$tcur(t), repgasprice_r(r,t) } ; + +*============================ +* Filter necessary input data +*============================ + +avail_filt(i,v,r,szn)$[cap_exist_iv(i,v)$(not vre(i))] = + smax{h$h_szn(h,szn), avail(i,r,h) * derate_geo_vintage(i,v) } ; + +can_exports_h_filt(r,h) = sum{t$tcur(t), can_exports_h(r,h,t)} ; + +can_imports_cap(i,v,r)$canada(i) = sum{t$tcur(t), m_capacity_exog(i,v,r,t) } ; + +can_imports_szn_filt(r,szn) = sum{t$tcur(t), can_imports_szn(r,szn,t)} ; + +*can_exports_h_filt(r,h)$[Sw_Canada = 2] = 0 ; +*can_imports_cap(i,v,r)$[Sw_Canada = 2] = 0 ; +*can_imports_szn_filt(r,szn)$[Sw_Canada = 2] = 0 ; + +cap_hyd_szn_adj_filt(i,szn,r)$[cap_exist_ir(i,r)$hydro_d(i)] = cap_hyd_szn_adj(i,szn,r) ; + +cost_cap_filt(i,t)$[storage_standalone(i)] = cost_cap(i,t)$tnext(t) ; + +cost_cap_fin_mult_filt(i,r,t)$([storage_standalone(i)]) = cost_cap_fin_mult(i,r,t)$tnext(t) ; + +cost_vom_filt(i,v,r)$cap_exist(i,v,r) = sum{t$tcur(t), cost_vom(i,v,r,t) } ; + +cf_adj_t_filt(i,v,t)$[cap_exist_iv(i,v)$trange(t)] = cf_adj_t(i,v,t) ; +cf_adj_t_filt(i,v,"%next_year%") = cf_adj_t(i,v,"%next_year%")$(vre(i) or pvb(i)) ; + +ctt_i_ii_filt(i,ii) = ctt_i_ii(i,ii)$cap_exist_i(i) ; + +ctt_i_ii_psh(i,ii) = ctt_i_ii(i,ii)$[valcap_i_filt(i)$psh(i)] ; + +emit_rate_filt(e,i,v,r)$cap_exist(i,v,r) = sum{(t,etype)$tcur(t), emit_rate(etype,e,i,v,r,t) } ; + +heat_rate_filt(i,v,r)$cap_exist(i,v,r) = sum{t$tcur(t), heat_rate(i,v,r,t) } ; + +inv_cond_filt(i,v,t)$[(vre(i) or pvb(i))$tnext(t)] = sum{(tt,r), inv_cond(i,v,r,tt,t) } ; + +m_cf_filt(i,v,r,h)$[(vre(i) or pvb(i))$cap_exist(i,v,r)] = sum{t$tnext(t), m_cf(i,v,r,h,t) } ; + +m_cf_szn_filt(i,v,r,szn)$[hydro(i)$cap_exist(i,v,r)] = sum{t$tcur(t), m_cf_szn(i,v,r,szn,t) } ; + +minloadfrac_filt(r,i,szn)$[hydro(i)$cap_exist_ir(i,r)$szn_rep(szn)] = + sum{h$h_szn(h,szn), minloadfrac(r,i,h) * hours(h) } / sum{h$h_szn(h,szn), hours(h) } ; + +rsc_dat_filt(i,r,"cost",rscbin)$[storage_standalone(i)$cap_exist_ir(i,r)] = rsc_dat(i,r,"cost",rscbin) ; + + +storage_eff_filt(i)$storage(i) = sum{t$tnext(t), storage_eff(i,t) } ; + +upgrade_to_filt(i,ii) = upgrade_to(i,ii)$cap_exist_i(i) ; + +*============================ +* Get ReEDS transmission data +*============================ + +cap_trans_energy(r,rr,trtype) = sum{t$tcur(t), CAPTRAN_ENERGY.l(r,rr,trtype,t) } ; +cap_trans_prm(r,rr,trtype) = sum{t$tcur(t), CAPTRAN_PRM.l(r,rr,trtype,t) } ; + +cap_converter_filt(r) = sum{t$tcur(t), CAP_CONVERTER.l(r,t) } ; + +* In Augur, trtype="AC" includes everything except for VSC +routes_filt(r,rr,trtype) = sum{t$tcur(t), routes(r,rr,trtype,t) } ; + +*============================ +* Flexible load data +*============================ + +flex_load(r,h) = sum{(flex_type,t)$tcur(t), load_exog_flex(flex_type,r,h,t) } ; + +flex_load_opt(r,h) = sum{(flex_type,t)$tcur(t), FLEX.l(flex_type,r,h,t) } ; + +ra_cap_loadsite(r,t)$[Sw_LoadSiteCF$val_loadsite(r)] = CAP_LOADSITE.l(r,t) ; + +*============================ +* Extra consumption data +*============================ + +prod_filt(i,v,r,h)$[sum{t$tcur(t), valcap(i,v,r,t)}$consume(i)$hours(h)] = + sum{(p,t)$[i_p(i,p)$tcur(t)], PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) } ; + +*============================ +* Get ReEDS emissions prices [$/metric ton] +*============================ +* NOT included: eq_emit_rate_limit (disabled by default), eq_CSAPR_Budget, eq_CSAPR_Assurance +emissions_price(e,r) = + (1 / cost_scale) + * sum{t$tcur(t), + (1 / pvf_onm(t)) * eq_annual_cap.m(e,t) + + emit_tax(e,r,t) + } ; + +* Add marginal prices from CO2-specific constraints +emissions_price("CO2",r) = + emissions_price("CO2",r) + + (1 / cost_scale) + * sum{t$tcur(t), + (1 / pvf_onm(t)) * [ + eq_RGGI_cap.m(t)$RGGI_R(r) + + sum{st$r_st(r,st), eq_state_cap.m(st,t) } + ] + } ; + +*=================================== +* Get ReEDS energy prices ($/MWh) +*=================================== + +energy_price(r,h)$hours(h) = + sum{t$tcur(t), + (1 / cost_scale) * (1 / pvf_onm(t)) * eq_supply_demand_balance.m(r,h,t) / hours(h) } ; + +*======================================= +* Unload all relevant data to a gdx file +*======================================= + +execute_unload 'ReEDS_Augur%ds%augur_data%ds%reeds_data_%cur_year%.gdx' + avail_filt + bcr + bir_pvb_config + can_exports_h_filt + can_imports_cap + can_imports_szn_filt + cap_converter_filt + cap_exog_filt + cap_hyd_szn_adj_filt + cap_init + cap_ivrt + cap_energy_ivrt + cap_trans_energy + cap_trans_prm + cf_adj_t_filt + converter_efficiency_vsc + cost_cap_filt + cost_cap_fin_mult_filt + cost_vom_filt + csp_sm + ctt_i_ii_filt + ctt_i_ii_psh + degrade_annual + emissions_price + emit_rate_filt + energy_price + flex_load + flex_load_opt + fuel_price_filt + fuel2tech + gen_h_stress_filt + geo + h_szn + heat_rate_filt + hierarchy + hydro_d + hydro_nd + hours + hydmin + i + ilr + ilr_pvb_config + i_subsets + inv_cond_filt + inv_ivrt + inv_energy_ivrt + ivt_num + m_cf_filt + m_cf_szn_filt + maxage + minloadfrac_filt + notvsc + nuclear + prm + prod_filt + pvf_onm + r + rfeas + r_cendiv + ra_cap_loadsite + repbioprice_filt + repgasprice_filt + ret + ret_ivrt + routes_filt + rsc_dat_filt + sdbin + storage_duration + storage_eff + storage_eff_filt + storage_standalone + Sw_VSC + szn + tfirst + tmodel_new + tranloss + trtype + upgrade_to_filt + v + vom_hyd +; + + +*** dump data for tax credit phaseout calculations +execute_unload "outputs%ds%tc_phaseout_data%ds%emit_for_tc_phaseout_calc_%cur_year%.gdx" + emit_nat_tc, emit_r_tc +; \ No newline at end of file diff --git a/reeds/core/solve/solve.py b/reeds/core/solve/solve.py new file mode 100644 index 00000000..a9807fd1 --- /dev/null +++ b/reeds/core/solve/solve.py @@ -0,0 +1,177 @@ +#%% Imports +import os +import sys +import argparse +import pandas as pd +import subprocess +from glob import glob +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent.parent)) +import reeds +from reeds.resource_adequacy import Augur + + +#%% Main function +def run_reeds(casepath, t, onlygams=False, iteration=0): + """ + """ + # #%% Arguments for testing + # casepath = os.path.expanduser('~/github/ReEDS-2.0/runs/v20230512_prasM0_ERCOT') + # t = 2020 + # onlygams = 0 + # iteration = 0 + # os.chdir(casepath) + + #%% Get the run settings + sw = reeds.io.get_switches(casepath) + years = pd.read_csv( + os.path.join(casepath,'inputs_case','modeledyears.csv') + ).columns.astype(int).values + tprev = {**{years[0]:years[0]}, **dict(zip(years[1:], years))} + tnext = {**dict(zip(years, years[1:])), **{years[-1]:years[-1]}} + + #%%### Run GAMS LP + if not onlyaugur: + #%% Get the command to run GAMS for this solve year + batch_case = os.path.basename(casepath) + stress_year = f"{t}i{iteration}" + ### Get the restartfile (last iteration from previous year) + if t == min(years): + restartfile = batch_case + else: + restartfile = sorted( + glob(os.path.join(casepath,'g00files',f"{batch_case}_{tprev[t]}i*")) + )[-1] + + cmd_gams = reeds.parse.solvestring_sequential( + batch_case=batch_case, + caseSwitches=sw, + cur_year=t, + next_year=tnext[t], + prev_year=tprev[t], + stress_year=stress_year, + restartfile=restartfile, + hpc=int(sw['hpc']), + iteration=iteration, + ) + print(cmd_gams) + + ### Run GAMS LP + result = subprocess.run(cmd_gams, shell=True) + if result.returncode: + raise Exception(f'3_solve_oneyear.gms failed with return code {result.returncode}') + + #%% Add solve time to run metadata + try: + cmd_log = ( + f"python {os.path.join(casepath, 'reeds', 'log.py')}" + f" --year={t}\n" + ) + subprocess.run(cmd_log, shell=True) + except Exception as err: + print(err) + + #%% Check to see if the restart file exists + savefile = f"{batch_case}_{t}i{iteration}" + if not os.path.isfile(os.path.join("g00files", savefile+".g00")): + raise Exception(f"Missing {savefile}.g00") + + + #%%### Run Augur + if (not onlygams) and (tnext[t] > int(sw.GSw_SkipAugurYear)): + Augur.main(t=t, tnext=tnext[t], casedir=casepath, iteration=iteration) + + +#%% Driver function +def main(casepath, t, overwrite=False): + """ + """ + ### Get the run settings + sw = reeds.io.get_switches(casepath) + for iteration in range(int(sw.GSw_PRM_StressIterateMax)): + #%% If not overwriting, skip iterations that have already finished + if ( + (not overwrite) + ## Check if GAMS finished + and os.path.isfile( + os.path.join( + sw.casedir, 'g00files', + f"{os.path.basename(sw.casedir)}_{t}i{iteration}.g00")) + ## Check if the output of hourly_writetimeseries.py for this year/iteration + ## exists, indicating stress period calcluations finished (or that we're not + ## using stress periods) + and os.path.isfile( + os.path.join( + sw.casedir, 'inputs_case', f'stress{t}i{iteration+1}', 'cf_vre.csv')) + ## Check if Augur finished + and os.path.isfile( + os.path.join( + sw.casedir, 'ReEDS_Augur', 'augur_data', f'ReEDS_Augur_{t}.gdx')) + ): + print(f'Already ran {t}i{iteration} so continuing to next iteration') + continue + + #%% Run ReEDS and Augur + run_reeds(casepath, t, iteration=iteration) + + #%% Stop here if there's no stress period data for the next iteration + ### (either because we're not iterating or because the threshold was met) + if not os.path.isfile( + os.path.join( + sw.casedir, 'inputs_case', f'stress{t}i{iteration+1}', 'set_h.csv') + ): + print('No new stress periods to add, so moving to next solve year') + break + ### Otherwise continue iterating + else: + print(f'NEUE threshold was not met, so performing iteration {iteration+1}') + + ### Delete old restart files if desired + years = pd.read_csv( + os.path.join(casepath,'inputs_case','modeledyears.csv') + ).columns.astype(int).values + tprev = {**{years[0]:years[0]}, **dict(zip(years[1:], years))} + + if ((not int(sw['keep_g00_files'])) and (not int(sw['debug']))) and (min(years) < t): + g00files = glob(os.path.join(casepath, 'g00files', f'*{tprev[t]}i*.g00')) + for i in g00files: + os.remove(i) + + +#%% Procedure +if __name__ == '__main__': + #%% Argument inputs + import argparse + parser = argparse.ArgumentParser(description='Sequential ReEDS') + parser.add_argument('casepath', type=str, + help='path to ReEDS run folder') + parser.add_argument('t', type=int, + help='year to run') + parser.add_argument('--iteration', '-i', type=int, default=0, + help='iteration counter for this run') + parser.add_argument('--onlygams', '-g', action='store_true', + help='Only run GAMS (skip Augur)') + parser.add_argument('--onlyaugur', '-a', action='store_true', + help='Only run Augur (skip GAMS)') + parser.add_argument('--overwrite', '-o', action='store_true', + help='Overwrite iterations that have already finished') + + args = parser.parse_args() + casepath = args.casepath + t = args.t + iteration = args.iteration + onlygams = args.onlygams + onlyaugur = args.onlyaugur + overwrite = args.overwrite + + #%% Switch to run folder + os.chdir(casepath) + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(casepath,'gamslog.txt'), + ) + + #%% Run it + main(casepath=casepath, t=t, overwrite=overwrite) diff --git a/reeds/core/solve_pcm/solve_pcm.gms b/reeds/core/solve_pcm/solve_pcm.gms new file mode 100644 index 00000000..7742a76e --- /dev/null +++ b/reeds/core/solve_pcm/solve_pcm.gms @@ -0,0 +1,25 @@ +*** Reset years +tmodel(t) = no ; +tmodel("%cur_year%") = yes ; +set t_unfix(t) "year to unfix variables when rerunning a single solve year" ; +t_unfix("%cur_year%") = yes ; + +*** Activate PCM mode +Sw_PCM = 1 ; +Sw_MinCF = 0 ; + +*** Unfix the operational variables +$include reeds%ds%core%ds%solve_pcm%ds%unfix_op.gms + +*** Define the h- and szn-dependent parameters +$onMultiR +$include reeds%ds%core%ds%solve%ds%2_temporal_params.gms +$offMulti + +*** Solve it +solve ReEDSmodel minimizing Z using lp ; + +*** Abort if the solver returns an error +if (ReEDSmodel.modelStat > 1, + abort "Model did not solve to optimality", + ReEDSmodel.modelStat) ; diff --git a/reeds/core/solve_pcm/unfix_op.gms b/reeds/core/solve_pcm/unfix_op.gms new file mode 100644 index 00000000..8ef23ad9 --- /dev/null +++ b/reeds/core/solve_pcm/unfix_op.gms @@ -0,0 +1,115 @@ +* load +LOAD.lo(r,h,t_unfix) = 0 ; +LOAD.up(r,h,t_unfix) = +inf ; +FLEX.lo(flex_type,r,h,t_unfix)$Sw_EFS_flex = 0 ; +FLEX.up(flex_type,r,h,t_unfix)$Sw_EFS_flex = +inf ; +DROPPED.lo(r,h,t_unfix)$[(yeart(t_unfix)=h2_demand_start)] = 0 ; +CREDIT_H2PTC.up(i,v,r,h,t_unfix)$[valgen_h2ptc(i,v,r,t_unfix)$h_rep(h)$Sw_H2_PTC$h2_ptc_years(t_unfix)$(yeart(t_unfix)>=h2_demand_start)] = +inf ; + +* CO2 capture and storage +CO2_CAPTURED.lo(r,h,t_unfix)$Sw_CO2_Detail = 0 ; +CO2_CAPTURED.up(r,h,t_unfix)$Sw_CO2_Detail = +inf ; +CO2_STORED.lo(r,cs,h,t_unfix)$[Sw_CO2_Detail$r_cs(r,cs)] = 0 ; +CO2_STORED.up(r,cs,h,t_unfix)$[Sw_CO2_Detail$r_cs(r,cs)] = +inf ; +CO2_FLOW.lo(r,rr,h,t_unfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = 0 ; +CO2_FLOW.up(r,rr,h,t_unfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = +inf ; + +* Positive/negative variables +EMIT.lo(etype,e,r,t_unfix)$emit_modeled(e,r,t_unfix) = -inf ; +EMIT.up(etype,e,r,t_unfix)$emit_modeled(e,r,t_unfix) = +inf ; +STORAGE_INTERDAY_DISPATCH.lo(i,v,r,h,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = -inf ; +STORAGE_INTERDAY_DISPATCH.up(i,v,r,h,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = +inf ; +STORAGE_INTERDAY_LEVEL_MAX_DAY.lo(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = -inf ; +STORAGE_INTERDAY_LEVEL_MAX_DAY.up(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = +inf ; +STORAGE_INTERDAY_LEVEL_MIN_DAY.lo(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = -inf ; +STORAGE_INTERDAY_LEVEL_MIN_DAY.up(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = +inf ; diff --git a/reeds/core/terminus/dump_alldata.gms b/reeds/core/terminus/dump_alldata.gms new file mode 100644 index 00000000..cceef599 --- /dev/null +++ b/reeds/core/terminus/dump_alldata.gms @@ -0,0 +1,4 @@ +* turns on compression of gdx files +$setenv GDXCOMPRESS 1 +execute_unload "alldata.gdx" ; + diff --git a/reeds/core/terminus/powfrac_calc.gms b/reeds/core/terminus/powfrac_calc.gms new file mode 100644 index 00000000..3a4d9b92 --- /dev/null +++ b/reeds/core/terminus/powfrac_calc.gms @@ -0,0 +1,126 @@ + +* Author: Kelly Eurek +* Date: 2019/08/14 +* Source Bialek (1996). Tracing the flow of electricity + +* ====================== +* Calculate Flow Factors +* ====================== + +* --- calculate inflows, outflows, and flows between BAs --- + +parameter + flow_in(rr,allh,t) "--MW-- average flow of power into BA rr during time-slice h" + flow_out(r,allh,t) "--MW-- average flow of power out of BA r during time-slice h" + flow_ba2ba(r,rr,allh,t) "--MW-- average flow of power out of BA r into BA rr during time-slice h" +; + +flow_in(rr,h,t)$tmodel_new(t) = sum{(r,trtype)$routes(rr,r,trtype,t), FLOW.l(r,rr,h,t,trtype) } ; +flow_out(r,h,t)$tmodel_new(t) = sum{(rr,trtype)$routes(rr,r,trtype,t), FLOW.l(r,rr,h,t,trtype) } ; +flow_ba2ba(r,rr,h,t)$[tmodel_new(t)$sum{trtype,routes(rr,r,trtype,t) }] = + sum{(trtype)$[routes(rr,r,trtype,t)], FLOW.l(r,rr,h,t,trtype) } ; + +* --- calculate the "total load" --- + +Parameter totload(r,allh,t) "--MW-- load modified to include charging of storage and transmission losses" ; + +totload(r,h,t)$[tmodel_new(t)] = + load_exog(r,h,t) + can_exports_h(r,h,t) + + sum{(i,v)$[valcap(i,v,r,t)$(storage_standalone(i) or hyd_add_pump(i))], STORAGE_IN.l(i,v,r,h,t)} + + sum{(rr,trtype)$routes(rr,r,trtype,t), tranloss(rr,r,trtype) * FLOW.l(rr,r,h,t,trtype)} +; + +* --- calcultate power flowing through a balancing area --- + +Parameter flow_through(r,allh,t) "--MW-- flow through balancing area r during time-slice h: inflow + total generation" ; + +flow_through(r,h,t)$tmodel_new(t) = totload(r,h,t) + flow_out(r,h,t) + +* --- calculate total generation (including storage discharge) --- + +Parameter totgen(r,allh,t) "--MW-- total generation in region r during time-slice h" ; + +totgen(r,h,t)$tmodel_new(t) = + load_exog(r,h,t) + can_exports_h(r,h,t) + + + sum{(i,v)$[valcap(i,v,r,t)$(storage_standalone(i) or hyd_add_pump(i))], STORAGE_IN.l(i,v,r,h,t) } + + + sum{(rr,trtype)$routes(rr,r,trtype,t), + + FLOW.l(r,rr,h,t,trtype) + - FLOW.l(rr,r,h,t,trtype) + } + +; + +* --- define upstream and downstream power matricies --- + +Parameter A_upstream(rr,r,allh,t) "upstream power distribution matrix" ; +* see equation (4) in Bialek (1996) +A_upstream(rr,r,h,t)$tmodel_new(t) = 0 ; +A_upstream(r,r,h,t)$tmodel_new(t) = 1 ; +A_upstream(rr,r,h,t)$[(flow_ba2ba(r,rr,h,t)>0)$(flow_through(r,h,t)>0)$tmodel_new(t)] = - flow_ba2ba(r,rr,h,t) / flow_through(r,h,t) ; + +Parameter A_downstream(r,rr,allh,t) "downstream power distribution matrix" ; +* see equation (10) in Bialek (1996) +A_downstream(r,rr,h,t)$tmodel_new(t) = 0 ; +A_downstream(r,r,h,t)$tmodel_new(t) = 1 ; +A_downstream(r,rr,h,t)$[(flow_ba2ba(r,rr,h,t)>0)$(flow_through(rr,h,t)>0)$tmodel_new(t)] = -flow_ba2ba(r,rr,h,t) / flow_through(rr,h,t) ; + +* --- calculate the inverse of the upstream and downstream power matricies --- + +parameter + Ainv_upstream(r,rr,allh,t) "inverse of A_upstream" + Ainv_downstream(r,rr,allh,t) "inverse of A_downstream" + a(r,rr) temp matrix for A + ainv(r,rr) temp matrix for A-inverse +; + +Ainv_upstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; +Ainv_downstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; +a(r,rr) = 0 ; +ainv(r,rr) = 0 ; + +Loop((h,t)$[tmodel_new(t)$Sw_calc_powfrac], +a(rr,r) = A_upstream(rr,r,h,t) ; +execute_unload 'outputs%ds%gdxforinverse_%case%.gdx' r, a ; +execute 'invert outputs%ds%gdxforinverse_%case%.gdx r a outputs%ds%gdxfrominverse_%case%.gdx ainv >> outputs%ds%invert1_%case%.log' ; +execute_load 'outputs%ds%gdxfrominverse_%case%.gdx', ainv ; +Ainv_upstream(rr,r,h,t) = ainv(rr,r) ; +) ; + +Loop((h,t)$[tmodel_new(t)$Sw_calc_powfrac], +a(r,rr) = A_downstream(r,rr,h,t) ; +execute_unload 'outputs%ds%gdxforinverse_%case%.gdx' r, a ; +execute 'invert outputs%ds%gdxforinverse_%case%.gdx r a outputs%ds%gdxfrominverse_%case%.gdx ainv >> outputs%ds%invert2_%case%.log' ; +execute_load 'outputs%ds%gdxfrominverse_%case%.gdx', ainv ; +Ainv_downstream(r,rr,h,t) = ainv(r,rr) ; +) ; + +* --- remove gdx files that were created to do the inverse calculation --- +execute 'rm outputs%ds%gdxforinverse_%case%.gdx' ; +execute 'rm outputs%ds%gdxfrominverse_%case%.gdx' ; +execute 'rm outputs%ds%invert1_%case%.log' ; +execute 'rm outputs%ds%invert2_%case%.log' ; + +* --- calculate upsteram and downstream power fractions --- + +parameter + powerfrac_upstream(rr,r,allh,t) "--unitless-- power fraction upstream : fraction of power at BA rr that was generated at BA r during time-slice h" + powerfrac_downstream(r,rr,allh,t) "--unitless-- power fraction downstream: fraction of power generated at BA r that serves load at BA rr during time-slice h" +; + +powerfrac_upstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; +powerfrac_downstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; + +* see equation (6) in Bialek (1996) +if(Sw_calc_powfrac > 0, +powerfrac_upstream(rr,r,h,t)$[(flow_through(rr,h,t)>0)$tmodel_new(t)] = 1 / flow_through(rr,h,t) * Ainv_upstream(rr,r,h,t) * totgen(r,h,t) ; +powerfrac_upstream(rr,r,h,t)$[(powerfrac_upstream(rr,r,h,t)<1e-3)$tmodel_new(t)] = 0 ; + +* see equation (12) in Bialek (1996) +powerfrac_downstream(r,rr,h,t)$[(flow_through(r,h,t)>0)$tmodel_new(t)] = 1 / flow_through(r,h,t) * Ainv_downstream(r,rr,h,t) * totload(rr,h,t) ; +powerfrac_downstream(r,rr,h,t)$[(powerfrac_downstream(r,rr,h,t)<1e-3)$tmodel_new(t)] = 0 ; +) ; + +* --- write the outputs --- +execute_unload "outputs%ds%rep_powerfrac_%fname%.gdx" powerfrac_downstream, powerfrac_upstream ; diff --git a/reeds/core/terminus/report.gms b/reeds/core/terminus/report.gms new file mode 100644 index 00000000..ebd14d10 --- /dev/null +++ b/reeds/core/terminus/report.gms @@ -0,0 +1,2091 @@ +$setglobal ds \ + +$ifthen.unix %system.filesys% == UNIX +$setglobal ds / +$endif.unix + +$if not set case $setglobal case ref + + +sets +sys_costs / + inv_co2_network_pipe + inv_co2_network_spur + inv_converter_costs + inv_dac + inv_h2_pipeline + inv_h2_production + inv_h2_storage + inv_investment_capacity_costs + inv_investment_refurbishment_capacity + inv_investment_spurline_costs_rsc_technologies + inv_investment_water_access + inv_itc_payments_negative + inv_itc_payments_negative_refurbishments + inv_spurline_investment + inv_transmission_interzone_ac_investment + inv_transmission_interzone_dc_investment + inv_transmission_intrazone_investment + op_acp_compliance_costs + op_co2_incentive_negative + op_co2_network_fom_pipe + op_co2_network_fom_spur + op_co2_storage + op_co2_transport_storage + op_consume_fom + op_consume_vom + op_emissions_taxes + op_fom_costs + op_fuelcosts_objfn + op_h2combustion_fuel_costs + op_h2_fuel_costs + op_h2_revenue_exog + op_h2_transport + op_h2_transport_intrareg + op_h2_storage + op_h2_vom + op_h2_ptc_payments_negative + op_operating_reserve_costs + op_ptc_payments_negative + op_rect_fuel_costs + op_spurline_fom + op_startcost + op_transmission_fom + op_transmission_intrazone_fom + op_vom_costs +/, + +sys_costs_inv(sys_costs) / + inv_co2_network_pipe + inv_co2_network_spur + inv_converter_costs + inv_dac + inv_h2_pipeline + inv_h2_production + inv_h2_storage + inv_investment_capacity_costs + inv_investment_refurbishment_capacity + inv_investment_spurline_costs_rsc_technologies + inv_investment_water_access + inv_itc_payments_negative + inv_itc_payments_negative_refurbishments + inv_spurline_investment + inv_transmission_interzone_ac_investment + inv_transmission_interzone_dc_investment + inv_transmission_intrazone_investment +/, + +sys_costs_op(sys_costs) / + op_acp_compliance_costs + op_co2_incentive_negative + op_co2_network_fom_pipe + op_co2_network_fom_spur + op_co2_storage + op_co2_transport_storage + op_consume_fom + op_consume_vom + op_emissions_taxes + op_fom_costs + op_fuelcosts_objfn + op_h2combustion_fuel_costs + op_h2_fuel_costs + op_h2_revenue_exog + op_h2_transport + op_h2_transport_intrareg + op_h2_storage + op_h2_ptc_payments_negative + op_operating_reserve_costs + op_ptc_payments_negative + op_spurline_fom + op_startcost + op_transmission_fom + op_transmission_intrazone_fom + op_vom_costs +/, + +rev_cat "categories for renvenue streams" /load, res_marg, oper_res, rps, charge /, + +lcoe_cat "categories for LCOE calculation" /capcost, upgradecost, rsccost, fomcost, vomcost, gen / + +loadtype "categories for types of load" / end_use, dist_loss, trans_loss, stor_charge, h2_prod, h2_network, dac / + +h2_demand_type / "electricity", "cross-sector"/ + +; + +* Parameter definitions in the following file are read from e_report_params.csv +* and parsed in copy_files.py. +* All output parameters should be defined in e_report_params.csv. +$include e_report_params.gms + +* Restrict operational outputs to representative timeslices and seasons +h(h)$[not h_rep(h)] = no ; +szn(szn)$[not szn_rep(szn)] = no ; + +*================================================= +* -- CAPACITY ABOVE INTERCONNECTION QUEUE LIMIT -- +*================================================= + +cap_above_limit(tg,r,t)$tmodel_new(t) = CAP_ABOVE_LIM.l(tg,r,t) ; + +*===================== +* -- CO2 Reporting -- +*===================== + +CO2_CAPTURED_out(r,h,t)$tmodel_new(t) = CO2_CAPTURED.l(r,h,t) ; +CO2_CAPTURED_out_ann(r,t)$tmodel_new(t) = sum(h,hours(h) * CO2_CAPTURED.l(r,h,t) ); +CO2_STORED_out(r,cs,h,t)$[tmodel_new(t)$csfeas(cs)] = CO2_STORED.l(r,cs,h,t) ; +CO2_STORED_out_ann(r,cs,t)$[tmodel_new(t)$csfeas(cs)] = sum(h,hours(h) * CO2_STORED.l(r,cs,h,t) ); +CO2_TRANSPORT_INV_out(r,rr,t)$tmodel_new(t) = CO2_TRANSPORT_INV.l(r,rr,t) ; +CO2_SPURLINE_INV_out(r,cs,t)$[tmodel_new(t)$csfeas(cs)] = CO2_SPURLINE_INV.l(r,cs,t) ; + +CO2_FLOW_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) + CO2_FLOW.l(rr,r,h,t) ; +CO2_FLOW_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * (CO2_FLOW.l(r,rr,h,t) + CO2_FLOW.l(rr,r,h,t)) } ; + +CO2_FLOW_pos_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) ; +CO2_FLOW_pos_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * CO2_FLOW.l(r,rr,h,t) } ; + +CO2_FLOW_neg_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = -1 * CO2_FLOW.l(rr,r,h,t) ; +CO2_FLOW_neg_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = -1 * sum{h, hours(h) * CO2_FLOW.l(rr,r,h,t) } ; + +CO2_FLOW_net_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) - CO2_FLOW.l(rr,r,h,t) ; +CO2_FLOW_net_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * (CO2_FLOW.l(r,rr,h,t) - CO2_FLOW.l(rr,r,h,t)) } ; + +*========================= +* LCOE +*========================= + +avg_avail(i,v,r) = sum{h, hours(h) * avail(i,r,h) * derate_geo_vintage(i,v) } / 8760 ; +avg_cf(i,v,r,t)$[CAP.l(i,v,r,t)$(not rsc_i(i))] = + sum{h, GEN.l(i,v,r,h,t) * hours(h) } + / sum{h, + CAP.l(i,v,r,t) + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + * hours(h) } +; + +*non-rsc technologies do not face the grid supply curve +*and thus can be assigned to an individual bin + +*LCOE calculation is appropriate for sequential solve mode only where annual energy production is the same in every year. +*In inter-temporal modes this isn't the case and energy production should be discounted appropriately. + +lcoe(i,v,r,t,"bin1")$[(not rsc_i(i))$valcap_init(i,v,r,t)$ivt(i,v,t)$avg_avail(i,v,r)] = +* cost of capacity divided by generation + ((crf(t) * cost_cap_fin_mult(i,r,t) * cost_cap(i,t)$newv(v) + + cost_fom(i,v,r,t) + ) / (avg_avail(i,v,r) * 8760)) +*plus VOM costs + + cost_vom(i,v,r,t) +* plus fuel costs - assuming constant fuel prices here (model prices might be different) + + heat_rate(i,v,r,t) * fuel_price(i,r,t) +; + +gen_rsc(i,v,r,t)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)] = + sum{h, m_cf(i,v,r,h,t) * hours(h) } ; + +lcoe(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$gen_rsc(i,v,r,t)] = +* cost of capacity divided by generation + (crf(t) + * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) +* Spur-line costs embedded in supply curve for techs without explicitly-modeled spurlines + + m_rsc_dat(r,i,rscbin,"cost")$[newv(v)$(not spur_techs(i))] +* Spur-line costs assuming 1:1 ratio between gen cap and spur cap (i.e. no overbuilding) + + sum{x$[xfeas(x)$x_r(x,r)$spur_techs(i)], spurline_cost(x) * Sw_SpurCostMult} + ) + + cost_fom(i,v,r,t) + ) / gen_rsc(i,v,r,t) +*plus VOM costs + + cost_vom(i,v,r,t) +; + +lcoe_cf_act(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$rsc_i(i)] = lcoe(i,v,r,t,rscbin) ; +lcoe_cf_act(i,v,r,t,"bin1")$[(not rsc_i(i))$valcap_init(i,v,r,t)$ivt(i,v,t)$avg_cf(i,v,r,t)] = +* cost of capacity divided by generation + ((crf(t) * cost_cap_fin_mult(i,r,t) * cost_cap(i,t)$newv(v) + + cost_fom(i,v,r,t) + ) / (avg_cf(i,v,r,t) * 8760) + ) +*plus VOM costs + + cost_vom(i,v,r,t) +*plus fuel costs - assuming constant fuel prices here (model prices might be different) + + heat_rate(i,v,r,t) * fuel_price(i,r,t) +; + +lcoe_nopol(i,v,r,t,rscbin)$valcap_init(i,v,r,t) = lcoe(i,v,r,t,rscbin) ; +lcoe_nopol(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$gen_rsc(i,v,r,t)] = +* cost of capacity divided by generation + (crf(t) + * (cost_cap_fin_mult_noITC(i,r,t) * cost_cap(i,t) +* Spur-line costs embedded in supply curve for techs without explicitly-modeled spurlines + + m_rsc_dat(r,i,rscbin,"cost")$newv(v)$(not spur_techs(i))) +* Spur-line costs assuming 1:1 ratio between gen cap and spur cap (i.e. no overbuilding) + + sum{x$[xfeas(x)$x_r(x,r)$spur_techs(i)], spurline_cost(x) * Sw_SpurCostMult} + + cost_fom(i,v,r,t) + ) / gen_rsc(i,v,r,t) +*plus VOM costs + + cost_vom(i,v,r,t) +; + +lcoe_fullpol(i,v,r,t,rscbin)$valcap_init(i,v,r,t) = lcoe(i,v,r,t,rscbin) ; +lcoe_fullpol(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$gen_rsc(i,v,r,t)] = +* cost of capacity divided by generation + (crf(t) + * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) +* Spur-line costs embedded in supply curve for techs without explicitly-modeled spurlines + + m_rsc_dat(r,i,rscbin,"cost")$newv(v)$(not spur_techs(i))) +* Spur-line costs assuming 1:1 ratio between gen cap and spur cap (i.e. no overbuilding) + + sum{x$[xfeas(x)$x_r(x,r)$spur_techs(i)], spurline_cost(x) * Sw_SpurCostMult} + + cost_fom(i,v,r,t)) + / gen_rsc(i,v,r,t) +*plus VOM costs + + cost_vom(i,v,r,t) +; + +lcoe_built(i,r,t)$[ [sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], GEN.l(i,v,r,h,t) * hours(h) }] or + [sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], GEN.l(i,v,r,h,t) * hours(h) }] ] = + (crf(t) * ( + sum{v$valinv(i,v,r,t), + INV.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) ) } + + sum{v$[valinv(i,v,r,t)$battery(i)], + INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap_energy(i,t) ) } + + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], + UPGRADES.l(i,v,r,t) * (cost_upgrade(i,v,r,t) * cost_cap_fin_mult(i,r,t) ) } + + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)], + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) } + ) + + sum{v$valinv(i,v,r,t), cost_fom(i,v,r,t) * INV.l(i,v,r,t) } + + sum{v$[valinv(i,v,r,t)$battery(i)], cost_fom_energy(i,v,r,t) * INV_ENERGY.l(i,v,r,t) } + + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], cost_fom(i,v,r,t) * UPGRADES.l(i,v,r,t) } + + sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], (cost_vom(i,v,r,t)+ heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h) } + + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], (cost_vom(i,v,r,t)+ heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h) } + ) / (sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], GEN.l(i,v,r,h,t) * hours(h) } + + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], GEN.l(i,v,r,h,t) * hours(h) }) +; + +lcoe_built_nat(i,t)$[sum{(v,r)$valinv(i,v,r,t), INV.l(i,v,r,t) }] = + sum{r, lcoe_built(i,r,t) * sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) } } + / sum{(v,r)$valinv(i,v,r,t), INV.l(i,v,r,t) } ; + +lcoe_pieces("capcost",i,r,t)$tmodel_new(t) = + sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) ) } + + sum{v$[valinv(i,v,r,t)$battery(i)], INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap_energy(i,t) ) } ; + +lcoe_pieces("upgradecost",i,r,t)$tmodel_new(t) = + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], + cost_upgrade(i,v,r,t) * cost_cap_fin_mult(i,r,t) * UPGRADES.l(i,v,r,t) } + + sum{(v,rscbin)$allow_cap_up(i,v,r,rscbin,t), + cost_cap_fin_mult(i,r,t) * INV_CAP_UP.l(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } + + sum{(v,rscbin)$allow_ener_up(i,v,r,rscbin,t), + cost_cap_fin_mult(i,r,t) * INV_ENER_UP.l(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } ; + +lcoe_pieces("rsccost",i,r,t)$tmodel_new(t) = + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)], + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) } ; + +lcoe_pieces("fomcost",i,r,t)$tmodel_new(t) = + sum{v$valinv(i,v,r,t), cost_fom(i,v,r,t) * INV.l(i,v,r,t) } + + sum{v$[valinv(i,v,r,t)$battery(i)], cost_fom_energy(i,v,r,t) * INV_ENERGY.l(i,v,r,t) } + + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], cost_fom(i,v,r,t) * UPGRADES.l(i,v,r,t) } ; + +lcoe_pieces("vomcost",i,r,t)$tmodel_new(t) = + sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], + (cost_vom(i,v,r,t) + heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h) } + + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], + (cost_vom(i,v,r,t) + heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h)} ; + +lcoe_pieces("gen",i,r,t)$tmodel_new(t) = + sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], GEN.l(i,v,r,h,t) * hours(h) } + + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], GEN.l(i,v,r,h,t) * hours(h) } ; + +lcoe_pieces_nat(lcoe_cat,i,t)$tmodel_new(t) = sum{r, lcoe_pieces(lcoe_cat,i,r,t) } ; + +*======================================== +* REQUIREMENT PRICES AND QUANTITIES +*======================================== + +objfn_raw = z.l ; + +load_frac_rt(r,t)$sum{(rr,h), LOAD.l(rr,h,t) } = sum{h, hours(h) * LOAD.l(r,h,t) }/ sum{(rr,h), hours(h) * LOAD.l(rr,h,t) } ; + +*Load and operating reserve prices are $/MWh, and reserve margin price is $/MW/rep-day for +* capacity credit formulation and $/MW/stress-timeslice for stress period formulation. +reqt_price('load','na',r,h,t)$tmodel_new(t) = + (1 / cost_scale) * (1 / pvf_onm(t)) * eq_supply_demand_balance.m(r,h,t) / hours(h) ; + +reqt_price('oper_res',ortype,r,h,t)$tmodel_new(t) = + (1 / cost_scale) * (1 / pvf_onm(t)) * eq_OpRes_requirement.m(ortype,r,h,t) / hours(h) ; + +reqt_price('state_rps',RPSCat,r,'ann',t)$tmodel_new(t) = + (1 / cost_scale) * (1 / pvf_onm(t)) * sum{st$r_st(r,st), eq_REC_Requirement.m(RPSCat,st,t) } ; + +reqt_price('nat_gen','na',r,'ann',t)$tmodel_new(t) = + (1 / cost_scale) * (1 / pvf_onm(t)) * eq_national_gen.m(t) ; + +reqt_price('annual_cap',e,r,'ann',t)$tmodel_new(t) = + (1 / cost_scale) * (1 / pvf_onm(t)) * eq_annual_cap.m(e,t) ; + +* Capacity credit formulation ($/MW/rep-day) +reqt_price('res_marg','na',r,ccseason,t)$[Sw_PRM_CapCredit$tmodel_new(t)] = + eq_reserve_margin.m(r,ccseason,t) * (1 / cost_scale) * (1 / pvf_onm(t)) ; +* Stress period formulation ($/MW/stress-timeslice) +reqt_price('res_marg','na',r,allh,t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)$h_stress_t(allh,t)] = + eq_supply_demand_balance.m(r,allh,t) * (1 / cost_scale) * (1 / pvf_onm(t)) ; + +reqt_price('res_marg_ann','na',r,'ann',t)$tmodel_new(t) = +* Capacity credit formulation ($/MW-yr) + sum{ccseason, reqt_price('res_marg','na',r,ccseason,t) }$Sw_PRM_CapCredit +* Stress period formulation ($/MW-yr) + + sum{allh$h_stress_t(allh,t), reqt_price('res_marg','na',r,allh,t) }$(Sw_PRM_CapCredit=0) +; +*The marginal on the total load constraint, eq_loadcon is converted to $/MW-yr. +*We can't convert to $/MWh because stress periods have no hours. +reqt_price('eq_loadcon','na',r,allh,t)$[tmodel_new(t)$h_t(allh,t)] = + (1 / cost_scale) * (1 / pvf_onm(t)) * eq_loadcon.m(r,allh,t) ; + + +*Load and operating reserve quantities are MWh, and reserve margin quantity is MW +* Demand from production activities (H2 and DAC) doesn't count toward electricity demand +reqt_quant('load','na',r,h,t)$tmodel_new(t) = + hours(h) * ( + LOAD.l(r,h,t) + - sum{(p,i,v)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)$Sw_Prod], + PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) } + ) ; + +* Capacity credit formulation +reqt_quant('res_marg','na',r,ccseason,t)$[Sw_PRM_CapCredit$tmodel_new(t)] = + (peakdem_static_ccseason(r,ccseason,t) +* + PEAK_FLEX.l(r,ccseason,t) + ) * (1 + prm(r,t)) ; +* Stress period formulation +reqt_quant('res_marg','na',r,allh,t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)$h_stress_t(allh,t)] = + LOAD.l(r,allh,t) ; + +* Annual res_marg quantity is defined as the max requirement level in the year. +reqt_quant('res_marg_ann','na',r,'ann',t)$tmodel_new(t) = +* Capacity credit formulation + smax{ccseason, reqt_quant('res_marg','na',r,ccseason,t) }$Sw_PRM_CapCredit +* Stress period formulation + + smax{allh$h_stress_t(allh,t), reqt_quant('res_marg','na',r,allh,t) }$(Sw_PRM_CapCredit=0) +; + +reqt_quant('oper_res',ortype,r,h,t)$tmodel_new(t) = + hours(h) * ( + orperc(ortype,"or_load") * LOAD.l(r,h,t) + + orperc(ortype,"or_wind") * sum{(i,v)$[wind(i)$valgen(i,v,r,t)], + GEN.l(i,v,r,h,t) } + + orperc(ortype,"or_pv") * sum{(i,v)$[pv(i)$valcap(i,v,r,t)], + CAP.l(i,v,r,t) }$dayhours(h) + ) ; +reqt_quant('state_rps',RPSCat,r,'ann',t)$tmodel_new(t) = + sum{(st,h)$r_st_rps(r,st), RecPerc(RPSCat,st,t) * hours(h) *( + ( (LOAD.l(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] + - sum{v$valgen("distpv",v,r,t), GEN.l("distpv",v,r,h,t) }) * (1.0 - distloss) + )$(RecStyle(st,RPSCat)=0) + + + ( LOAD.l(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] + - sum{v$valgen("distpv",v,r,t), GEN.l("distpv",v,r,h,t) } + )$(RecStyle(st,RPSCat)=1) + + + ( sum{(i,v)$[valgen(i,v,r,t)$(not storage_standalone(i))], GEN.l(i,v,r,h,t) + - (distloss * GEN.l(i,v,r,h,t))$(distpv(i)) + - (STORAGE_IN_GRID.l(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] } + - can_exports_h(r,h,t)$[(Sw_Canada=1)$sameas(RPSCat,"CES")] + )$(RecStyle(st,RPSCat)=2) + )} ; + +reqt_quant('nat_gen','na',r,'ann',t)$tmodel_new(t) = + national_gen_frac(t) * ( +* if Sw_GenMandate = 1, then apply the fraction to the bus bar load + ( + sum{h, LOAD.l(r,h,t) * hours(h) } + + sum{(rr,h,trtype)$routes(rr,r,trtype,t), (tranloss(rr,r,trtype) * FLOW.l(rr,r,h,t,trtype) * hours(h)) } + )$[Sw_GenMandate = 1] + +* if Sw_GenMandate = 2, then apply the fraction to the end use load + + (sum{h, + hours(h) * + ( (LOAD.l(r,h,t) - can_exports_h(r,h,t)) * (1.0 - distloss) - sum{v$valgen("distpv",v,r,t), GEN.l("distpv",v,r,h,t) }) + })$[Sw_GenMandate = 2] + ) ; +reqt_quant('annual_cap',e,r,'ann',t)$tmodel_new(t) = emit_cap(e,t) * load_frac_rt(r,t) ; + +*We keep quantity of eq_loadcon in MW +reqt_quant('eq_loadcon','na',r,allh,t)$[tmodel_new(t)$h_t(allh,t)] = LOAD.l(r,allh,t) ; + +*System-wide quantities: +reqt_quant_sys('load','na',h,t)$tmodel_new(t) = sum{r, reqt_quant('load','na',r,h,t)} ; +reqt_quant_sys('oper_res',ortype,h,t)$tmodel_new(t) = sum{r, reqt_quant('oper_res',ortype,r,h,t)} ; +reqt_quant_sys('state_rps',RPSCat,'ann',t)$tmodel_new(t) = sum{r, reqt_quant('state_rps',RPSCat,r,'ann',t)} ; +reqt_quant_sys('nat_gen','na','ann',t)$tmodel_new(t) = sum{r, reqt_quant('nat_gen','na',r,'ann',t)} ; +reqt_quant_sys('annual_cap',e,'ann',t)$tmodel_new(t) = sum{r, reqt_quant('annual_cap',e,r,'ann',t)} ; +reqt_quant_sys('res_marg','na',ccseason,t)$[Sw_PRM_CapCredit$tmodel_new(t)] = + sum{r, reqt_quant('res_marg','na',r,ccseason,t)} ; +reqt_quant_sys('res_marg','na',allh,t)$[(Sw_PRM_CapCredit=0)$h_stress_t(allh,t)$tmodel_new(t)] = + sum{r, reqt_quant('res_marg','na',r,allh,t)} ; +reqt_quant_sys('res_marg_ann','na','ann',t)$tmodel_new(t) = sum{r, reqt_quant('res_marg_ann','na',r,'ann',t)} ; + +*System-wide average prices: +reqt_price_sys('load','na',h,t)$reqt_quant_sys('load','na',h,t) = + sum{r, reqt_price('load','na',r,h,t) * reqt_quant('load','na',r,h,t)}/ + reqt_quant_sys('load','na',h,t) ; + +reqt_price_sys('oper_res',ortype,h,t)$reqt_quant_sys('oper_res',ortype,h,t) = + sum{r, reqt_price('oper_res',ortype,r,h,t) * reqt_quant('oper_res',ortype,r,h,t)}/ + reqt_quant_sys('oper_res',ortype,h,t) ; + +reqt_price_sys('state_rps',RPSCat,'ann',t)$reqt_quant_sys('state_rps',RPSCat,'ann',t) = + sum{r, reqt_price('state_rps',RPSCat,r,'ann',t) * reqt_quant('state_rps',RPSCat,r,'ann',t)}/ + reqt_quant_sys('state_rps',RPSCat,'ann',t) ; + +reqt_price_sys('nat_gen','na','ann',t)$reqt_quant_sys('nat_gen','na','ann',t) = + sum{r, reqt_price('nat_gen','na',r,'ann',t) * reqt_quant('nat_gen','na',r,'ann',t)}/ + reqt_quant_sys('nat_gen','na','ann',t) ; + +reqt_price_sys('annual_cap',e,'ann',t)$reqt_quant_sys('annual_cap',e,'ann',t) = + sum{r, reqt_price('annual_cap',e,r,'ann',t) * reqt_quant('annual_cap',e,r,'ann',t)}/ + reqt_quant_sys('annual_cap',e,'ann',t) ; + +reqt_price_sys('res_marg','na',ccseason,t)$[Sw_PRM_CapCredit$reqt_quant_sys('res_marg','na',ccseason,t)] = + sum{r, reqt_price('res_marg','na',r,ccseason,t) * reqt_quant('res_marg','na',r,ccseason,t)}/ + reqt_quant_sys('res_marg','na',ccseason,t) ; +reqt_price_sys('res_marg','na',allh,t)$[(Sw_PRM_CapCredit=0)$h_stress_t(allh,t)$reqt_quant_sys('res_marg','na',allh,t)] = + sum{r, reqt_price('res_marg','na',r,allh,t) * reqt_quant('res_marg','na',r,allh,t)}/ + reqt_quant_sys('res_marg','na',allh,t) ; + +reqt_price_sys('res_marg_ann','na','ann',t)$reqt_quant_sys('res_marg_ann','na','ann',t) = + sum{r, reqt_price('res_marg_ann','na',r,'ann',t) * reqt_quant('res_marg_ann','na',r,'ann',t)}/ + reqt_quant_sys('res_marg_ann','na','ann',t) ; + +load_rt(r,t)$tmodel_new(t) = sum{h, hours(h) * load_exog(r,h,t) } ; + +load_stress(r,allh,t)$[tmodel_new(t)$h_stress_t(allh,t)] = LOAD.l(r,allh,t) ; + +co2_price(t)$tmodel_new(t) = (1 / cost_scale) * (1 / pvf_onm(t)) * eq_annual_cap.m("CO2",t) ; + +rggi_price(t)$tmodel_new(t) = (1 / cost_scale) * (1 / pvf_onm(t)) * eq_RGGI_cap.m(t) ; +rggi_quant(t)$tmodel_new(t) = RGGI_cap(t) ; + +state_cap_and_trade_price(st,t)$tmodel_new(t) = + (1 / cost_scale) * (1 / pvf_onm(t)) + * eq_state_cap.m(st,t) ; + +state_cap_and_trade_quant(st,t)$tmodel_new(t) = + state_cap(st,t) ; + +tran_hurdle_cost_ann(r,rr,trtype,t)$[tmodel_new(t)$routes(r,rr,trtype,t)$cost_hurdle(r,rr,t)] = + sum{h, hours(h) * cost_hurdle(r,rr,t) * FLOW.l(r,rr,h,t,trtype) } ; + +*======================================== +* RPS, CES, AND TAX CREDIT OUTPUTS +*======================================== + +rec_outputs(RPSCat,i,st,ast,t)$[stfeas(st)$(stfeas(ast) or sameas(ast,"voluntary"))$tmodel_new(t)] = RECS.l(RPSCat,i,st,ast,t) ; +acp_purchases_out(rpscat,st,t) = ACP_PURCHASES.l(RPSCat,st,t) ; +ptc_out(i,v,t)$[tmodel_new(t)$ptc_value_scaled(i,v,t)] = ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t) ; + +*======================================== +* FUEL PRICES AND QUANTITIES +*======================================== + +* The marginal biomass fuel price is derived from the linear program constraint marginals +* Case 1: the resource of a biomass class is NOT exhausted, i.e., BIOUSED.l(bioclass) < biosupply(bioclass) +* Marginal Biomass Price = eq_bioused.m +* Case 2: the resource of one or more biomass classes ARE exhausted, i.e., BIOUSED.l(bioclass) = biosupply(bioclass) +* Marginal Biomass Price = maximum difference between eq_bioused.m and eq_biousedlimit.m(bioclass) across all biomass classes in a region + +repbioprice(r,t)$tmodel_new(t) = max{0, smax{bioclass$BIOUSED.l(bioclass,r,t), eq_bioused.m(r,t) - + sum{usda_region$r_usda(r,usda_region), eq_biousedlimit.m(bioclass,usda_region,t) } } } / pvf_onm(t) ; + +* quantity of biomass used (convert from mmBTU to dry tons using biomass energy content) +bioused_out(bioclass,r,t)$tmodel_new(t) = BIOUSED.l(bioclass,r,t) / bio_energy_content ; +bioused_usda(bioclass,usda_region,t)$tmodel_new(t) = sum{r$r_usda(r,usda_region), bioused_out(bioclass,r,t) } ; + +* 1e9 converts from MMBtu to Quads +repgasquant(cendiv,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 3)$tmodel_new(t)] = + sum{(gb,h), GASUSED.l(cendiv,gb,h,t) * hours(h) } * gas_scale/ 1e9 ; + +repgasquant(cendiv,t)$[(Sw_GasCurve = 1 or Sw_GasCurve = 2)$tmodel_new(t)] = + ( sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], + hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t)} + + sum{(v,r,h)$[valcap("dac_gas",v,r,t)$r_cendiv(r,cendiv)], + hours(h) * dac_gas_cons_rate("dac_gas",v,t) * PRODUCE.l("DAC","dac_gas",v,r,h,t) }$Sw_DAC_Gas + + sum{(p,i,v,r,h)$[r_cendiv(r,cendiv)$valcap(i,v,r,t)$smr(i)], + hours(h) * smr_methane_rate * PRODUCE.l(p,i,v,r,h,t) }$Sw_H2 + ) / 1e9 ; + +repgasquant_irt(i,r,t)$tmodel_new(t) = + ( sum{(v,h)$[valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], + hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } + + sum{(v,h)$[valcap("dac_gas",v,r,t)], + hours(h) * dac_gas_cons_rate("dac_gas",v,t) * PRODUCE.l("DAC","dac_gas",v,r,h,t) }$Sw_DAC_Gas + + sum{(p,v,h)$[valcap(i,v,r,t)$smr(i)], + hours(h) * smr_methane_rate * PRODUCE.l(p,i,v,r,h,t) }$Sw_H2 + ) / 1e9 ; + +repgasquant_nat(t)$tmodel_new(t) = sum{cendiv, repgasquant(cendiv,t) } ; + +*for reported gasprice (not that used to compute system costs) +*scale back to $ / mmbtu +repgasprice(cendiv,t)$[(Sw_GasCurve = 0)$tmodel_new(t)$repgasquant(cendiv,t)] = + smax{gb$[sum{h, GASUSED.l(cendiv,gb,h,t) }], gasprice(cendiv,gb,t) } / gas_scale ; + +repgasprice(cendiv,t)$[(Sw_GasCurve = 2)$tmodel_new(t)$repgasquant(cendiv,t)] = + sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], + hours(h)*heat_rate(i,v,r,t)*fuel_price(i,r,t)*GEN.l(i,v,r,h,t) + } / (repgasquant(cendiv,t) * 1e9) ; + +repgasprice_r(r,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 2)$tmodel_new(t)] = sum{cendiv$r_cendiv(r,cendiv), repgasprice(cendiv,t) } ; + +repgasprice_r(r,t)$[(Sw_GasCurve = 1)$tmodel_new(t)] = + ( sum{(h,cendiv), + gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * + hours(h) } / sum{h, hours(h) } + + + smax((fuelbin,cendiv)$[VGASBINQ_REGIONAL.l(fuelbin,cendiv,t)$r_cendiv(r,cendiv)], gasbinp_regional(fuelbin,cendiv,t) ) + + + smax(fuelbin$VGASBINQ_NATIONAL.l(fuelbin,t), gasbinp_national(fuelbin,t) ) + ) ; + +repgasprice(cendiv,t)$[(Sw_GasCurve = 1)$tmodel_new(t)$repgasquant(cendiv,t)] = + sum{(i,r)$r_cendiv(r,cendiv), repgasprice_r(r,t) * repgasquant_irt(i,r,t) } / repgasquant(cendiv,t) ; + +repgasprice_nat(t)$[tmodel_new(t)$sum{cendiv, repgasquant(cendiv,t) }] = + sum{cendiv, repgasprice(cendiv,t) * repgasquant(cendiv,t) } + / sum{cendiv, repgasquant(cendiv,t) } ; + +*======================================== +* NATURAL GAS FUEL COSTS +*======================================== + +gasshare_ba(r,cendiv,t)$[r_cendiv(r,cendiv)$tmodel_new(t)$repgasquant(cendiv,t)] = + sum{i$[valgen_irt(i,r,t)$gas(i)],repgasquant_irt(i,r,t) / repgasquant(cendiv,t) } ; + +gasshare_techba(i,r,cendiv,t)$[r_cendiv(r,cendiv)$tmodel_new(t)$repgasquant(cendiv,t)$gas(i)] = + repgasquant_irt(i,r,t) / repgasquant(cendiv,t) ; + +gasshare_cendiv(cendiv,t)$[sum{cendiv2,repgasquant(cendiv2,t)}] = repgasquant(cendiv,t) / sum{cendiv2,repgasquant(cendiv2,t)} ; + +gascost_cendiv(cendiv,t)$tmodel_new(t) = +*cost of natural gas for Sw_GasCurve = 2 (static natural gas prices) + + sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t) + $[not bio(i)]$[not cofire(i)]$[Sw_GasCurve = 2]], + hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN.l(i,v,r,h,t) } + +*cost of natural gas for Sw_GasCurve = 0 (census division supply curves natural gas prices) + + sum{gb, sum{h,hours(h) * GASUSED.l(cendiv,gb,h,t) } * gasprice(cendiv,gb,t) + }$[Sw_GasCurve = 0] + +*cost of natural gas for Sw_GasCurve = 3 (national supply curve for natural gas prices with census division multipliers) + + sum{(h,gb), hours(h) * GASUSED.l(cendiv,gb,h,t) + * gasadder_cd(cendiv,t,h) + gasprice_nat_bin(gb,t) + }$[Sw_GasCurve = 3] +*cost of natural gas for Sw_GasCurve = 1 (national and census division supply curves for natural gas prices) +*first - anticipated costs of gas consumption given last year's amount + + (sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)], + gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * + hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } +*second - adjustments based on changes from last year's consumption at the regional and national level + + sum{(fuelbin), + gasbinp_regional(fuelbin,cendiv,t) * VGASBINQ_REGIONAL.l(fuelbin,cendiv,t) } + + + sum{(fuelbin), + gasbinp_national(fuelbin,t) * VGASBINQ_NATIONAL.l(fuelbin,t) } * gasshare_cendiv(cendiv,t) + + )$[Sw_GasCurve = 1]; + +*======================================== +* BIOFUEL COSTS +*======================================== + +bioshare_techba(i,r,t)$[(cofire(i) or bio(i))$tmodel_new(t)] = +* biofuel-based generation of tech i in the BA (biopower + cofire) + (( sum{(v,h)$[valgen(i,v,r,t)$bio(i)], hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } + + sum{(v,h)$[cofire(i)$valgen(i,v,r,t)], bio_cofire_perc * hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } + ) / +* biofuel-based generation of all techs in the BA (biopower + cofire) + ( sum{(ii,v,h)$[valgen(ii,v,r,t)$bio(ii)], hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } + + sum{(ii,v,h)$[cofire(ii)$valgen(ii,v,r,t)], bio_cofire_perc * hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } + ) + )$[ sum{(ii,v,h)$[valgen(ii,v,r,t)$bio(ii)], hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } + + sum{(ii,v,h)$[cofire(ii)$valgen(ii,v,r,t)], bio_cofire_perc * hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } + ] +; + +*========================= +* GENERATION +*========================= + +* Calculate generation and include charging, pumping, and production as negative values +gen_h(i,r,h,t)$[tmodel_new(t)$valgen_irt(i,r,t)] = + sum{v$valgen(i,v,r,t), GEN.l(i,v,r,h,t) +* less storage charging + - STORAGE_IN.l(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)]} +* less load from hydrogen production + - sum{(v,p)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)], PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t)}$Sw_Prod +; +* A small amount of upv capacity is actually csp-ns, so convert it back now. +* UPV capacity is already in MWac at this point (matching csp-ns), +* so don't need to account for ILR. +gen_h("csp-ns",r,h,t)$[cap_cspns(r,t)$tmodel_new(t)] + = cap_cspns(r,t) * m_cf("upv_6","new1",r,h,t) ; +* We have to take csp-ns generation from somewhere, so take it from upv_6 (which all the +* csp-ns-containing regions have) +gen_h("upv_6",r,h,t)$[cap_cspns(r,t)$tmodel_new(t)] + = gen_h("upv_6",r,h,t) - gen_h("csp-ns",r,h,t) ; +* Make sure it doesn't go negative, just in case +gen_h("upv_6",r,h,t)$[cap_cspns(r,t)$tmodel_new(t)$(gen_h("upv_6",r,h,t) < 0)] = 0 ; +gen_h_nat(i,h,t)$tmodel_new(t) = sum{r, gen_h(i,r,h,t) } ; + +* Do it again for stress periods +gen_h_stress(i,r,allh,t)$[tmodel_new(t)$valgen_irt(i,r,t)$h_stress_t(allh,t)] = + sum{v$valgen(i,v,r,t), GEN.l(i,v,r,allh,t) +* less storage charging + - STORAGE_IN.l(i,v,r,allh,t)$[storage_standalone(i) or hyd_add_pump(i)] } +* less load from hydrogen production + - sum{(v,p)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)], + PRODUCE.l(p,i,v,r,allh,t) / prod_conversion_rate(i,v,r,t)}$Sw_Prod +; +gen_h_stress_nat(i,allh,t)$[tmodel_new(t)$h_stress_t(allh,t)] = sum{r, gen_h_stress(i,r,allh,t) } ; + +gen_ann(i,r,t)$tmodel_new(t) = sum{h, gen_h(i,r,h,t) * hours(h) } ; +gen_ann_nat(i,t)$tmodel_new(t) = sum{r, gen_ann(i,r,t) } ; + +* Report generation without the charging and production included as above +gen_ivrt(i,v,r,t)$valgen(i,v,r,t) = sum{h, GEN.l(i,v,r,h,t) * hours(h) } ; +gen_ivrt_uncurt(i,v,r,t)$[(vre(i) or storage_hybrid(i)$(not csp(i)))$valgen(i,v,r,t)] = + sum{h, m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; + +* Report generation that will be used as a denominator in outputs, where VRE uses uncurtailed gen and storage uses GEN +gen_uncurtailed(i,r,t)$[valgen_irt(i,r,t)$(not vre(i))] = sum{v, gen_ivrt(i,v,r,t) } ; +gen_uncurtailed(i,r,t)$[valgen_irt(i,r,t)$vre(i)] = sum{v, gen_ivrt_uncurt(i,v,r,t) } ; +gen_uncurtailed_nat(i,t)$tmodel_new(t) = sum{r, gen_uncurtailed(i,r,t) } ; + +* Storage outputs +stor_inout(i,v,r,t,"in")$[valgen(i,v,r,t)$storage(i)$[not storage_hybrid(i)$(not csp(i))]] = sum{h, STORAGE_IN.l(i,v,r,h,t) * hours(h) } ; +stor_inout(i,v,r,t,"out")$[valgen(i,v,r,t)$storage(i)] = gen_ivrt(i,v,r,t) ; +stor_in(i,v,r,h,t)$[storage(i)$valgen(i,v,r,t)$(not storage_hybrid(i)$(not csp(i)))] = STORAGE_IN.l(i,v,r,h,t) ; +stor_out(i,v,r,h,t)$[storage(i)$valgen(i,v,r,t)] = GEN.l(i,v,r,h,t) ; +stor_level(i,v,r,h,t)$[valgen(i,v,r,t)$storage(i)] = STORAGE_LEVEL.l(i,v,r,h,t) ; +stor_interday_level(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL.l(i,v,r,allszn,t) ; +stor_interday_dispatch(i,v,r,h,t)$[valgen(i,v,r,t)$storage_interday(i)] = STORAGE_INTERDAY_DISPATCH.l(i,v,r,h,t) ; + +*===================================================================== +* WATER ACCOUNTING, CAPACITY, NEW CAPACITY, AND RETIRED CAPACITY +*===================================================================== +water_withdrawal_ivrt(i,v,r,t)$valgen(i,v,r,t) = sum{h$valgen(i,v,r,t), WAT.l(i,v,"with",r,h,t) } ; +water_consumption_ivrt(i,v,r,t)$valgen(i,v,r,t) = sum{h$valgen(i,v,r,t), WAT.l(i,v,"cons",r,h,t) } ; + +watcap_ivrt(i,v,r,t)$valcap(i,v,r,t) = WATCAP.l(i,v,r,t) ; +watcap_out(i,r,t)$valcap_irt(i,r,t) = sum{v$valcap(i,v,r,t), WATCAP.l(i,v,r,t) } ; +watcap_new_out(i,r,t)$[valcap_irt(i,r,t)$i_water_cooling(i)] = + sum{h$h_rep(h), + hours(h) + * sum{w$[i_w(i,w)], + water_rate(i,w) } + * ( sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)} + + sum{v$valcap(i,v,r,t), + (1-upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))}$[upgrade(i)$Sw_Upgrades] ) + * (1 + sum{(v,szn), h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + } / 1E6 + + sum{v$[psh(i)$valinv(i,v,r,t)], WATCAP.l(i,v,r,t)} ; + +watcap_new_ivrt(i,v,r,t)$[valcap(i,v,r,t)$i_water_cooling(i)] = + sum{h$h_rep(h), + hours(h) + * sum{w$[i_w(i,w)], + water_rate(i,w) } + * ( [INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)]$valinv(i,v,r,t) + + [(1-upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))]$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades] ) + * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) + } / 1E6 + + WATCAP.l(i,v,r,t)$psh(i) ; + +watcap_new_ann_out(i,v,r,t)$watcap_new_ivrt(i,v,r,t) = watcap_new_ivrt(i,v,r,t) / (yeart(t) - sum(tt$tprev(t,tt), yeart(tt))) ; + +* --- Water Capacity Retirements ---* +watret_out(i,r,t)$[(not tfirst(t))] = sum{tt$tprev(t,tt), watcap_out(i,r,tt)} - watcap_out(i,r,t) + watcap_new_out(i,r,t) ; +watret_out(i,r,t)$[abs(watret_out(i,r,t)) < 1e-6] = 0 ; + +watret_ivrt(i,v,r,t)$[(not tfirst(t))] = sum{tt$tprev(t,tt), watcap_ivrt(i,v,r,tt)} - watcap_ivrt(i,v,r,t) + watcap_new_ivrt(i,v,r,t) ; +watret_ivrt(i,v,r,t)$[(abs(watret_ivrt(i,v,r,t)) < 1e-6)] = 0 ; + +watret_ann_out(i,v,r,t)$watret_ivrt(i,v,r,t) = watret_ivrt(i,v,r,t) / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; + +*========================= +* Operating Reserves +*========================= + +opres_supply_h(ortype,i,r,h,t)$[tmodel_new(t)$reserve_frac(i,ortype)] = + sum{v, OPRES.l(ortype,i,v,r,h,t) } ; + + +opres_supply(ortype,i,r,t)$[tmodel_new(t)$reserve_frac(i,ortype)] = + sum{h, hours(h) * opRes_supply_h(ortype,i,r,h,t) } ; + +* total opres trade +opres_trade(ortype,r,rr,t)$[opres_routes(r,rr,t)$tmodel_new(t)] = + sum{h, hours(h) * OPRES_FLOW.l(ortype,r,rr,h,t) } ; + +*========================= +* LOSSES AND CURTAILMENT +*========================= + +gen_new_uncurt(i,r,h,t)$[(vre(i) or storage_hybrid(i)$(not csp(i)))$valcap_irt(i,r,t)] = + sum{v$valinv(i,v,r,t), (INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)) * m_cf(i,v,r,h,t) * hours(h) } +; + +* Formulation follows eq_curt_gen_balance(r,h,t); since it uses =g= there may be extra curtailment +* beyond CURT.l(r,h,t) so we recalculate as (availability - generation - operating reserves) +curt_h(r,h,t)$tmodel_new(t) = + sum{(i,v)$[valcap(i,v,r,t)$(vre(i) or storage_hybrid(i)$(not csp(i)))], + m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) } + - sum{(i,v)$[valgen(i,v,r,t)$vre(i)], GEN.l(i,v,r,h,t) } + - sum{(i,v)$[valgen(i,v,r,t)$storage_hybrid(i)$(not csp(i))], GEN_PLANT.l(i,v,r,h,t) }$Sw_HybridPlant + - sum{(ortype,i,v)$[Sw_OpRes$opres_h(h)$reserve_frac(i,ortype)$valgen(i,v,r,t)$vre(i)], + OPRES.l(ortype,i,v,r,h,t) } +; + +curt_ann(r,t)$tmodel_new(t) = sum{h, curt_h(r,h,t) * hours(h) } ; + +curt_tech(i,r,t)$[tmodel_new(t)$vre(i)] = + sum{(v,h)$valcap(i,v,r,t), + m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } + - sum{(v,h)$valgen(i,v,r,t), + GEN.l(i,v,r,h,t) * hours(h) } + - sum{(ortype,v,h)$[Sw_OpRes$opres_h(h)$reserve_frac(i,ortype)$valgen(i,v,r,t)], + OPRES.l(ortype,i,v,r,h,t) * hours(h) } +; + +curt_rate_tech(i,r,t)$[tmodel_new(t)$vre(i)$(gen_ann(i,r,t) + curt_tech(i,r,t))] = + curt_tech(i,r,t) / (gen_ann(i,r,t) + curt_tech(i,r,t)) +; + +curt_rate(t) + $[tmodel_new(t) + $(sum{(i,r)$[vre(i) or storage_hybrid(i)$(not csp(i))], gen_ann(i,r,t) } + sum{r, curt_ann(r,t) })] + = sum{r, curt_ann(r,t) } + / (sum{(i,r)$[vre(i) or storage_hybrid(i)$(not csp(i))], gen_ann(i,r,t) } + sum{r, curt_ann(r,t) }) ; + +losses_ann('storage',t)$tmodel_new(t) = sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)], STORAGE_IN.l(i,v,r,h,t) * hours(h) } + - sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)], GEN.l(i,v,r,h,t) * hours(h) } ; + +losses_ann('trans',t)$tmodel_new(t) = + sum{(rr,r,h,trtype)$routes(rr,r,trtype,t), + (tranloss(rr,r,trtype) * FLOW.l(rr,r,h,t,trtype) * hours(h)) + + ((CONVERSION.l(r,h,"AC","VSC",t) + CONVERSION.l(r,h,"VSC","AC",t))* (1 - converter_efficiency_vsc) * hours(h))$[val_converter(r,t)$Sw_VSC] + } ; + +losses_ann('curt',t)$tmodel_new(t) = sum{r, curt_ann(r,t) } ; + +losses_ann('load',t)$tmodel_new(t) = sum{(r,h), LOAD.l(r,h,t) * hours(h) } ; + +losses_tran_h(rr,r,h,trtype,t)$[routes(r,rr,trtype,t)$tmodel_new(t)] + = tranloss(rr,r,trtype) * FLOW.l(rr,r,h,t,trtype) + + ((CONVERSION.l(r,h,"AC","VSC",t) + CONVERSION.l(r,h,"VSC","AC",t))* (1 - converter_efficiency_vsc))$[val_converter(r,t)$Sw_VSC] ; + +*========================= +* CAPACITY +*========================= + +cap_deg_ivrt(i,v,r,t)$valcap(i,v,r,t) = CAP.l(i,v,r,t) / ilr(i) ; + +cap_ivrt(i,v,r,t)$[(not (upv(i) or wind(i)))$valcap(i,v,r,t)] = cap_deg_ivrt(i,v,r,t) ; +*upv, and wind have degradation, so use INV rather than CAP to get the reported capacity +cap_ivrt(i,v,r,t)$[(upv(i) or wind(i))$valcap(i,v,r,t)] = ( + m_capacity_exog(i,v,r,t)$tmodel_new(t) + + sum{tt$[inv_cond(i,v,r,t,tt)$[tmodel(tt) or tfix(tt)]], + INV.l(i,v,r,tt) + INV_REFURB.l(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]}) / ilr(i) ; + +cap_out(i,r,t)$[valcap_irt(i,r,t)$tmodel_new(t)] = sum{v$valcap(i,v,r,t), cap_ivrt(i,v,r,t) } ; +* A small amount of upv capacity is actually csp-ns, so convert it back now. +* UPV capacity is already in MWac at this point (matching csp-ns), +* so don't need to account for ILR +cap_out("csp-ns",r,t)$[cap_cspns(r,t)$tmodel_new(t)] = cap_cspns(r,t) ; +* We have to take csp-ns capacity from somewhere, so take it from upv_6 (which all the +* csp-ns-containing regions have) +cap_out("upv_6",r,t)$[cap_cspns(r,t)$tmodel_new(t)] = cap_out("upv_6",r,t) - cap_cspns(r,t) ; +* Make sure it doesn't go negative, just in case +cap_out("upv_6",r,t)$[cap_cspns(r,t)$tmodel_new(t)$(cap_out("upv_6",r,t) < 0)] = 0 ; +cap_nat(i,t)$tmodel_new(t) = sum{r, cap_out(i,r,t) } ; + +* Exogenous capacity (used by reeds_to_rev) +cap_exog(i,v,r,t)$tmodel_new(t) = m_capacity_exog(i,v,r,t) ; + +*========================= +* NEW CAPACITY +*========================= + +cap_new_out(i,r,t)$[valcap_irt(i,r,t)] = [ + sum{v$valinv(i,v,r,t), + INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t) } + + sum{v$valcap(i,v,r,t), + (1 - upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))}$[upgrade(i)$Sw_Upgrades] + ] / ilr(i) ; +* Capacity of distpv is not tracked in INV because it is an exogenous input, so use the change in cap_out to calculate new capacity +* (except for the first year, in which all distpv capacity is counted as new) +cap_new_out("distpv",r,t)$[tfirst(t)$valcap_irt("distpv",r,t)] = cap_out("distpv",r,t) ; +cap_new_out("distpv",r,t)$[(not tfirst(t))$valcap_irt("distpv",r,t)] = cap_out("distpv",r,t) - sum{tt$tprev(t,tt), cap_out("distpv",r,tt) } ; +cap_new_ann(i,r,t)$cap_new_out(i,r,t) = cap_new_out(i,r,t) / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; +cap_new_ann_nat(i,t)$tmodel_new(t) = sum{r, cap_new_ann(i,r,t) } ; +cap_new_bin_out(i,v,r,t,rscbin)$[rsc_i(i)$valinv(i,v,r,t)] = INV_RSC.l(i,v,r,rscbin,t) / ilr(i) ; +cap_new_bin_out(i,v,r,t,"bin1")$[(not rsc_i(i))$valinv(i,v,r,t)] = INV.l(i,v,r,t) / ilr(i) ; +cap_new_ivrt(i,v,r,t)$[valcap(i,v,r,t)] = [ + [INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)]$valinv(i,v,r,t) + + [(1-upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))]$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades] + ] / ilr(i) ; +cap_new_ivrt("distpv",v,r,t)$[tfirst(t)$valcap("distpv",v,r,t)] = cap_ivrt("distpv",v,r,t) ; +cap_new_ivrt("distpv",v,r,t)$[(not tfirst(t))$valcap("distpv",v,r,t)] = cap_ivrt("distpv",v,r,t) - sum{tt$tprev(t,tt), cap_ivrt("distpv",v,r,tt) } ; +cap_new_ivrt_refurb(i,v,r,t)$valinv(i,v,r,t) = INV_REFURB.l(i,v,r,t) / ilr(i) ; + +* Capacity by reV site +site_spurinv(x,t)$[tmodel_new(t)$xfeas(x)] = INV_SPUR.l(x,t) ; +site_spurcap(x,t)$[tmodel_new(t)$xfeas(x)] = CAP_SPUR.l(x,t) ; + +site_cap(i,x,t)$[tmodel_new(t)$sum{(r,rscbin), spurline_sitemap(i,r,rscbin,x)}] = + sum{(v,r,rscbin,tt) + $[spurline_sitemap(i,r,rscbin,x) + $cap_new_bin_out(i,v,r,tt,rscbin) + $(yeart(tt) <= yeart(t))], +* Multiply by ILR to get DC capacity for PV + cap_new_bin_out(i,v,r,tt,rscbin) * ilr(i) + } ; + +site_gir(i,x,t)$[site_cap(i,x,t)$site_spurcap(x,t)] = site_cap(i,x,t) / site_spurcap(x,t) ; + +site_pv_fraction(x,t)$sum{i$spur_techs(i), site_cap(i,x,t)} = + sum{i$upv(i), site_cap(i,x,t)} / (sum{i$spur_techs(i), site_cap(i,x,t)}) ; + +site_hybridization(x,t)$site_pv_fraction(x,t) = abs(1 - 2 * abs(site_pv_fraction(x,t) - 0.5)) ; + +*========================= +* AVAILABLE CAPACITY +*========================= +cap_avail(i,r,t,rscbin)$[tmodel_new(t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i)] = + m_rsc_dat(r,i,rscbin,"cap") + + hyd_add_upg_cap(r,i,rscbin,t)$(Sw_HydroCapEnerUpgradeType=1) + +- ( + sum{(ii,v,tt)$[valinv(ii,v,r,tt)$(yeart(tt) < yeart(t))$rsc_agg(i,ii)], + INV_RSC.l(ii,v,r,rscbin,tt) * resourcescaler(ii) } + + + sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], + capacity_exog_rsc(ii,v,r,rscbin,tt) } +); + +capacity_offline(i,r,allh,t) + $[valcap_irt(i,r,t)$tmodel_new(t)$(h_stress_t(allh,t) or h_rep(allh))] = + cap_out(i,r,t) * (1 - avail(i,r,allh)) ; + +forced_outage(i) = sum{(r,h), outage_forced_h(i,r,h) * hours(h) } / sum{(r,h), hours(h) } ; +planned_outage(i) = sum{h, outage_scheduled_h(i,h) * hours(h) } / sum{h, hours(h) } ; + +*========================= +* UPGRADED CAPACITY +*========================= + +cap_upgrade(i,r,t)$[upgrade(i)$valcap_irt(i,r,t)] = sum{v, (1-upgrade_derate(i,v,r,t)) * UPGRADES.l(i,v,r,t) } ; +cap_upgrade_ivrt(i,v,r,t)$[valcap(i,v,r,t)$upgrade(i)$Sw_Upgrades] = (1-upgrade_derate(i,v,r,t)) * UPGRADES.l(i,v,r,t) ; + +*========================= +* RETIRED CAPACITY +*========================= + +ret_ivrt(i,v,r,t)$[(not tfirst(t))] = + sum{tt$tprev(t,tt), cap_ivrt(i,v,r,tt) } - cap_ivrt(i,v,r,t) + cap_new_ivrt(i,v,r,t) + - sum{ii$upgrade_from(ii,i), UPGRADES.l(ii,v,r,t) } ; +ret_ivrt(i,v,r,t)$[abs(ret_ivrt(i,v,r,t)) < 1e-6] = 0 ; + +ret_out(i,r,t)$[(not tfirst(t))] = sum{v, ret_ivrt(i,v,r,t) } ; +ret_out(i,r,t)$[abs(ret_out(i,r,t)) < 1e-6] = 0 ; +ret_ann(i,r,t)$ret_out(i,r,t) = ret_out(i,r,t) / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; +ret_ann_nat(i,t)$tmodel_new(t) = sum{r, ret_ann(i,r,t) } ; + +*================================== +* BINNED STORAGE CAPACITY +*================================== + +cap_sdbin_out(i,r,ccseason,sdbin,t)$valcap_irt(i,r,t) = sum{v, CAP_SDBIN.l(i,v,r,ccseason,sdbin,t)} ; + +* energy capacity of storage +stor_energy_cap(i,v,r,t)$[tmodel_new(t)$valcap(i,v,r,t)] = + storage_duration(i) * CAP.l(i,v,r,t) * (1$CSP_Storage(i) + 1$psh(i) + bcr(i)$[battery(i) or storage_hybrid(i)$(not csp(i))]) ; + +* add PSH energy capacity to cap_energy_ivrt +cap_energy_ivrt(i,v,r,t)$[valcap(i,v,r,t)$psh(i)] = CAP.l(i,v,r,t) * storage_duration(i) ; + +* battery storage duration +storage_duration_out(i,v,r,t)$[valcap(i,v,r,t)$battery(i)$CAP.l(i,v,r,t)] = + CAP_ENERGY.l(i,v,r,t) / CAP.l(i,v,r,t) ; + +*================================== +* CAPACITY CREDIT AND FIRM CAPACITY +*================================== + +cc_all_out(i,v,r,ccseason,t)$tmodel_new(t) = + cc_int(i,v,r,ccseason,t)$[(vre(i) or csp(i) or storage(i) or storage_hybrid(i)$(not csp(i)))$valcap(i,v,r,t)] + + m_cc_mar(i,r,ccseason,t)$[(vre(i) or csp(i) or storage(i) or storage_hybrid(i)$(not csp(i)))$valinv_init(i,v,r,t)] +; + +cap_new_cc(i,r,ccseason,t)$[(vre(i) or storage(i) or storage_hybrid(i)$(not csp(i)))$valcap_irt(i,r,t)] = sum{v$ivt(i,v,t),cap_new_ivrt(i,v,r,t) } ; + +cc_new(i,r,ccseason,t)$[valcap_irt(i,r,t)$cap_new_cc(i,r,ccseason,t)] = sum{v$ivt(i,v,t), cc_all_out(i,v,r,ccseason,t) } ; + +cap_firm(i,r,ccseason,t)$[valcap_irt(i,r,t)$[not consume(i)]$tmodel_new(t)$Sw_PRM_CapCredit] = + sum{v$[(not vre(i))$(not hydro(i))$(not storage(i))$(not storage_hybrid(i)$(not csp(i)))$valcap(i,v,r,t)], + CAP.l(i,v,r,t) * (1 + ccseason_cap_frac_delta(i,v,r,ccseason,t)) } + + cc_old(i,r,ccseason,t) + + sum{v$[(vre(i) or csp(i) or storage_hybrid(i)$(not csp(i)))$valinv(i,v,r,t)], + m_cc_mar(i,r,ccseason,t) * (INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)$[refurbtech(i)$Sw_Refurb]) } + + sum{v$[(vre(i) or csp(i) or storage_hybrid(i)$(not csp(i)))$valcap(i,v,r,t)], + cc_int(i,v,r,ccseason,t) * CAP.l(i,v,r,t) } + + cc_excess(i,r,ccseason,t)$[(vre(i) or csp(i) or storage_hybrid(i)$(not csp(i)))] + + sum{(v,h)$[hydro_nd(i)$valgen(i,v,r,t)$h_ccseason_prm(h,ccseason)], + GEN.l(i,v,r,h,t) } + + sum{v$[hydro_d(i)$valcap(i,v,r,t)], + CAP.l(i,v,r,t) * cap_hyd_ccseason_adj(i,ccseason,r) * (1 + hydro_capcredit_delta(i,t)) } + + sum{(v,sdbin)$[valcap(i,v,r,t)$(storage_standalone(i) or hyd_add_pump(i))], CAP_SDBIN.l(i,v,r,ccseason,sdbin,t) * cc_storage(i,sdbin) } + + sum{(v,sdbin)$[valcap(i,v,r,t)$storage_hybrid(i)], CAP_SDBIN.l(i,v,r,ccseason,sdbin,t) * cc_storage(i,sdbin) * hybrid_cc_derate(i,r,ccseason,sdbin,t) } ; + +* Capacity trading to meet PRM +captrade(r,rr,trtype,ccseason,t)$[routes(r,rr,trtype,t)$routes_prm(r,rr)$tmodel_new(t)] = PRMTRADE.l(r,rr,trtype,ccseason,t) ; + +*======================================== +* REVENUE LEVELS +*======================================== + +revenue('load',i,r,t)$valgen_irt(i,r,t) = sum{(v,h)$valgen(i,v,r,t), + GEN.l(i,v,r,h,t) * hours(h) * reqt_price('load','na',r,h,t) } ; + +*revenue from storage charging (storage charging from curtailment recovery does not have a cost) +revenue('charge',i,r,t)$[storage_standalone(i)$valgen_irt(i,r,t)] = - sum{(v,h)$valgen(i,v,r,t), + STORAGE_IN.l(i,v,r,h,t) * hours(h) * reqt_price('load','na',r,h,t) } ; + +revenue('res_marg',i,r,t)$[valgen_irt(i,r,t)$Sw_PRM_CapCredit] = sum{ccseason, + cap_firm(i,r,ccseason,t) * reqt_price('res_marg','na',r,ccseason,t) } ; +revenue('res_marg',i,r,t)$[valgen_irt(i,r,t)$(Sw_PRM_CapCredit=0)] = sum{allh$h_stress_t(allh,t), + gen_h_stress(i,r,allh,t) * reqt_price('res_marg','na',r,allh,t) } ; + +revenue('oper_res',i,r,t)$valgen_irt(i,r,t) = sum{(ortype,v,h)$valgen(i,v,r,t), + OPRES.l(ortype,i,v,r,h,t) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } ; + +revenue('rps',i,r,t)$valgen_irt(i,r,t) = + sum{(v,h,RPSCat)$[valgen(i,v,r,t)$sum{st$r_st(r,st), RecTech(RPSCat,i,st,t) }], + GEN.l(i,v,r,h,t) * sum{st$r_st(r,st), RPSTechMult(RPSCat,i,st) } * hours(h) * reqt_price('state_rps',RPSCat,r,'ann',t) } ; + +revenue_nat(rev_cat,i,t)$tmodel_new(t) = sum{r, revenue(rev_cat,i,r,t) } ; + +revenue_en(rev_cat,i,r,t) + $[tmodel_new(t) + $valgen_irt(i,r,t) + $sum{h, gen_h(i,r,h,t) * hours(h) } + $[not vre(i)]] = + revenue(rev_cat,i,r,t) / sum{h, gen_h(i,r,h,t) * hours(h) } ; + +revenue_en(rev_cat,i,r,t) + $[tmodel_new(t) + $sum{(v,h)$[valcap(i,v,r,t)], m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) } + $vre(i)] = + revenue(rev_cat,i,r,t) / sum{(v,h)$valcap(i,v,r,t), + m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; + +revenue_en_nat(rev_cat,i,t) + $[tmodel_new(t) + $sum{(r,h)$valgen_irt(i,r,t), gen_h(i,r,h,t) * hours(h) } + $[not vre(i)]] = + revenue_nat(rev_cat,i,t) / sum{(r,h)$valgen_irt(i,r,t), gen_h(i,r,h,t) * hours(h) } ; + +revenue_en_nat(rev_cat,i,t) + $[tmodel_new(t) + $sum{(v,r,h)$[valcap(i,v,r,t)], m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) } + $vre(i)] = + revenue_nat(rev_cat,i,t) / sum{(v,r,h)$valcap(i,v,r,t), + m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; + +revenue_cap(rev_cat,i,r,t)$[tmodel_new(t)$cap_out(i,r,t)] = + revenue(rev_cat,i,r,t) / cap_out(i,r,t) ; + +revenue_cap_nat(rev_cat,i,t)$[tmodel_new(t)$sum{r$valcap_irt(i,r,t), cap_out(i,r,t) }] = + revenue_nat(rev_cat,i,t) / cap_nat(i,t) ; + +*======================================== +* Value (Revenue) of new builds +*======================================== + +valnew('MW',i,r,t)$[(not tfirst(t))$valcap_irt(i,r,t)] = + sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t) } / ilr(i) ; +valnew('inv_cap_ratio',i,r,t)$[valnew('MW',i,r,t)] = + sum{v$[valinv(i,v,r,t)$CAP.l(i,v,r,t)], (INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)) + / CAP.l(i,v,r,t) } ; +valnew('MWh',i,r,t)$[valnew('MW',i,r,t)] = + sum{v$valinv(i,v,r,t), gen_ivrt(i,v,r,t)} * valnew('inv_cap_ratio',i,r,t) ; +* Use uncurtailed energy for VRE +valnew('MWh',i,r,t)$[valnew('MW',i,r,t)$sum{v$valinv(i,v,r,t), gen_ivrt_uncurt(i,v,r,t)}] = + sum{v$valinv(i,v,r,t), gen_ivrt_uncurt(i,v,r,t)} * valnew('inv_cap_ratio',i,r,t) ; +valnew('MWh','benchmark',r,t)$tmodel_new(t) = sum{h, reqt_quant('load','na',r,h,t)} ; +valnew('MWh','benchmark','sys',t)$tmodel_new(t) = sum{(r,h), reqt_quant('load','na',r,h,t)} ; +valnew('MW','benchmark',r,t)$tmodel_new(t) = reqt_quant('res_marg_ann','na',r,'ann',t) ; +valnew('MW','benchmark','sys',t)$tmodel_new(t) = sum{r, reqt_quant('res_marg_ann','na',r,'ann',t)} ; + +valnew('val_load',i,r,t)$valnew('MW',i,r,t) = sum{(v,h)$valinv(i,v,r,t), + (GEN.l(i,v,r,h,t) - STORAGE_IN.l(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)]) + * hours(h) * reqt_price('load','na',r,h,t) } * valnew('inv_cap_ratio',i,r,t) ; +*'val_load_sys' is the val our tech would have if valued at the system-average load price profile. +valnew('val_load_sys',i,r,t)$valnew('MW',i,r,t) = sum{(v,h)$valinv(i,v,r,t), + (GEN.l(i,v,r,h,t) - STORAGE_IN.l(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)]) + * hours(h) * reqt_price_sys('load','na',h,t) } * valnew('inv_cap_ratio',i,r,t) ; +*Annual-average price at each r: +valnew('val_load','benchmark',r,t)$tmodel_new(t) = + sum{h, reqt_price('load','na',r,h,t) * reqt_quant('load','na',r,h,t)} ; +*Annual-average price of the system +valnew('val_load','benchmark','sys',t)$tmodel_new(t) = + sum{(r,h), reqt_price('load','na',r,h,t) * reqt_quant('load','na',r,h,t)} ; + +valnew('val_resmarg',i,r,t)$[(Sw_PRM_CapCredit=0)$valnew('MW',i,r,t)] = + sum{(v,allh)$[h_stress_t(allh,t)$valinv(i,v,r,t)], + (GEN.l(i,v,r,allh,t) - STORAGE_IN.l(i,v,r,allh,t)$[storage_standalone(i) or hyd_add_pump(i)]) + * reqt_price('res_marg','na',r,allh,t)} * valnew('inv_cap_ratio',i,r,t) ; +* New VRE for the CapCredit formulation is a special case in that new is distinct from old of the same vintage +valnew('val_resmarg',i,r,t)$[(Sw_PRM_CapCredit=1)$vre(i)$valnew('MW',i,r,t)] = + sum{ccseason, m_cc_mar(i,r,ccseason,t) * valnew('MW',i,r,t) * reqt_price('res_marg','na',r,ccseason,t)}; +valnew('val_resmarg_sys',i,r,t)$[(Sw_PRM_CapCredit=0)$valnew('MW',i,r,t)] = + sum{(v,allh)$[h_stress_t(allh,t)$valinv(i,v,r,t)], + (GEN.l(i,v,r,allh,t) - STORAGE_IN.l(i,v,r,allh,t)$[storage_standalone(i) or hyd_add_pump(i)]) + * reqt_price_sys('res_marg','na',allh,t)} * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_resmarg_sys',i,r,t)$[(Sw_PRM_CapCredit=1)$vre(i)$valnew('MW',i,r,t)] = + sum{ccseason, m_cc_mar(i,r,ccseason,t) * valnew('MW',i,r,t) * reqt_price_sys('res_marg','na',ccseason,t)} ; +* Note: val_resmarg and val_resmarg_sys are missing for the capacity credit formulation for non-VRE. +* These would need cap_firm() but with vintage... +valnew('val_resmarg','benchmark',r,t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)] = + sum{allh$h_stress_t(allh,t), reqt_price('res_marg','na',r,allh,t) * reqt_quant('res_marg','na',r,allh,t)} ; +valnew('val_resmarg','benchmark','sys',t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)] = + sum{(r,allh)$h_stress_t(allh,t), reqt_price('res_marg','na',r,allh,t) * reqt_quant('res_marg','na',r,allh,t)} ; +valnew('val_resmarg','benchmark',r,t)$[(Sw_PRM_CapCredit=1)$tmodel_new(t)] = + sum{ccseason, reqt_price('res_marg','na',r,ccseason,t) * reqt_quant('res_marg','na',r,ccseason,t)} ; +valnew('val_resmarg','benchmark','sys',t)$[(Sw_PRM_CapCredit=1)$tmodel_new(t)] = + sum{(r,ccseason), reqt_price('res_marg','na',r,ccseason,t) * reqt_quant('res_marg','na',r,ccseason,t)} ; + +valnew('val_opres',i,r,t)$[(not (wind(i) or pv(i) or pvb(i)))$valnew('MW',i,r,t)] = sum{(ortype,v,h)$valinv(i,v,r,t), + OPRES.l(ortype,i,v,r,h,t) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_opres',i,r,t)$[wind(i)$valnew('MW',i,r,t)] = + -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$valinv(i,v,r,t)], + orperc(ortype,"or_wind") * GEN.l(i,v,r,h,t) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } + * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_opres',i,r,t)$[(pv(i) or pvb(i))$valnew('MW',i,r,t)] = + -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$dayhours(h)], + orperc(ortype,"or_pv") * CAP.l(i,v,r,t) / ilr(i) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } + * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_opres_sys',i,r,t)$[(not (wind(i) or pv(i) or pvb(i)))$valnew('MW',i,r,t)] = sum{(ortype,v,h)$valinv(i,v,r,t), + OPRES.l(ortype,i,v,r,h,t) * hours(h) * reqt_price_sys('oper_res',ortype,h,t) } * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_opres_sys',i,r,t)$[wind(i)$valnew('MW',i,r,t)] = + -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$valinv(i,v,r,t)], + orperc(ortype,"or_wind") * GEN.l(i,v,r,h,t) * hours(h) * reqt_price_sys('oper_res',ortype,h,t) } + * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_opres_sys',i,r,t)$[(pv(i) or pvb(i))$valnew('MW',i,r,t)] = + -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$dayhours(h)], + orperc(ortype,"or_pv") * CAP.l(i,v,r,t) / ilr(i) * hours(h) * reqt_price_sys('oper_res',ortype,h,t) } + * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_opres','benchmark',r,t)$tmodel_new(t) = + sum{(ortype,h), reqt_price('oper_res',ortype,r,h,t) * reqt_quant('oper_res',ortype,r,h,t)} ; +valnew('val_opres','benchmark','sys',t)$tmodel_new(t) = + sum{(ortype,r,h), reqt_price('oper_res',ortype,r,h,t) * reqt_quant('oper_res',ortype,r,h,t)} ; + +valnew('val_rps',i,r,t)$valnew('MW',i,r,t) = + sum{(v,h,RPSCat)$[valinv(i,v,r,t)$sum{st$r_st(r,st), RecTech(RPSCat,i,st,t) }], + GEN.l(i,v,r,h,t) * sum{st$r_st(r,st), RPSTechMult(RPSCat,i,st) } * hours(h) * reqt_price('state_rps',RPSCat,r,'ann',t) } + * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_rps_sys',i,r,t)$valnew('MW',i,r,t) = + sum{(v,h,RPSCat)$[valinv(i,v,r,t)$sum{st$r_st(r,st), RecTech(RPSCat,i,st,t) }], + GEN.l(i,v,r,h,t) * sum{st$r_st(r,st), RPSTechMult(RPSCat,i,st) } * hours(h) * reqt_price_sys('state_rps',RPSCat,'ann',t) } + * valnew('inv_cap_ratio',i,r,t) ; +valnew('val_rps','benchmark',r,t)$tmodel_new(t) = + sum{RPSCat, reqt_price('state_rps',RPSCat,r,'ann',t) * reqt_quant('state_rps',RPSCat,r,'ann',t)} ; +*Annual-average price of the system +valnew('val_rps','benchmark','sys',t)$tmodel_new(t) = + sum{(r,RPSCat), reqt_price('state_rps',RPSCat,r,'ann',t) * reqt_quant('state_rps',RPSCat,r,'ann',t)} ; + +*========================= +* EMISSIONS +*========================= +* emit_r is calculated the same as the EMIT variable in the model. We do not use +* EMIT.l here because the emissions are only modeled for those in the emit_modeled +* set. +emit_r(etype,e,r,t)$tmodel_new(t) = + +* Emissions from generation + sum{(i,v,h)$[valgen(i,v,r,t)$h_rep(h)], + hours(h) * emit_rate(etype,e,i,v,r,t) + * (GEN.l(i,v,r,h,t) + + CCSFLEX_POW.l(i,v,r,h,t)$[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)]) + } + +* Plus emissions produced via production activities (SMR, SMR-CCS, DAC) +* The "production" of negative CO2 emissions via DAC is also included here + + sum{(p,i,v,h)$[valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], + hours(h) * prod_emit_rate(etype,e,i,t) + * PRODUCE.l(p,i,v,r,h,t) + } + +*[minus] co2 reduce from flexible CCS capture +*capture = capture per energy used by the ccs system * CCS energy + +* Flexible CCS - bypass + - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_byp(i)$h_rep(h)], + ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POW.l(i,v,r,h,t) })$[sameas(e,"co2")]$Sw_CCSFLEX_BYP + +* Flexible CCS - storage + - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_sto(i)$h_rep(h)], + ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POWREQ.l(i,v,r,h,t) })$[sameas(e,"co2")]$Sw_CCSFLEX_STO +; + +* Apply global warming potential to include CH4 and N2O in CO2(e) +emit_r(etype,"CO2e",r,t)$tmodel_new(t) = sum{e,emit_r(etype,e,r,t)*gwp(e)} ; +emit_nat(etype,eall,t)$tmodel_new(t) = sum{r, emit_r(etype,eall,r,t) } ; + +* Generation emissions by tech and region +emit_irt(etype,e,i,r,t)$[tmodel_new(t)$(not sameas(e,"CO2"))$valgen_irt(i,r,t)] = + sum{(v,h)$[valgen(i,v,r,t)], + hours(h) * emit_rate(etype,e,i,v,r,t) * GEN.l(i,v,r,h,t) } ; +* Production-related emissions by tech and region +emit_irt(etype,e,i,r,t)$[tmodel_new(t)$(not sameas(e,"CO2"))$sum{p, i_p(i,p)}] = + sum{(p,v,h)$i_p(i,p), + hours(h) * prod_emit_rate(etype,e,i,t) * PRODUCE.l(p,i,v,r,h,t) } ; +* CO2 generation emissions by tech and region +emit_irt(etype,"CO2",i,r,t)$[tmodel_new(t)$valgen_irt(i,r,t)] = sum{(v,h)$[valgen(i,v,r,t)], + hours(h) * emit_rate(etype,"CO2",i,v,r,t) * GEN.l(i,v,r,h,t) } ; +* CO2 production-related emissions by tech and region +emit_irt(etype,"CO2",i,r,t)$[tmodel_new(t)$sum{p, i_p(i,p)}] = + sum{(p,v,h)$i_p(i,p), + hours(h) * prod_emit_rate(etype,"CO2",i,t) * PRODUCE.l(p,i,v,r,h,t) } ; +* Apply global warming potential to include other GHGs in CO2(e) +emit_irt(etype,"CO2e",i,r,t)$tmodel_new(t) = sum{e,emit_irt(etype,e,i,r,t) * gwp(e)} ; + +emit_nat_tech(etype,eall,i,t) = sum{r, emit_irt(etype,eall,i,r,t)} ; + +emit_weighted(etype,eall) = sum{t$tmodel(t), emit_nat(etype,eall,t) * pvf_onm(t) } ; + +emit_rate_regional(r,t)$tmodel_new(t) = co2_emit_rate_r(r,t) ; + +* captured CO2 emissions from CCS and DAC +emit_captured_irt(i,r,t)$tmodel_new(t) = + sum{(v,h)$[valgen(i,v,r,t)], hours(h) * capture_rate("CO2",i,v,r,t) * GEN.l(i,v,r,h,t) } ; + +emit_captured_irt("smr_ccs",r,t)$tmodel_new(t) = + sum{(v,h,p)$[valcap("smr_ccs",v,r,t)$i_p("smr_ccs",p)], smr_capture_rate * hours(h) + * smr_co2_intensity * PRODUCE.l(p,"smr_ccs",v,r,h,t) } ; + +emit_captured_irt(i,r,t)$[tmodel_new(t)$dac(i)] = + sum{(v,h,p)$[dac(i)$valcap(i,v,r,t)$i_p(i,p)], hours(h) * PRODUCE.l(p,i,v,r,h,t)} ; + +emit_captured_nat(i,t)$tmodel_new(t) = sum{r, emit_captured_irt(i,r,t) } ; + + +*================================== +* National RE Constraint Marginals +*================================== + +RE_gen_price_nat(t)$tmodel_new(t) = (1/cost_scale) * crf(t) * eq_national_gen.m(t) ; + +*========================= +* [i,v,r,t]-level capital expenditures (for retail rate calculations) +*========================= + +capex_ivrt(i,v,r,t)$valcap(i,v,r,t) = + INV.l(i,v,r,t) * (cost_cap_fin_mult_no_credits(i,r,t) * cost_cap(i,t) ) + + INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult_no_credits(i,r,t) * cost_cap_energy(i,t) ) + + sum{(rscbin)$m_rscfeas(r,i,rscbin),INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * cost_cap_fin_mult_no_credits(i,r,t) } + + (INV_REFURB.l(i,v,r,t) * (cost_cap_fin_mult_no_credits(i,r,t) * cost_cap(i,t)))$[refurbtech(i)$Sw_Refurb] + + UPGRADES.l(i,v,r,t) * (cost_upgrade(i,v,r,t) * cost_cap_fin_mult_no_credits(i,r,t))$[upgrade(i)$Sw_Upgrades] ; + +*========================= +* Tech|BA-Level SYSTEM COST: Capital +*========================= + +* REPLICATION OF THE OBJECTIVE FUNCTION +* DOES NOT INCLUDE COSTS NOT INDEXED BY TECH (e.g., TRANSMISSION) + +systemcost_techba("inv_investment_capacity_costs",i,r,t)$tmodel_new(t) = +*investment costs (without the subtraction of any ITC/PTC value) + sum{v$valinv(i,v,r,t), + INV.l(i,v,r,t) * (cost_cap_fin_mult_noITC(i,r,t) * cost_cap(i,t) ) } +*plus investment energy costs (without the subtraction of any ITC/PTC value) + + sum{v$[valinv(i,v,r,t)$battery(i)], + INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult_noITC(i,r,t) * cost_cap_energy(i,t) ) } +*plus supply curve adjustment to capital cost (separated in outputs but part of m_rsc_dat(r,i,rscbin,"cost")) + + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$[not sccapcosttech(i)]$(not spur_techs(i))], + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_cap") * rsc_fin_mult_noITC(i,r,t) } +* Plus geo, hydro, and pumped-hydro techs, where costs are in the supply curves +*(Note that this deviates from the objective function structure) + + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$sccapcosttech(i)], + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult_noITC(i,r,t) } +*plus cost of upgrades + + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], + cost_upgrade(i,v,r,t) * cost_cap_fin_mult_noITC(i,r,t) * UPGRADES.l(i,v,r,t) } +*cost of capacity upsizing + + sum{(v,rscbin)$allow_cap_up(i,v,r,rscbin,t), + cost_cap_fin_mult_noITC(i,r,t) * INV_CAP_UP.l(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } +*cost of energy upsizing + + sum{(v,rscbin)$allow_ener_up(i,v,r,rscbin,t), + cost_cap_fin_mult_noITC(i,r,t) * INV_ENER_UP.l(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } +; + +systemcost_techba("inv_investment_spurline_costs_rsc_technologies",i,r,t)$tmodel_new(t) = +*costs of rsc spur line investment +*Note that cost_cap for hydro, pumped-hydro, and geo techs are zero +*but hydro and geo rsc_fin_mult is equal to the same value as cost_cap_fin_mult +*(Note that exclusions of geo and hydro here deviates from the objective function structure) + sum{(v,rscbin) + $[m_rscfeas(r,i,rscbin) + $valinv(i,v,r,t) + $rsc_i(i) + $[not sccapcosttech(i)] + $(not spur_techs(i)) + ], +*investment in resource supply curve technologies + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_trans") * rsc_fin_mult_noITC(i,r,t) } +; + +systemcost_techba("inv_itc_payments_negative",i,r,t)$tmodel_new(t) = +*investment costs (including reduction from ITC) + sum{v$valinv(i,v,r,t), + INV.l(i,v,r,t) * (cost_cap_fin_mult_out(i,r,t) * cost_cap(i,t) ) } +*energy investment costs (including reduction from ITC) + + sum{v$[valinv(i,v,r,t)$battery(i)], + INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult_out(i,r,t) * cost_cap_energy(i,t) ) } +*plus supply curve adjustment to capital cost (separated in outputs but part of m_rsc_dat(r,i,rscbin,"cost")) + + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$[not sccapcosttech(i)]$(not spur_techs(i))], + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_cap") * rsc_fin_mult(i,r,t) } +* Plus geo, hydro, and pumped-hydro techs, where costs are in the supply curves +*(Note that this deviates from the objective function structure) + + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$sccapcosttech(i)], + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) } +*plus cost of upgrades + + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], + cost_upgrade(i,v,r,t) * cost_cap_fin_mult_out(i,r,t) * UPGRADES.l(i,v,r,t) } +*cost of capacity upsizing + + sum{(v,rscbin)$allow_cap_up(i,v,r,rscbin,t), + cost_cap_fin_mult_out(i,r,t) * INV_CAP_UP.l(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } +*cost of energy upsizing + + sum{(v,rscbin)$allow_ener_up(i,v,r,rscbin,t), + cost_cap_fin_mult_out(i,r,t) * INV_ENER_UP.l(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } +*minus capacity costs without ITC + - systemcost_techba("inv_investment_capacity_costs",i,r,t) +*plus supply curve transmission costs (including cost reductions from the ITC for applicable techs) + +sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$[not sccapcosttech(i)]$(not spur_techs(i))], + INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_trans") * rsc_fin_mult(i,r,t) } +*minus rsc transmission costs without ITC + - systemcost_techba("inv_investment_spurline_costs_rsc_technologies",i,r,t) +; + +*assign consume techs to their own category and then zero it out +systemcost_techba("inv_dac",i,r,t)$[tmodel_new(t)$dac(i)] = systemcost_techba("inv_investment_capacity_costs",i,r,t) ; +systemcost_techba("inv_h2_production",i,r,t)$[tmodel_new(t)$h2(i)] = systemcost_techba("inv_investment_capacity_costs",i,r,t) ; +systemcost_techba("inv_investment_capacity_costs",i,r,t)$[tmodel_new(t)$consume(i)] = 0 ; + +systemcost_techba("inv_investment_refurbishment_capacity",i,r,t)$tmodel_new(t) = +*costs of refurbishments of RSC tech (without the subtraction of any ITC/PTC value) + + sum{v$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], + (cost_cap_fin_mult_noITC(i,r,t) * cost_cap(i,t)) * INV_REFURB.l(i,v,r,t) } +; + +systemcost_techba("inv_itc_payments_negative_refurbishments",i,r,t)$tmodel_new(t) = +*costs of refurbishments of RSC tech (including reduction from ITC) + + sum{v$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], + (cost_cap_fin_mult_out(i,r,t) * cost_cap(i,t)) * INV_REFURB.l(i,v,r,t) } +*minus capacity costs without ITC + - systemcost_techba("inv_investment_refurbishment_capacity",i,r,t) +; + +systemcost_techba("inv_investment_water_access",i,r,t)$tmodel_new(t) = +*cost of water access + + (8760/1E6) * sum{ (v,w)$[i_w(i,w)$valinv(i,v,r,t)], sum{wst$i_wst(i,wst), m_watsc_dat(wst,"cost",r,t)} * water_rate(i,w) * + ( INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)$[refurbtech(i)$Sw_Refurb] ) } + + sum{(rscbin,v)$[m_rscfeas(r,i,rscbin)$psh(i)], sum{wst$i_wst(i,wst), m_watsc_dat(wst,"cost",r,t) } * + ( INV_RSC.l(i,v,r,rscbin,t) * water_req_psh(r,rscbin) ) } +; + +*=============== +* Tech|BA-Level SYSTEM COST: Operational (the op_ prefix is used by the retail rate module to identify which costs are operational costs) +*=============== + +* DOES NOT INCLUDE COSTS NOT INDEXED BY TECH (e.g., ACP COMPLIANCE) + +systemcost_techba("op_vom_costs",i,r,t)$tmodel_new(t) = +*variable O&M costs + sum{(v,h)$[valgen(i,v,r,t)$cost_vom(i,v,r,t)], + hours(h) * cost_vom(i,v,r,t) * GEN.l(i,v,r,h,t) } + +* include production costs from production technologies + + sum{(p,v,h)$[(h2(i) or dac(i))$valcap(i,v,r,t)$i_p(i,p)], + hours(h) * cost_prod(i,v,r,t) * PRODUCE.l(p,i,v,r,h,t) }$Sw_Prod +; + +systemcost_techba("op_consume_vom",i,r,t)$[tmodel_new(t)$consume(i)] = systemcost_techba("op_vom_costs",i,r,t)$tmodel_new(t) ; +systemcost_techba("op_vom_costs",i,r,t)$[tmodel_new(t)$consume(i)] = 0 ; + +systemcost_techba("op_fom_costs",i,r,t)$tmodel_new(t) = +*fixed O&M costs for generation capacity + + sum{v$[valcap(i,v,r,t)$((not one_newv(i)) or retiretech(i,v,r,t))], + cost_fom(i,v,r,t) * cap_ivrt(i,v,r,t) * ilr(i) } +*for technologies with only one newv that are not allowed to retire, +*use the investments rather than the capacity to calculate FOM costs + + sum{(v,tt)$[inv_cond(i,v,r,t,tt)$one_newv(i)$(not retiretech(i,v,r,tt))], + INV.l(i,v,r,tt) * cost_fom(i,v,r,tt) * ilr(i) } + + sum{(v,tt)$[inv_cond(i,v,r,t,tt)$one_newv(i)$(not retiretech(i,v,r,tt))], + INV_ENERGY.l(i,v,r,tt) * cost_fom_energy(i,v,r,tt) * ilr(i) } +; + +systemcost_techba("op_consume_fom",i,r,t)$[tmodel_new(t)$consume(i)] = systemcost_techba("op_fom_costs",i,r,t)$tmodel_new(t) ; +systemcost_techba("op_fom_costs",i,r,t)$[tmodel_new(t)$consume(i)] = 0 ; + +systemcost_techba("op_operating_reserve_costs",i,r,t)$tmodel_new(t) = +*operating reserve costs + + sum{(v,h,ortype)$[valgen(i,v,r,t)$cost_opres(i,ortype,t)], + hours(h) * cost_opres(i,ortype,t) * OpRes.l(ortype,i,v,r,h,t) } +; + +systemcost_techba("op_fuelcosts_objfn",i,r,t)$tmodel_new(t) = +*cost of coal and nuclear fuel (except coal used for cofiring) + + sum{(v,h)$[valgen(i,v,r,t)$heat_rate(i,v,r,t) + $(not gas(i))$(not bio(i))$(not cofire(i)) + $((not h2_combustion(i)) or h2_combustion(i)$[(Sw_H2=0) or h_stress(h)])], + hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN.l(i,v,r,h,t) } + +*cofire coal consumption - cofire bio consumption already accounted for in accounting of BIOUSED + + sum{(v,h)$[valgen(i,v,r,t)$cofire(i)$heat_rate(i,v,r,t)], + (1-bio_cofire_perc) * hours(h) * heat_rate(i,v,r,t) + * fuel_price("coal-new",r,t) * GEN.l(i,v,r,h,t) } + +*cost of natural gas fuel + + sum{cendiv$r_cendiv(r,cendiv), gascost_cendiv(cendiv,t) * gasshare_techba(i,r,cendiv,t) } + +*cost biofuel consumption by the tech in the BA + + bioshare_techba(i,r,t) * sum{bioclass, BIOUSED.l(bioclass,r,t) * + (sum{usda_region$r_usda(r,usda_region), biosupply(usda_region, bioclass, "price") } + bio_transport_cost) } + +; + +systemcost_techba("op_emissions_taxes",i,r,t)$tmodel_new(t) = +*plus any taxes on emissions + sum{(e,v,h)$[valgen(i,v,r,t)], + hours(h) * (emit_rate("process",e,i,v,r,t) + emit_rate("upstream",e,i,v,r,t)$Sw_Upstream) * GEN.l(i,v,r,h,t) * emit_tax(e,r,t) } +; + +systemcost_techba("op_h2_fuel_costs",i,r,t)$tmodel_new(t) = +* H2 production costs + sum{(v,h,p)$[h2(i)$valcap(i,v,r,t)], + hours(h) * h2_fuel_cost(i,v,r,t) * PRODUCE.l(p,i,v,r,h,t) } +; + +systemcost_techba("op_h2combustion_fuel_costs",i,r,t)$[tmodel_new(t)$h2_combustion(i)$Sw_H2] = +* fuel costs for H2-CT/CC techs + + (1 / cost_scale) * (1 / pvf_onm(t)) +* when using national demand, calculate total annual demand and multiply by national average price +* [MW] * [hours] * [MMBTU/MWh] * [metric tons/MMBTU] * [$/metric ton] = [$] + * ( (sum{(v,h), GEN.l(i,v,r,h,t) * hours(h) * heat_rate(i,v,r,t) * h2_combustion_intensity } + * eq_h2_demand.m("H2",t) + )$[Sw_H2 = 1] +* when using regional demand by hour, apply price to each hour and then sum total costs +* [MW] * [hours] * [MMBTU/MWh] * [metric tons/MMBTU] * [$/[metric tons/hour]] / [hours] = [$] + + (sum{(v,h), GEN.l(i,v,r,h,t) * hours(h) * heat_rate(i,v,r,t) * h2_combustion_intensity + * eq_h2_demand_regional.m(r,h,t) / hours(h) } + )$[Sw_H2 = 2] + ) +; + +systemcost_techba("op_h2_vom",i,r,t)$tmodel_new(t) = +* vom costs from H2 production + sum{(v,h,p)$[h2(i)$valcap(i,v,r,t)], + hours(h) * h2_vom(i,t) * PRODUCE.l(p,i,v,r,h,t) } +; + +* transport and storage cost of captured CO2 +systemcost_techba("op_co2_transport_storage",i,r,t)$[tmodel_new(t)$(not Sw_CO2_Detail)] = + emit_captured_irt(i,r,t) * Sw_CO2_Storage +; + +systemcost_techba("op_co2_incentive_negative",i,r,t)$tmodel_new(t) = + - sum{(v,h)$[valgen(i,v,r,t)$co2_captured_incentive(i,v,r,t)], + (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * capture_rate("CO2",i,v,r,t) * GEN.l(i,v,r,h,t)} + + - sum{(p,v,h)$[dac(i)$valcap(i,v,r,t)], + (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * PRODUCE.l(p,i,v,r,h,t)} +; + +* PTC for generation +systemcost_techba('op_ptc_payments_negative',i,r,t)$tmodel_new(t) = + - sum{(v,h)$[valgen(i,v,r,t)$ptc_value_scaled(i,v,t)], + hours(h) * ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t) * GEN.l(i,v,r,h,t) } +; + +* PTC value for hydrogen production +* Note: all electrolyzers which produce H2 are assuming to be receiving hydrogen production credits during eligible years +systemcost_techba('op_h2_ptc_payments_negative','electrolyzer',r,t)$[tmodel_new(t)] = + - (sum{(p,v,h)$[valcap("electrolyzer",v,r,t)$(sameas(p,"H2"))$h2_ptc("electrolyzer",v,r,t)$h_rep(h)], + hours(h) * PRODUCE.l(p,"electrolyzer",v,r,h,t) * + (crf(t) / crf_h2_incentive(t)) * h2_ptc("electrolyzer",v,r,t) * 1e3} ) + $[Sw_H2_PTC$Sw_H2$h2_ptc_years(t)$(yeart(t) >= h2_demand_start)] +; + +* Startup/ramping costs +systemcost_techba('op_startcost',i,r,t)$[tmodel_new(t)$Sw_StartCost$startcost(i)] = + sum{(h,hh)$[numhours_nexth(h,hh)$valgen_irt(i,r,t)], + startcost(i) * numhours_nexth(h,hh) * RAMPUP.l(i,r,h,hh,t) } +; + + +*For bulk system costs present value as of model year, capital costs are unchanged, +*while operation costs use pvf_onm_undisc +systemcost_techba_bulk(sys_costs,i,r,t) = systemcost_techba(sys_costs,i,r,t) ; +systemcost_techba_bulk(sys_costs_op,i,r,t) = systemcost_techba(sys_costs_op,i,r,t) * pvf_onm_undisc(t) ; + +systemcost_techba_bulk_ew(sys_costs,i,r,t) = systemcost_techba_bulk(sys_costs,i,r,t) ; +systemcost_techba_bulk_ew(sys_costs_op,i,r,t)$tlast(t) = systemcost_techba(sys_costs_op,i,r,t) ; + +* Sum across technologies to get BA-level costs for all applicable categories +systemcost_ba(sys_costs,r,t) = sum{i,systemcost_techba(sys_costs,i,r,t)} ; + +*========================= +* BA-Level SYSTEM COST: Capital +*========================= + +* REPLICATION OF THE OBJECTIVE FUNCTION + +* Interzonal transmission: Split costs between the two connected zones +* DC: INVTRAN is defined (and is equal) in both directions, so just include (r,rr) and divide by 2 +systemcost_ba("inv_transmission_interzone_dc_investment",r,t)$tmodel_new(t) = + sum{(rr,trtype)$[routes_inv(r,rr,trtype,t)$(not aclike(trtype))], + trans_cost_cap_fin_mult(t) + * transmission_cost_nonac(r,rr,trtype) + * INVTRAN.l(r,rr,trtype,t) + / 2 } +; + +* AC: TRAN_CAPEX_BINS is only defined for r < rr, so add (r,rr) + (rr,r) and divide by 2 +* First get the cumulative investment cost, split across zones +parameter capex_transmission_interzone_ac(r,t) "Cumulative interzonal AC transmission capex" ; +capex_transmission_interzone_ac(r,t)$tmodel_new(t) = + sum{(rr,tscbin)$[routes_inv(r,rr,"AC",t)$tsc_binwidth(r,rr,tscbin)], + trans_cost_cap_fin_mult(t) * TRAN_CAPEX_BINS.l(r,rr,tscbin,t) / 2 } + + sum{(rr,tscbin)$[routes_inv(rr,r,"AC",t)$tsc_binwidth(rr,r,tscbin)], + trans_cost_cap_fin_mult(t) * TRAN_CAPEX_BINS.l(rr,r,tscbin,t) / 2 } +; +* Loop over each year and keep the capex difference to get model-year investment +loop(t$[tmodel_new(t)$(not tfirst(t))], + systemcost_ba("inv_transmission_interzone_ac_investment",r,t) + = + capex_transmission_interzone_ac(r,t) + - sum{tt$tprev(t, tt), + capex_transmission_interzone_ac(r,tt) + } ; +) ; + +systemcost_ba("inv_transmission_intrazone_investment",r,t)$[tmodel_new(t)$Sw_TransIntraCost] = +* cost of intra-zone network reinforcement + trans_cost_cap_fin_mult(t) * Sw_TransIntraCost * 1000 * INV_POI.l(r,t) +; + +systemcost_ba("op_transmission_fom",r,t)$tmodel_new(t) = +*fixed O&M costs for transmission lines + sum{(rr,trtype)$routes(r,rr,trtype,t), + transmission_line_fom(r,rr,trtype) * CAPTRAN_ENERGY.l(r,rr,trtype,t) } +*fixed O&M costs for LCC AC/DC converters + + sum{(rr,trtype)$[lcclike(trtype)$routes(r,rr,trtype,t)], + cost_acdc_lcc * 2 * trans_fom_frac * CAPTRAN_ENERGY.l(r,rr,trtype,t) } +*fixed O&M costs for VSC AC/DC converters + + cost_acdc_vsc * trans_fom_frac * CAP_CONVERTER.l(r,t) +; + +systemcost_ba("op_transmission_intrazone_fom",r,t)$[tmodel_new(t)$Sw_TransIntraCost] = +* FOM cost for intra-zone network reinforcement + Sw_TransIntraCost * 1000 * trans_fom_frac + * sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], INV_POI.l(r,tt) } +; + +systemcost_ba("inv_converter_costs",r,t)$tmodel_new(t) = +* LCC and B2B AC/DC converter stations: each interface has two, one on either side of the interface, +* but each interface shows up in both INVTRAN(r,rr) and INVTRAN(rr,r) so don't multiply by 2 + sum{(rr,trtype)$[lcclike(trtype)$routes_inv(r,rr,trtype,t)], + trans_cost_cap_fin_mult(t) * cost_acdc_lcc * INVTRAN.l(r,rr,trtype,t) } +* VSC AC/DC converter stations + + trans_cost_cap_fin_mult(t) * cost_acdc_vsc * INV_CONVERTER.l(r,t) +; + +systemcost_ba("inv_spurline_investment",r,t)$[tmodel_new(t)$Sw_SpurScen] = +* capital cost of spur lines modeled explicitly + sum{x$[xfeas(x)$x_r(x,r)], spurline_cost(x) * Sw_SpurCostMult * INV_SPUR.l(x,t) } +; + +systemcost_ba("op_spurline_fom",r,t)$tmodel_new(t) = +* fixed O&M cost of spur lines modeled explicitly + sum{x$[Sw_SpurScen$xfeas(x)$x_r(x,r)], spurline_cost(x) * trans_fom_frac * CAP_SPUR.l(x,t) } +* fixed O&M cost of spur lines modeled as part of supply curve + + sum{(i,v,rscbin) + $[m_rscfeas(r,i,rscbin)$valcap(i,v,r,t) + $rsc_i(i)$(not spur_techs(i))$(not sccapcosttech(i))], + m_rsc_dat(r,i,rscbin,"cost_trans") * trans_fom_frac * CAP_RSC.l(i,v,r,rscbin,t) + } +; + +systemcost_ba("inv_co2_network_pipe",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = +*costs of co2 trunk pipeline investment (cost_co2_pipeline_cap already includes distance; see b_inputs) + + sum{rr$co2_routes(r,rr), cost_co2_pipeline_cap(r,rr,t) * + ( (CO2_TRANSPORT_INV.l(r,rr,t) + CO2_TRANSPORT_INV.l(rr,r,t) ) / 2 ) } +; + +systemcost_ba("inv_co2_network_spur",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = +*costs of co2 spurline investment (cost_co2_spurline_cap already includes distance; see b_inputs) + + sum{cs$r_cs(r,cs), cost_co2_spurline_cap(r,cs,t) * CO2_SPURLINE_INV.l(r,cs,t) } +; + +systemcost_ba("inv_h2_pipeline",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = +* H2 transport network investment costs (investments defined only for r < rr) + sum{rr$h2_routes_inv(r,rr), + cost_h2_transport_cap(r,rr,t) * H2_TRANSPORT_INV.l(r,rr,t) } +; + +systemcost_ba("inv_h2_storage",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = +* H2 storage investment costs + + sum{h2_stor$h2_stor_r(h2_stor,r), cost_h2_storage_cap(h2_stor,t) * H2_STOR_INV.l(h2_stor,r,t) } +; + +*=============== +* BA-Level SYSTEM COST: Operational (the op_ prefix is used by the retail rate module to identify which costs are operational costs) +*=============== + +systemcost_ba("op_co2_storage",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = + + sum{(h,cs)$r_cs(r,cs), hours(h) * CO2_STORED.l(r,cs,h,t) * cost_co2_stor_bec(cs,t) } +; + +* here following same logic of transmission pipelines +systemcost_ba("op_co2_network_fom_pipe",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = + sum{(rr)$[co2_routes(r,rr)], cost_co2_pipeline_fom(r,rr,t) + * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], + (CO2_TRANSPORT_INV.l(r,rr,tt) + CO2_TRANSPORT_INV.l(rr,r,tt) ) / 2 } + }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] +; + +systemcost_ba("op_co2_network_fom_spur",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = + sum{(cs)$r_cs(r,cs), cost_co2_spurline_fom(r,cs,t) + * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], + CO2_SPURLINE_INV.l(r,cs,tt) } + }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] +; + +systemcost_ba("op_co2_network_fom_spur",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = + sum{(cs)$r_cs(r,cs), cost_co2_spurline_fom(r,cs,t) + * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], + CO2_SPURLINE_INV.l(r,cs,tt) } + }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] +; + +* same for H2 pipelines and storage +systemcost_ba("op_h2_transport",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = + sum{rr$h2_routes_inv(r,rr), + cost_h2_transport_fom(r,rr,t) * + sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], + H2_TRANSPORT_INV.l(r,rr,t) } } +; + +systemcost_ba("op_h2_transport_intrareg",r,t)$[tmodel_new(t)$Sw_H2] = +* H2 transport and storage intra-regional investment costs + sum{(i,v,h)$[valcap(i,v,r,t)$newv(v)$i_p(i,"H2")], + hours(h) * PRODUCE.l("H2",i,v,r,h,t) * (Sw_H2_IntraReg_Transport * 1e3) } +; + +systemcost_ba("op_h2_storage",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = + sum{h2_stor$h2_stor_r(h2_stor,r), + cost_h2_storage_fom(h2_stor,t) * H2_STOR_CAP.l(h2_stor,r,t) } +; + +systemcost_ba("op_acp_compliance_costs",r,t)$[tmodel_new(t)$(yeart(t)>=firstyear_RPS)] = +*plus ACP purchase costs, attributed to bas based on fraction of state requirement + + sum{(st,RPSCat) + $[stfeas(st)$r_st(r,st)$RecPerc(RPSCat,st,t) + $sum{rr$r_st(rr,st), reqt_quant('state_rps',RPSCat,rr,'ann',t) }], + acp_price(st,t) * ACP_PURCHASES.l(RPSCat,st,t) * reqt_quant('state_rps',RPSCat,r,'ann',t) + / sum{rr$r_st(rr,st), reqt_quant('state_rps',RPSCat,rr,'ann',t) } + } +* spread voluntary purchase costs based on BA load frac + + sum{RPSCat$RecPerc(RPSCat,"voluntary",t), acp_price("voluntary",t) * ACP_PURCHASES.l(RPSCat,"voluntary",t) } + * load_frac_rt(r,t) + +; + +systemcost_ba("op_co2_incentive_negative",r,t)$tmodel_new(t) = + - sum{(i,v,h)$[valgen(i,v,r,t)$co2_captured_incentive(i,v,r,t)], + (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * capture_rate("CO2",i,v,r,t) * GEN.l(i,v,r,h,t)} + + - sum{(i,p,v,h)$[dac(i)$valcap(i,v,r,t)], + (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * PRODUCE.l(p,i,v,r,h,t)} +; + + +*If the op_h2combustion_fuel_costs are included in the systemcost_ba it will lead to double-counting +*but these fuel costs are needed for the retail rate module. We therefore zero them out here. +systemcost_ba_retailrate(sys_costs,r,t) = systemcost_ba(sys_costs,r,t) ; +systemcost_ba("op_h2combustion_fuel_costs",r,t) = 0 ; + +*For bulk system costs present value as of model year, capital costs are unchanged, +*while operation costs use pvf_onm_undisc +systemcost_ba_bulk(sys_costs,r,t) = systemcost_ba(sys_costs,r,t) ; +systemcost_ba_bulk(sys_costs_op,r,t) = systemcost_ba(sys_costs_op,r,t) * pvf_onm_undisc(t) ; + +systemcost_ba_bulk_ew(sys_costs,r,t) = systemcost_ba_bulk(sys_costs,r,t) ; +systemcost_ba_bulk_ew(sys_costs_op,r,t)$tlast(t) = systemcost_ba(sys_costs_op,r,t) ; + + +*========================= +* National System Cost +*========================= + +systemcost(sys_costs,t) = sum{r, systemcost_ba(sys_costs,r,t) } ; +systemcost_bulk(sys_costs,t) = systemcost(sys_costs,t) ; +systemcost_bulk(sys_costs_op,t) = systemcost(sys_costs_op,t) * pvf_onm_undisc(t) ; + +systemcost_bulk_ew(sys_costs,t) = systemcost_bulk(sys_costs,t) ; +systemcost_bulk_ew(sys_costs_op,t)$tlast(t) = systemcost(sys_costs_op,t) ; + +* Federal tax expenditure calculation +tax_expenditure_itc(t) = systemcost("inv_itc_payments_negative",t) + + systemcost("inv_itc_payments_negative_refurbishments",t) ; + +tax_expenditure_ptc(t) = systemcost("op_ptc_payments_negative",t) + + systemcost("op_co2_incentive_negative",t) ; + +raw_inv_cost(t) = sum{sys_costs_inv, systemcost(sys_costs_inv,t) } ; +raw_op_cost(t) = sum{sys_costs_op, systemcost(sys_costs_op,t) } ; + +*====================== +* Error Check +*====================== +* Objective function cost - reported system cost, adjusted for intentional differences +error_check('z') = ( + z.l + - sum{t$tmodel(t), +* Start with the system cost, then make adjustments for objective function values that are +* not intended to be in the system costs + cost_scale * (pvf_capital(t) * raw_inv_cost(t) + pvf_onm(t) * raw_op_cost(t)) +* Cost of growth penalties +* (Note: adjustments should have the same sign (+/-) as they do in the objective function) + + pvf_capital(t) * sum{(gbin,i,st)$[sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)], + cost_growth(i,st,t) * growth_penalty(gbin) * GROWTH_BIN.l(gbin,i,st,t) + * (yeart(t) - sum{tt$[tprev(t,tt)], yeart(tt) }) + }$[(yeart(t)>=model_builds_start_yr)$Sw_GrowthPenalties$(yeart(t)<=Sw_GrowthPenLastYear)] +* Small penalty to move storage into shorter duration bins + + pvf_capital(t) * sum{(i,v,r,ccseason,sdbin)$[valcap(i,v,r,t)$(storage(i) or hyd_add_pump(i))$(not csp(i))$Sw_PRM_CapCredit$Sw_StorageBinPenalty], + bin_penalty(sdbin) * CAP_SDBIN.l(i,v,r,ccseason,sdbin,t) } +* Retirement penalty + - pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$retiretech(i,v,r,t)$Sw_RetirePenalty], + cost_fom(i,v,r,t) * retire_penalty(t) + * (CAP.l(i,v,r,t) - INV.l(i,v,r,t) - INV_REFURB.l(i,v,r,t)$[refurbtech(i)$Sw_Refurb] - UPGRADES.l(i,v,r,t)$[upgrade(i)$Sw_Upgrades]) } +* Revenue from purchases of curtailed VRE + - pvf_onm(t) * sum{(r,h), CURT.l(r,h,t) * hours(h) * cost_curt(t) }$Sw_CurtMarket +* Hurdle costs + + pvf_onm(t) * sum{(r,rr,trtype)$cost_hurdle(r,rr,t), tran_hurdle_cost_ann(r,rr,trtype,t) } +* Penalty cost for dropped/excess load before Sw_StartMarkets + + pvf_onm(t) * sum{(r,h), (DROPPED.l(r,h,t) + EXCESS.l(r,h,t)) * hours(h) * cost_dropped_load } +* Retail adder for electricity consuming technologies + + pvf_onm(t) * sum{(p,i,v,r,h)$[valcap(i,v,r,t)$i_p(i,p)$h_rep(h)$Sw_RetailAdder$Sw_Prod], + hours(h) * Sw_RetailAdder * PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) } +* Account for difference in fixed O&M between model (CAP.l(i,v,r,t)) +* and outputs (cap_ivrt(i,v,r,t) * ilr(i)) for techs with more than one newv + + pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$((not one_newv(i)) or retiretech(i,v,r,t))], + cost_fom(i,v,r,t) * (CAP.l(i,v,r,t) - cap_ivrt(i,v,r,t) * ilr(i)) } +* Account for difference in fixed O&M between model (CAP.l(i,v,r,t)) +* and outputs (based on INV.l) for techs with more only one newv that cannot retire + + pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$(one_newv(i))$(not retiretech(i,v,r,t))], + cost_fom(i,v,r,t) * CAP.l(i,v,r,t) + - sum{(tt)$[inv_cond(i,v,r,t,tt)$(not retiretech(i,v,r,tt))], + INV.l(i,v,r,tt) * cost_fom(i,v,r,tt) * ilr(i) } } +* Account for difference in fixed O&M between model (CAP_ENERGY.l(i,v,r,t)) +* and outputs (based on INV_ENERGY.l) for techs with more only one newv that cannot retire + + pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$(one_newv(i))$(not retiretech(i,v,r,t))], + cost_fom_energy(i,v,r,t) * CAP_ENERGY.l(i,v,r,t) + - sum{(tt)$[inv_cond(i,v,r,t,tt)$(not retiretech(i,v,r,tt))], + INV_ENERGY.l(i,v,r,tt) * cost_fom_energy(i,v,r,tt) * ilr(i) } } +* Objective function uses cumulative interzonal transmission capex but we report +* model-year investment, so subtract the difference between the two + - ( + sum{r, systemcost_ba("inv_transmission_interzone_ac_investment",r,t) } + - sum{r, capex_transmission_interzone_ac(r,t) } + ) +* Account for difference in capital costs of objective, which use cost_cap_fin_mult, +* and outputs, which use cost_cap_fin_mult_out + + pvf_capital(t) * ( + sum{(i,v,r)$[valinv(i,v,r,t)], + cost_cap(i,t) * INV.l(i,v,r,t) + * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } + + + sum{(i,v,r)$[valinv(i,v,r,t)$battery(i)], + cost_cap_energy(i,t) * INV_ENERGY.l(i,v,r,t) + * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } + + + sum{(i,v,r)$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], + cost_upgrade(i,v,r,t) * UPGRADES.l(i,v,r,t) + * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } + + + sum{(i,v,r,rscbin)$allow_cap_up(i,v,r,rscbin,t), + cost_cap_up(i,v,r,rscbin,t) * INV_CAP_UP.l(i,v,r,rscbin,t) + * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } + + + sum{(i,v,r,rscbin)$allow_ener_up(i,v,r,rscbin,t), + cost_ener_up(i,v,r,rscbin,t) * INV_ENER_UP.l(i,v,r,rscbin,t) + * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } + + + sum{(i,v,r)$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], + cost_cap(i,t) * INV_REFURB.l(i,v,r,t) + * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } + ) +* account for penalty paid to deploy capacity beyond interconnection queue limits + + sum{(tg,r), cap_penalty(tg) * CAP_ABOVE_LIM.l(tg,r,t) } + } +) / z.l ; + +*Round error_check for z because of small number differences that always show up due to machine rounding and tolerances +error_check('z') = round(error_check('z'), 6) ; + +* Check to see is any generation or capacity from dissallowed resources +error_check("gen") = sum{(i,v,r,allh,t)$[not valgen(i,v,r,t)], GEN.l(i,v,r,allh,t) } ; +error_gen(i,v,r,allh,t)$[not valgen(i,v,r,t)] = GEN.l(i,v,r,allh,t) ; +error_check("cap") = sum{(i,v,r,t)$[not valcap(i,v,r,t)], CAP.l(i,v,r,t) } ; +error_check("RPS") = sum{(RPSCat,i,st,ast,t)$[(not RecMap(i,RPSCat,st,ast,t))$[(not stfeas(ast)) or not sameas(ast,"voluntary")]], RECS.l(RPSCat,i,st,ast,t) } ; +error_check("OpRes") = sum{(ortype,i,v,r,h,t)$[not valgen(i,v,r,t)], OPRES.l(ortype,i,v,r,h,t) } ; +error_check("m_rsc_dat") = sum{(r,i,rscbin)$m_rsc_dat(r,i,rscbin,"cap"), m_rsc_dat_init(r,i,rscbin) - m_rsc_dat(r,i,rscbin,"cap") } ; + +* Check to make sure there's no dropped/excess load in or after Sw_StartMarkets +error_check("dropped") = sum{(r,h,t)$[yeart(t)>=Sw_StartMarkets], DROPPED.l(r,h,t) } ; +error_check("excess") = sum{(r,h,t)$[yeart(t)>=Sw_StartMarkets], EXCESS.l(r,h,t) } ; + +* Report DROPPED and EXCESS variable levels +dropped_load(r,h,t) = DROPPED.l(r,h,t) ; +excess_load(r,h,t) = EXCESS.l(r,h,t) ; + +*====================== +* Transmission +*====================== + +invtran_out(r,rr,trtype,t)$routes_inv(r,rr,trtype,t) = INVTRAN.l(r,rr,trtype,t) ; + +tran_cap_energy(r,rr,trtype,t)$routes(r,rr,trtype,t) = CAPTRAN_ENERGY.l(r,rr,trtype,t) ; +tran_cap_prm(r,rr,trtype,t)$routes(r,rr,trtype,t) = CAPTRAN_PRM.l(r,rr,trtype,t) ; +tran_cap_grp(transgrp,transgrpp,t)$trancap_init_transgroup(transgrp,transgrpp,"AC") + = CAPTRAN_GRP.l(transgrp,transgrpp,t) ; + +tran_out(r,rr,trtype,t)$[(ord(r)2}'.format) + return dfout_month + + +def postprocess_outputs(case, outputs_path=None, verbose=0): + ## Parse inputs + _outputs_path = os.path.join(case, 'outputs') if outputs_path is None else outputs_path + + ## System cost + reeds.output_calc.calc_systemcost(case).to_csv( + os.path.join(_outputs_path, 'post_systemcost_annualized.csv'), + index=False, + ) + + ## Reinforcement and spur-line + reeds.output_calc.calc_reinforcement_spur_capacity_miles(case).to_csv( + os.path.join(_outputs_path, 'post_tech_transmission.csv'), + index=False, + ) + + ## Hydrogen prices by month + sw = reeds.io.get_switches(case) + try: + dfin_timestamp = reeds.timeseries.timeslice_to_timestamp(case, 'h2_price_h') + dfout_month = timestamp_to_month(dfin_timestamp) + dfout_month.rename(columns={'Value':'$2004/kg'}).to_csv( + os.path.join(_outputs_path, 'h2_price_month.csv'), index=False) + except Exception: + if int(sw.GSw_H2): + print(traceback.format_exc()) + + +#%% Procedure +if __name__ == '__main__' and not hasattr(sys, 'ps1'): + tic = datetime.datetime.now() + # parse arguments + parser = argparse.ArgumentParser( + description="Convert ReEDS run results from gdx to specified filetype" + ) + parser.add_argument("case", help="ReEDS scenario name") + parser.add_argument('--csv', '-c', action='store_true', help='write csv files') + parser.add_argument('--xlsx', '-x', action='store_true', help='write xlsx file') + + args = parser.parse_args() + case = args.case + write_csv = args.csv + write_xlsx = args.xlsx + + # #%% Inputs for debugging + # case = os.path.join(reeds_path, 'runs', 'v20250312_scheduledM0_Pacific') + # write_csv = False + # write_xlsx = False + + #%% Set up logger + reeds_path = os.path.abspath(os.path.dirname(__file__)) + log = reeds.log.makelog(scriptname=__file__, logpath=os.path.join(case, "gamslog.txt")) + + print("Starting report_dump.py") + + # %%### Parse inputs and get switches + outputs_path = os.path.join(case, "outputs") + + ### Get switches + sw = reeds.io.get_switches(case) + + ### Get new file names if applicable + dfparams = pd.read_csv( + os.path.join(case, "e_report_params.csv"), + comment="#", + index_col="param", + ) + rename = dfparams.loc[~dfparams.output_rename.isnull(), "output_rename"].to_dict() + ## drop the indices + rename = {k.split("(")[0]: v for k, v in rename.items()} + print(f"renamed parameters: {rename}") + + # %%### Write results for each gdx file + ### outputs gdx + print("Loading outputs gdx") + dict_out = gdxpds.to_dataframes( + os.path.join(outputs_path, f"rep_{os.path.basename(case)}.gdx") + ) + print("Finished loading outputs gdx") + + write_dfdict( + dfdict=dict_out, + outputs_path=outputs_path, + write_csv=write_csv, + write_xlsx=write_xlsx, + rename=rename, + ) + + ### powerfrac results + if int(sw.GSw_calc_powfrac): + print("Loading powerfrac gdx") + dict_powerfrac = gdxpds.to_dataframes( + os.path.join(outputs_path, f"rep_powerfrac_{os.path.basename(case)}.gdx") + ) + print("Finished loading powerfrac gdx") + + dfdict_to_csv( + dict_powerfrac, + outputs_path=outputs_path, + rename=rename, + ) + + + #%% Special handling of particular outputs + postprocess_outputs(case, outputs_path=outputs_path) + + + #%% All done + print("Completed report_dump.py") + try: + toc(tic=tic, year=0, path=case, process="report_dump.py") + except NameError: + print("reeds/log.py not found, so not logging output") diff --git a/reeds/core/terminus/report_params.csv b/reeds/core/terminus/report_params.csv new file mode 100644 index 00000000..92d20d84 --- /dev/null +++ b/reeds/core/terminus/report_params.csv @@ -0,0 +1,276 @@ +# Full-line and line-end comments can be indicated with # (but use the comment column when possible),,,,, +# This parameter list is in alphabetical order - please add new entries that way,,,,, +param,units,comment,reeds2x,output_rename,input +"acp_purchases_out(rpscat,st,t)",MWh,Annual alternative compliance credits from the variable ACP_PURCHASES,,, +"avg_cf(i,v,r,t)",frac,Annual average capacity factor for rsc technologies,,, +"avg_avail(i,v,r)",frac,Annual average avail factor,,, +"bioused_out(bioclass,r,t)",dry tons (imperial),biomass used by class in each model region (-> bioused.csv),,, +"bioused_usda(bioclass,usda_region,t)",dry tons (imperial),biomass used by class in each USDA region,,, +"state_cap_and_trade_price(st,t)",$/metric ton,marginal from state annual CO2 cap constraints,,, +"state_cap_and_trade_quant(st,t)",metric tons,state annual CO2 cap constraints,,, +"cap_above_limit(tg,r,t)",MW,Near-term capacity deployed above interconnection queue limit,,, +"cap_avail(i,r,t,rscbin)",MW,Available capacity at beginning of model year for rsc techs,,, +"cap_exog(i,v,r,t)",MW,Exogenous capacity from m_capacity_exog; used by reeds_to_rev,,, +"cap_firm(i,r,ccseason,t)",MW,firm capacity that counts toward the reserve margin constraint by BA and season,,, +"cap_nat(i,t)",MW,national capacity,,, +"cap_new_cc(i,r,ccseason,t)",MW,"new capacity that is VRE, for new capacity credit calculation",,, +"cc_new(i,r,ccseason,t)",frac,capacity credit for new VRE techs,,, +"cap_converter_out(r,t)",MW,AC/DC converter capacity,1,, +"cap_ivrt(i,v,r,t)",MW,"undegraded power capacity by tech, year, region, and class",1,, +"cap_energy_ivrt(i,v,r,t)",MWh,"undegraded energy capacity by tech, year, region, and class",1,, +"cap_sdbin_out(i,r,ccseason,sdbin,t)",MW,binned storage capacity by year,,, +"cap_deg_ivrt(i,v,r,t)",MW,"Degraded capacity, equal to CAP.l",,, +"cap_new_ann(i,r,t)",MW/yr,new annual capacity by region,,, +"cap_new_ann_nat(i,t)",MW/yr,new annual capacity national,,, +"cap_new_bin_out(i,v,r,t,rscbin)",MW,capacity of built techs,,, +"cap_new_ivrt(i,v,r,t)",MW,new capacity,1,, +"cap_new_ivrt_refurb(i,v,r,t)",MW,new refurbished capacity,,, +"cap_new_out(i,r,t)",MW,"new capacity by region, which are investments and upgrades from one solve year to the next",,, +"cap_energy_new_out(i,v,r,t)",MWh,"new energy capacity by region, which are investments from one solve year to the next",,, +"cap_out(i,r,t)",MW,capacity by region,1,cap, +"cap_out_ivrt(i,v,r,t)",MW,capacity by region and vintage,,, +"capacity_offline(i,r,allh,t)",MW,capacity offline due to forced or scheduled outages,,, +"capex_ivrt(i,v,r,t)",$,"capital expenditure for new capacity, no ITC/depreciation/PTC reductions",,, +"cap_upgrade(i,r,t)",MW,upgraded capacity by region,,, +"cap_upgrade_ivrt(i,v,r,t)",MW,upgraded capacity by region and vintage,,, +"CO2_CAPTURED_out(r,allh,t)",metric tons/hour,amount of CO2 captured_out from DAC and CCS technologies,,, +"CO2_STORED_out(r,cs,allh,t)",metric tons/hour,amount of CO2 stored_out underground,,, +"CO2_CAPTURED_out_ann(r,t)",metric tons,amount of CO2 captured_out from DAC and CCS technologies,,, +"CO2_STORED_out_ann(r,cs,t)",metric tons,amount of CO2 stored_out underground,,, +"CO2_TRANSPORT_INV_out(r,rr,t)",metric ton-hours,investment in interregional CO2 transport_out capacity,,, +"CO2_SPURLINE_INV_out(r,cs,t)",metric ton-hours,investment in spur line CO2 transport capacity between BAs and reservoirs,,, +"CO2_FLOW_out(r,rr,allh,t)",metric tons/hour,gross interregional flow of CO2 by timeslice,,, +"CO2_FLOW_out_ann(r,rr,t)",metric tons,gross interregional flow of CO2,,, +"CO2_FLOW_pos_out(r,rr,allh,t)",metric tons/hour,positive interregional flow of CO2 from region r to rr by timeslice,,, +"CO2_FLOW_pos_out_ann(r,rr,t)",metric tons,positive interregional flow of CO2 from region r to rr,,, +"CO2_FLOW_neg_out(r,rr,allh,t)",metric tons/hour,negative interregional flow of CO2 from region r to rr by timeslice (reported as a negative value),,, +"CO2_FLOW_neg_out_ann(r,rr,t)",metric tons,negative interregional flow of CO2 from region r to rr (reported as a negative value),,, +"CO2_FLOW_net_out(r,rr,allh,t)",metric tons/hour,net interregional flow of CO2 from region r to rr by timeslice,,, +"CO2_FLOW_net_out_ann(r,rr,t)",metric tons,net interregional flow of CO2 from region r to rr,,, +co2_price(t),$/metric ton,marginal from national annual CO2 cap constraint (eq_annual_cap),1,, +"cost_cap_fin_mult(i,r,t)",frac,final capital cost multiplier for regions and technologies - used in the objective function,,, +"cost_vom_rr(i,v,rr,t)",$/MWh,"vom cost for all regions, including resource regions",,, +"curt_tech(i,r,t)",MWh,annual curtailment resolved by technology,,, +"curt_h(r,allh,t)",MW,curtailment from VRE generators,,, +"curt_ann(r,t)",MWh,annual curtailment from VRE generators by region,,, +curt_rate(t),frac,fraction of VRE generation that is curtailed,,, +"curt_rate_tech(i,r,t)",frac,annual curtailment resolved by technology,,, +"curt_rr(i,r,allh,t)",frac,"curtailment fraction for all regions, including resource region",,, +"gen_new_uncurt(i,r,allh,t)",MWh,uncurtailed generation from new VRE techs,,, +"curt_new(i,r,allh,t)",frac,curtailment frac for new VRE techs,,, +"cc_all_out(i,v,r,ccseason,t)",frac,combined cc_int and m_cc_mar output,,, +"dropped_load(r,allh,t)",MW,level values of DROPPED variable for dropped load,,, +"emit_captured_irt(i,r,t)",metric tons,CO2 emissions captured by tech and region,,, +"emit_captured_nat(i,t)",metric tons,"CO2 emissions captured, national",,, +"emit_irt(etype,eall,i,r,t)",metric tons,"emissions by pollutant, tech, and region",,, +"emit_nat(etype,eall,t)",metric tons,"emissions by pollutant, national",,, +"emit_nat_tech(etype,eall,i,t)",metric tons,"emissions by pollutant and tech, national",,, +"emit_r(etype,eall,r,t)",metric tons,"emissions by pollutant, regional",,, +"emit_rate_regional(r,t)",metric tons,"regional average CO2 emissions rate, used in state CO2 caps",,, +"emit_weighted(etype,eall)",metric tons * pvf,national emissions * pvf_onm,,, +error_check(*),unitless,set of checks to determine if there is an error - values should be zero if there is no error,,, +"error_gen(i,v,r,allh,t)",MWh,"erroneous generation that disobeys valgen(i,v,r,t)",,, +"excess_load(r,allh,t)",MW,level values of EXCESS variable for surplus load,,, +"expenditure_flow(*,r,rr,t)",2004$,expenditures from flows of * moving from r to rr,,, +"expenditure_flow_rps(st,ast,t)",2004$,expenditures from trades of RECS from st to ast,,, +"expenditure_flow_int(r,t)",2004$,expenditures from exogenous international imports/exports,,, +"flex_load_out(flex_type,r,allh,t)",MWh,flexible load consumed in each timeslice,,, +forced_outage(i),fraction,average forced outage rate (h-weighted; r-unweighted) by technology during representative periods,,, +planned_outage(i),fraction,average scheduled outage rate (h-weighted) by technology during representative periods,,, +"gen_ivrt_uncurt(i,v,r,t)",MWh,annual uncurtailed generation from VREs,,, +"gen_ivrt(i,v,r,t)",MWh,annual generation,,, +"gen_h(i,r,allh,t)",MW,generation by timeslice with charge and production load as negative generation,1,, +"gen_h_nat(i,allh,t)",MW,national generation by timeslice with charge and production load as negative generation,,, +"gen_h_stress(i,r,allh,t)",MW,generation by stress timeslice with charge and production load as negative generation,,, +"gen_h_stress_nat(i,allh,t)",MW,national generation by stress timeslice with charge and production load as negative generation,,, +"gen_ann(i,r,t)",MWh,annual generation with charge and production load as negative generation,,, +"gen_ann_nat(i,t)",MWh,"annual generation with charge and production load as negative generation, nationwide",,, +"gen_uncurtailed(i,r,t)",MWh,annual generation where VRE uses uncurtailed gen and storage uses GEN,,, +"gen_uncurtailed_nat(i,t)",MWh,national annual generation where VRE uses uncurtailed gen and storage uses GEN,,, +"gen_rsc(i,v,r,t)",MWh/MW,Annual generation per MW from rsc techs,,, +"h2_demand_by_sector(h2_demand_type,t)",metric tons,national H2 demand by use,,, +"h2_inout(h2_stor,r,allh,t,*)",metric tons/hour,H2 moving in/out of storage,,, +"h2_price_h(r,allh,t)",$2004/kg,Marginal cost of H2 by region and timeslice,,, +"h2_price_szn(r,allszn,t)",$2004/kg,Marginal cost of H2 by region and season,,, +"h2_ptc_generation(i,v,r,allh,t)",MWh,generation of clean electricity for electrolyzers receiving the hydrogen production tax credit (derived from CREDIT_H2PTC variable),,, +"h2_ptc_marginal_region(h2ptcreg,t)",$2004/MWh,marginal cost of producing an Energy Attribute Credit with regional and annual matching,,, +"h2_ptc_marginal_region_hour(h2ptcreg,allh,t)",$2004/MWh,"marginal cost of producing an Energy Attribute Credit with regional, hourly and annual matching",,, +"h2_storage_level(h2_stor,r,actualszn,allh,t)",metric tons,H2 storage level by timeslice,,, +"h2_storage_level_szn(h2_stor,r,actualszn,t)",metric tons,H2 storage level by season,,, +"h2_storage_cap(h2_stor,r,t)",metric tons,total H2 storage capacity by BA and type,,, +"h2_trans_cap(r,rr,t)",metric tons/hour,H2 pipeline capacity between BAs,,, +"h2_trans_flow(r,rr,allh,t)",metric tons/hour,net flow of H2 between BAs,,, +"h2_trans_flow_all(r,rr,allh,t)",metric tons/hour,flow of H2 between BAs,,, +"h2_usage(r,allh,t)",metric tons/hour,total H2 usage by hour (H2-CT/CC and non-electric H2 consumption),,, +"load_cat(loadtype,r,t)",MWh,Annual exogenous load by category,,, +"load_rt(r,t)",MWh,Annual exogenous load,,, +"load_stress(r,allh,t)",MW,Timeslice load during stress periods,,, +"load_frac_rt(r,t)",fraction,Fraction of LOAD in each region,,, +"loadsite_cap(r,t)",MW,Capacity of flexibly sited load,,, +"loadsite_op(r,allh,t)",MW,Operations of flexibly sited load,,, +"invtran_out(r,rr,trtype,t)",MW,new transmission capacity,1,, +"lcoe(i,v,r,t,rscbin)",$/MWh,levelized cost of electricity for all tech options,,, +"lcoe_built(i,r,t)",$/MWh,levelized cost of electricity for technologies that were built,,, +"lcoe_built_nat(i,t)",$/MWh,national average levelized cost of electricity for technologies that were built,,, +"lcoe_cf_act(i,v,r,t,rscbin)",$/MWh,LCOE using actual (instead of max) capacity factors,,, +"lcoe_fullpol(i,v,r,t,rscbin)",$/MWh,"LCOE considering full ITC and PTC value, whereas the LCOE parameter considers the annualized objective function",,, +"lcoe_nopol(i,v,r,t,rscbin)",$/MWh,LCOE without considering ITC and PTC adjustments,,, +"lcoe_pieces(lcoe_cat,i,r,t)",varies,levelized cost of electricity elements for technologies that were built,,, +"lcoe_pieces_nat(lcoe_cat,i,t)",varies,national average levelized cost of electricity elements for technologies that were built,,, +"losses_ann(*,t)",MWh,annual losses by category,1,, +"losses_tran_h(rr,r,allh,trtype,t)",MW,transmission losses by timeslice,,, +objfn_raw,2004$ in net present value terms,the raw objective function value,,, +"opRes_supply_h(ortype,i,r,allh,t)",MW,supply of operating reserves by timeslice and region,,, +"opRes_supply(ortype,i,r,t)",MW-h,annual supply of operating reserves by region,,, +"opres_trade(ortype,r,rr,t)",MW-h,total annual trade of operating reserves between sending region (r) and receiving region (rr),,, +"peak_load_adj(r,ccseason,t)",MWh,peak load adjustment for each ccseason,,, +"prm(r,t)",fraction,final planning reserve margin for each BA in year t,,, +"prod_load(i,r,allh,t)",MW,additional load from production activities,,, +"prod_load_ann(i,r,t)",MWh/year,additional annual load from production activities,,, +"prod_cap(i,v,r,t)",metric tons/hour,"production capacity, note unit change from MW to metric tons/hour",,, +"prod_produce(i,r,allh,t)",metric tons/hour,"production activities by technology, BA, and timeslice",,, +"prod_produce_ann(i,r,t)",metric tons/year,annual production by technology and BA,,, +"prod_h2_price(p,t)",$2004/metric ton,annual national average marginal cost of producing H2 (see also h2_price_h and h2_price_szn),,, +"prod_h2comb_cost(p,t)",$2004/mmbtu,marginal cost of fuels used for H2-CT/CC combustion,,, +"prod_syscosts(sys_costs,i,r,t)",2004$,BA- and tech-specific investment and operation costs associated with production activities,,, +"prod_SMR_emit(e,r,t)",metric tons,emissions from SMR activities,,, +"ptc_out(i,v,t)",2004$/MWh,"value of the ptc, equal to ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t)",1,, +raw_inv_cost(t),2004$,sum of investment costs from systemcost,,, +raw_op_cost(t),2004$,sum of operational costs from systemcost,,, +"rec_outputs(RPSCat,i,st,ast,t)",MWh,quantity of RECs served from state st to state ast,,, +"reduced_cost(i,v,r,t,*,*)",2004$/kW undiscounted,the reduced cost of each investment option. All non-rsc are assigned to nobin,,, +RE_gen_price_nat(t),2004$/MWh,marginal cost of the national RE generation constraint,,, +"repbioprice(r,t)",2004$/MMBtu,highest marginal bioprice of utilized bins for each region,,, +"repgasprice(cendiv,t)",2004$/MMBtu,highest marginal gas price of utilized gas bins for each census division,,, +"repgasprice_r(r,t)",2004$/MMBtu,highest marginal gas price of utilized gas bins for each region,1,, +repgasprice_nat(t),2004$/MMBtu,weighted-average national natural gas price assuming that plants pay the marginal price,,, +"repgasquant(cendiv,t)",Quads,quantity of gas consumed in each census division,,, +"repgasquant_irt(i,r,t)",Quads,quantity of gas consumed by tech and region,,, +repgasquant_nat(t),Quads,national consumption of natural gas,,, +"reqt_price(*,*,r,*,t)",varies,Price of requirements,,, +"reqt_price_sys(*,*,*,t)",varies,System-average price of requirements,,, +"reqt_quant(*,*,r,*,t)",varies,Requirement quantity,,, +"reqt_quant_sys(*,*,*,t)",varies,System-wide requirement quantity,,, +"ret_ann(i,r,t)",MW/yr,annual retired capacity by region,,, +"ret_ann_nat(i,t)",MW/yr,annual retired capacity national,,, +"ret_ivrt(i,v,r,t)",MW,retired capacity by region and vintage,1,, +"ret_out(i,r,t)",MW,retired capacity by region,,, +"revenue(rev_cat,i,r,t)",2004$,sum of revenues,,, +"revenue_nat(rev_cat,i,t)",2004$,sum of revenues,,, +"revenue_en(rev_cat,i,r,t)",2004$/MWh,revenues per MWh of generation,,, +"revenue_en_nat(rev_cat,i,t)",2004$/MWh,revenues per MWh of generation,,, +"revenue_cap(rev_cat,i,r,t)",2004$/MW,revenues per MW of capacity,,, +"revenue_cap_nat(rev_cat,i,t)",2004$/MW,revenues per MW of capacity,,, +"site_cap(i,x,t)",MW,capacity by reV site,,, +"site_spurcap(x,t)",MW,spur-line capacity to reV site,,, +"site_spurinv(x,t)",MW,spur-line investment at reV site,,, +"site_gir(i,x,t)",MWgen/MWspur,Generator-to-interconnection ratio by tech,,, +"site_hybridization(x,t)",unitless,"Hybridization factor: 0 for all-PV or all-wind, 1 for 50:50 PV:wind",,, +"site_pv_fraction(x,t)",MWpv/MWgen,Fraction of capacity at reV site that is PV,,, +rggi_price(t),2004$/metric ton,shadow price from RGGI constraint,,, +rggi_quant(t),metric tons,annual CO2 cap for the regional greenhouse gas initiative (RGGI),,, +"stor_energy_cap(i,v,r,t)",MWh,"energy capacity of storage devices by tech, BA, vintage, and year",,, +"storage_duration_out(i,v,r,t)",h,"storage duration of battery",,, +"stor_inout(i,v,r,t,*)",MWh,Annual energy going into and out of storage,,, +"stor_in(i,v,r,allh,t)",MW,energy going into storage by timeslice,,, +"stor_interday_level(i,v,r,allszn,t)",MWh,storage level for inter-day technogies,,, +"stor_interday_dispatch(i,v,r,allh,t)",MW,storage net dispatch for inter-day technogies,,, +"stor_level(i,v,r,allh,t)",MWh,storage level,,, +"stor_out(i,v,r,allh,t)",MW,energy leaving storage,,, +# Begin system cost parameters,,,,, +"systemcost(sys_costs,t)",2004$,"reported system cost for each component, where inv costs are model year present value and op costs are single year",,, +"systemcost_bulk(sys_costs,t)",2004$,"reported system cost for each component, where inv costs are model year present value and op costs are model year present value until next model year",,, +"systemcost_bulk_ew(sys_costs,t)",2004$,"same as systemcost_bulk, but the end year is only 1 year of operation and CRF times the investment",,, +"systemcost_ba(sys_costs,r,t)",2004$,"reported ba-level system cost for each component, where inv costs are model year present value and op costs are single year",,, +"systemcost_ba_bulk(sys_costs,r,t)",2004$,"reported system cost for each component, where inv costs are model year present value and op costs are model year present value until next model year",,, +"systemcost_ba_bulk_ew(sys_costs,r,t)",2004$,"same as systemcost_ba_bulk, but the end year is only 1 year of operation and CRF times the investment",,, +"systemcost_ba_retailrate(sys_costs,r,t)",2004$,"same as systemcost_ba, but with outputs adapted for the retailrate module",,, +"systemcost_techba(sys_costs,i,r,t)",2004$,"reported tech|ba-level system cost for each component, where inv costs are model year present value and op costs are single year",,, +"systemcost_techba_bulk(sys_costs,i,r,t)",2004$,"reported tech|ba-level system cost for each component, where inv costs are model year present value and op costs are model year present value until next model year",,, +"systemcost_techba_bulk_ew(sys_costs,i,r,t)",2004$,"same as systemcost_techba_bulk, but the end year is only 1 year of operation and CRF times the investment",,, +# end system cost parameters,,,,, +tax_expenditure_itc(t),2004$,ITC tax expenditure,,, +tax_expenditure_ptc(t),2004$,PTC tax expenditure,,, +"tran_flow_all_rep(r,rr,allh,trtype,t)",MW,transmission flows between regions by representative time slice,,, +"tran_flow_all_stress(r,rr,allh,trtype,t)",MW,transmission flows between regions by stress time slice,,, +"tran_flow_rep(r,rr,allh,trtype,t)",MW,"power flow along each transmission line, losses NOT included",,, +"tran_flow_rep_ann(r,rr,trtype,t)",MWh,"annual net energy flow across each transmission interface, losses NOT included",,, +"tran_flow_stress(r,rr,allh,trtype,t)",MW,transmission flows between regions during stress periods,,, +"tran_util_h_rep(r,rr,allh,trtype,t)",fraction,Fractional transmission route utilization by timeslice during representative periods,,, +"tran_util_h_stress(r,rr,allh,trtype,t)",fraction,Fractional transmission route utilization by timeslice during stress periods,,, +"tran_util_ann_rep(r,rr,trtype,t)",fraction,Fractional transmission route utilization by year during representative periods,,, +"tran_util_ann_stress(r,rr,trtype,t)",fraction,Fractional transmission route utilization by year during stress periods,,, +"net_import_h_rep(r,allh,t)",MW,Net power imports (imports - exports with losses on imports) by timeslice during representative periods,,, +"net_import_h_stress(r,allh,t)",MW,Net power imports (imports - exports with losses on imports) by timeslice during stress periods,,, +"net_import_ann_rep(r,t)",MWh,Net energy imports (imports - exports with losses on imports) by year during representative periods,,, +"net_import_ann_stress(r,t)",MWh,Net energy imports (imports - exports with losses on imports) by year during stress periods,,, +"import_h_rep(r,allh,t)",MW,Power imports (with losses) by timeslice for representative periods,,, +"export_h_rep(r,allh,t)",MW,Power exports (no losses) by timeslice for representative periods,,, +"import_ann_rep(r,t)",MWh,Energy imports (with losses) by year for representative periods,,, +"export_ann_rep(r,t)",MWh,Energy exports (no losses) by year for representative periods,,, +"poi_capacity(r,t)",MW,total point-of-connection capacity (used for intra-zone transmission network reinforcement),,, +"tran_hurdle_cost_ann(r,rr,trtype,t)",2004$,annual monetary value associated with hurdle rates on transmission flows,,, +"tran_mi_out(trtype,t)",MW-mi,total transmission capacity*distance for energy trading,,, +"tran_prm_mi_out(trtype,t)",MW-mi,total transmission capacity*distance for capacity trading,,, +"tran_mi_out_detail(r,rr,trtype,t)",MW-mi,total transmission capacity by distance between region,,, +"tran_cap_energy(r,rr,trtype,t)",MW,total transmission capacity for energy trading,,, +"tran_cap_prm(r,rr,trtype,t)",MW,total transmission capacity for PRM trading,,, +"tran_cap_grp(transgrp,transgrpp,t)",MW,transmission flow limit between transgrp regions,,, +"tran_out(r,rr,trtype,t)",MW,"total transmission capacity for energy trading, averaging over forward and reverse directions",1,, +"tran_prm_out(r,rr,trtype,t)",MW,"total transmission capacity for PRM trading, averaging over forward and reverse directions",,, +"captrade(r,rr,trtype,ccseason,t)",MW,planning reserve margin capacity traded from r to rr,,, +"gasshare_ba(r,cendiv,t)",unitless,share of natural gas consumption in BA relative to corresponding cendiv consumption,,, +"gasshare_techba(i,r,cendiv,t)",unitless,share of natural gas consumption in tech-BA combination relative to corresponding cendiv consumption,,, +"gasshare_cendiv(cendiv,t)",unitless,share of natural gas consumption in cendiv relative to national consumption,,, +"bioshare_techba(i,r,t)",unitless,share of biofuel consumption in tech-BA combination relative to total BA biofuel consumption,,, +"gascost_cendiv(cendiv,t)",2004$,natual gas fuel cost at cendiv level,,, +"valnew(*,*,*,t)",varies,value of new investments,,, +"water_withdrawal_ivrt(i,v,r,t)",Mgal,"water withdrawal by tech, year, region, and class",,, +"water_consumption_ivrt(i,v,r,t)",Mgal,"water consumption by tech, year, region, and class",,, +"watcap_ivrt(i,v,r,t)",Mgal,"water capacity by tech, year, region, and class",,, +"watcap_out(i,r,t)",Mgal,water capacity by region,,, +"watcap_new_ivrt(i,v,r,t)",Mgal,new water capacity,,, +"watcap_new_out(i,r,t)",Mgal,"new water capacity by region, which are investments from one solve year to the next",,, +"watcap_new_ann_out(i,v,r,t)",Mgal/yr,new annual water capacity by region,,, +"watret_ivrt(i,v,r,t)",Mgal,retired water capacity by region and vintage,,, +"watret_out(i,r,t)",Mgal,retired water capacity by region,,, +"watret_ann_out(i,v,r,t)",Mgal/yr,annual retired water capacity by region,,, +# Parameters defined earlier in model - some are used in r2x,,,,, +bcr,,,,,1 +biosupply,,,,,1 +cap_hyd_szn_adj,,,,,1 +capture_rate,,,,,1 +cf_adj_t,,,,,1 +cf_hyd,,,,,1 +cost_cap,,,,,1 +cost_cap_energy,,,,,1 +cost_cap_fin_mult,,,,,1 +cost_cap_fin_mult_noITC,,,,,1 +cost_hurdle,,,,,1 +cost_scale,,,,,1 +cost_vom,,,,,1 +cendiv,,,,,1 +degrade_annual,,,,,1 +e,,,,,1 +emit_rate,,,,,1 +fuel_price,,,,,1 +fuel2tech,,,,,1 +h_szn,,,,,1 +heat_rate,,,,,1 +hierarchy,,,,,1 +hours,,,,,1 +hydmin,,,,,1 +ilr,,,,,1 +outage_scheduled_h,,,,,1 +pvf_capital,,,,,1 +pvf_onm,,,,,1 +r,,,,,1 +rsc_dat,,,,,1 +storage_duration,,,,,1 +storage_eff,,,,,1 +szn_stress_t,,,,,1 +tc_phaseout_mult,,,,,1 +tranloss,,,,,1 +v,,,,,1 +valcap_i,,,,,1 +z_rep,,,,,1 diff --git a/reeds/hpc/aws_setup.sh b/reeds/hpc/aws_setup.sh new file mode 100644 index 00000000..fcc250f4 --- /dev/null +++ b/reeds/hpc/aws_setup.sh @@ -0,0 +1,135 @@ +# Some good instances +# m5a.12xlarge 48 x86_64 192 - - 10 Gigabit 2.064 USD per Hour +# r5a.24xlarge 96 x86_64 768 - - 20 Gigabit 5.424 USD per Hour + +#general process +#GIT installation +sudo yum -y install git +sudo amazon-linux-extras install epel +sudo yum -y install git-lfs +git lfs install + +#Append two export commands for GAMS and conda to .bashrc: +echo "export GAMSDIR=/opt/gams/gams35.1_linux_x64_64_sfx" >> ~/.bashrc +echo "export PATH=/home/ec2-user/anaconda3/bin/:$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/" >> ~/.bashrc + +#if this is a new disk, you need to establish a file system +sudo mkfs -t xfs /dev/xvdo +# copy over the gams license file from your local machine to the gams system directory +# this directory was created in the last step and varies with the gams installation version +#!!!! this needs to be changed for each user - both the directory names and gams license file +#!!!! following command need to be run from local terminal any time you mount or create a new file system on a drive +#scp -i MB_R2.pem -r /Users/mbrown1/Desktop/gamslice.txt ec2-user@172.18.32.113:~/r2/gamslice.txt + +#need to make a directory to mount that volume.. here just setting it up as ~/r2 +sudo mkdir ~/r2 +#then mount the directory to that folder: +#!!!! depends on what drive letter you assigned in ec2 web interface +#!!!! here i defined my drive as /dev/sdo +sudo mount /dev/sdo ~/r2 + +#make sure you have ownership of the drive and the opt directory (where we'll install GAMS): +sudo chown -R ec2-user ~/r2 +sudo chown -R ec2-user /opt +# make a directory for gams +mkdir /opt/gams +# change to that directory and download the gams installer +cd /opt/gams +#!!!! alternatively, this could be stored on your EBS drive and copied over +wget "https://d37drm4t2jghv5.cloudfront.net/distributions/35.1.0/linux/linux_x64_64_sfx.exe" + +# change permissions for the installation file +chmod 755 linux_x64_64_sfx.exe +# unpack the installation file +# not sure why the entire directory needs to spelled out here but it does... +/opt/gams/linux_x64_64_sfx.exe + +#copy over the gams license stored on your drive +cd gams35.1_linux_x64_64_sfx +nano ~/r2/gamslice.txt + +#add license file contents + +cp ~/r2/gamslice.txt gamslice.txt + +#export GAMSDIR for GDXPDS +export GAMSDIR=/opt/gams/gams35.1_linux_x64_64_sfx + +#installing anaconda +#following step only needs to be done if the installation file is not on your drive +cd ~/r2 +#!!!! alternatively, this could be stored on your EBS drive +wget "https://repo.anaconda.com/archive/Anaconda3-2020.11-Linux-x86_64.sh" + +#create a temporary directory given need to read/write from non-write-protected directory +mkdir ~/tmp +chown -R ec2-user ~/tmp +#actual installation call +#note setting up the temporary directory for this call and putting installation files +# in the /home/ec2-user/anaconda3 directory - the 'b' and 'p' arguments make it silent +# and indicate that the directory specified is the installation path +TMPDIR=~/tmp sh /home/ec2-user/r2/Anaconda3-2020.11-Linux-x86_64.sh -b -p /home/ec2-user/anaconda3 + +#ordering matters here - we want to be certain that +#the system sees the conda version of python before +#any other - both GAMS and otherwise +export PATH=/home/ec2-user/anaconda3/bin/:$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/ + +#make sure you have the appropriate packages installed +conda install pandas numpy scipy scikit-learn matplotlib networkx numba +pip install gdxpds + +#------------------------ +# -- git instructions -- +#------------------------ + +ssh-keygen -t rsa -b 4096 -C "youremailaddress@nlr.gov" +eval "$(ssh-agent -s)" +ssh-add id_rsa +#[copy key and add to your github.nrel.gov account] +ssh -T git@github.nrel.gov +#type yes +# should say Hi [username]! ... +git clone https://github.nrel.gov/ReEDS/ReEDS-2.0.git reeds + +git clone git@github.nrel.gov:ReEDS/ReEDS-2.0 + + +# Run ReEDS! +# (using nohup to keep the process from dying when you end your ssh session) +#nohup python runbatch_aws.py -c weekendcentroid -r 4 -b centwknd > myout.txt & + + +#======================================== +# -- old but potentially useful lines -- +#======================================== + +#Following lines needed if using the gams version of python... +#these export path lines could all be wrapped together +#but it helps me to break them out to avoid one big line +#export PATH=$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/ +#following instructs to use the gams version of python +#allows us to avoid installing/configuring conda +#ordering matters here! - we want the system to +#see the GAMS version of python first +#export PATH=/opt/gams/gams35.1_linux_x64_64_sfx/GMSPython:$PATH +#add python package directory for GAMS python to path: +#export PATH=$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/GMSPython/bin +#can test to see if the following worked by typing: +#which python +#and should get something like: +#/opt/gams/gams35.1_linux_x64_64_sfx/GMSPython/python + +#install necessary python packages +#sudo yum install git +#need to manually install a base package for python +#that is not included with the gams version for some reason +#and is not available via pip ----- such a headache +#i've contacted GAMS on this.. working on a solution +#git clone https://github.com/python/cpython.git +#cp -r cpython/Lib/unittest/ /opt/gams/gams35.1_linux_x64_64_sfx/GMSPython/lib/python3.8/site-packages/unittest/ + +#move to your git directory +#!!! could be different for different users +#cd ~/r2/r2_aws + diff --git a/reeds/hpc/srun_template.sh b/reeds/hpc/srun_template.sh new file mode 100644 index 00000000..4bebd071 --- /dev/null +++ b/reeds/hpc/srun_template.sh @@ -0,0 +1,9 @@ +#!/bin/bash +#SBATCH --account=[your HPC allocation] +#SBATCH --time=2-00:00:00 +#SBATCH --nodes=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mail-user=[your email address] +#SBATCH --mail-type=BEGIN,END,FAIL +#SBATCH --mem=246000 # RAM in MB; up to 246000 for normal or 2000000 for bigmem on kestrel +# add >>> #SBATCH --qos=high <<< above for quicker launch at double AU cost \ No newline at end of file diff --git a/reeds/input_processing/WriteHintage.py b/reeds/input_processing/WriteHintage.py new file mode 100644 index 00000000..2414ac45 --- /dev/null +++ b/reeds/input_processing/WriteHintage.py @@ -0,0 +1,775 @@ +# -*- coding: utf-8 -*- +""" +The purpose of this script is to group existing generating +units into historical, binned vintages (hintages) + +The primary arguments are the plant data file to use, the number of bins, +and the minimum deviation across bins. There are also operations specific +to proessing data for whether or not GSw_WaterMain are enabled. + +Kmeans clustering is the default option and the general sequence, by tech and +BA combinations, is as follows: + + 1. Check to see if the number of unique units is less than the number of bins + - if true, check to see if the deviation across all those different units exceeds the minimum deviation + - if true, use the raw data and assign bins to each individual units + - if false, proceed with binning + - if false, proceed with binning + 2. Perform capacity-weighted kmeans clustering with the maximum number of bins + - Maximum number of bins first defined as the minimum of.. + - number of bins assigned by user + - number of unique heat rates + - number of units in the tech/BA combination + 3. Check to see if the deviation across all heat rate centroids exceed the minimum deviation + - if so, proceed to '4' + - if not, return to '2' but reduce the number of bins by 1 + 4. Assign units to their nearest heat rate bin + - if only one unique unit in a bin, assign its original heat rate + - if more than one unit in a bin, assign the capacity-weighted average + 5. For all years from 2010-2100, compute the remaining amount of capacity + based on the units specified retirement date and compute the remaining + units' capacity-weighted-average characteristics (FOM/VOM/HR/...) + +--- + +For testing - the default arguments are passed in to the main(...) function + +""" +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import math +import numpy as np +import os +import sys +import pandas as pd +import warnings +import datetime +from sklearn.cluster import KMeans +from sklearn.exceptions import ConvergenceWarning +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +# Time the operation of this script +tic = datetime.datetime.now() + +# ignore ConvergenceWarnings that occur in this file from the kmeans function +warnings.filterwarnings("ignore", category=ConvergenceWarning) + + +#%% =========================================================================== +### --- FUNCTIONS AND CLASSES --- +### =========================================================================== +class grouping: + def __init__(self, nbins, *args, **kwargs): + #df = tdat + #nbins=6 + if nbins == 'unit': + self.df = self.unit(*args) + elif nbins == 'group': + self.df = self.group(*args) + else: + nbins = int(nbins) + self.df = self.kmeans(nbins, *args, **kwargs) + + + def unit(self, input_df, *args, **kwargs): + ''' + This method creates a unique hintage bin for every generator + + input_df: pandas DataFrame object containing ReEDS generators + *args: collects unused positional arguments to simplify code + **kwargs: collects unused keyword arguments to simplify code + ''' + output_df = pd.DataFrame() + # NOTE: The calculations here and below in group() can probably be done + # faster by group (without a loop). + for ba in input_df.r.unique(): + ba_df = input_df[input_df.r == ba].copy() + for tech in ba_df.TECH.unique(): + df = ba_df[ba_df.TECH == tech].copy() + df['bin'] = df.reset_index(drop=True).index + 1 + output_df = pd.concat([output_df, df]) + + return output_df + + + def group(self, input_df, col, *args, **kwargs): + ''' + This method creates hintage bins for unique region, tech, and specified + column combinations. + + input_df: (pandas.DataFrame object) containing ReEDS generators + col: (str) The name of the column used in binning + *args: collects unused positional arguments to simplify code + **kwargs: collects unused keyword arguments to simplify code + ''' + grouping_df = (input_df[['r', 'TECH', col]] + .drop_duplicates() + .reset_index(drop=True) + ) + output_df = pd.DataFrame() + for ba in grouping_df.r.unique(): + ba_df = grouping_df[grouping_df.r == ba].copy() + for tech in ba_df.TECH.unique(): + tech_df = ba_df[ba_df.TECH == tech].copy() + tech_df['bin'] = tech_df.reset_index().index + 1 + + output_df = pd.concat([output_df, tech_df.reset_index(drop=True)]) + + + output_df = input_df.merge(output_df, + on=['r','TECH', col], + how='left' + ) + return output_df + + + class _kmeans: + def __init__(self, input_df, col, bins, minSpread=2000, n_init=10): + ''' + bin and return the centroids or breakpoints of each bin + + df (DataFrame object): Pandas Dataframe containing data for binning + col (str): the column name that is to be binned + bins (int): the number of desired bins + ''' + self.bins = bins + df = input_df.copy() + + #if the number of unique heat rates is already less than the number of bins + if len(df[col].unique()) <= bins: + #no matter what, set the bins to the number of unique heat rates + self.bins = len(df[col].unique()) + + # in order for us to skip binning, we'll need to check + # if the minimum difference across all unique heat rates + # exceeds the minSpread - if so, we can justify + # avoiding the binning algorithm + if len(df[col].unique()) > 1: + mindiff_unique = min([abs(j-i) for i, j in zip(df[col].unique()[:-1], + df[col].unique()[1:])]) + + #note that if we only have one unique value for this tech/BA combo + #heat rate, then we can skip binning altogether + if len(df[col].unique()) == 1: + mindiff_unique = minSpread + 1 + + # if the number of unique elements is greater than the number of bins + # make sure we skip the following condition and perform binning + else: + mindiff_unique = minSpread + 1 + + # if you can use the raw data as is - ie if the observed heat rates + # are disparate enough such that they exceed minspread and the + # the number of units is not greater than the number of bins - + # then just use the raw data + if len(df[col].unique()) <= bins and mindiff_unique > minSpread: + bins = len(df[col].unique()) + # if the maximum deviation across all heat rates + # is less than the minimum deviation + if (df[col].max() - df[col].min()) < minSpread: + # put all units into one bin + df['centroid'] = df[col].mean() + df['bin'] = 1 + else: + df['centroid'] = df[col] + temp = pd.DataFrame(data=dict(centroid=df[col].unique(), bin=None)) + temp.bin = temp.index + 1 + df = df.merge(temp, on='centroid', how='left') + self.centers = df + + # if you can't just use the raw data + else: + # if the number of bins exceeds the number of observations + # reset the number of bins to the length of the data + # note if the heat rates were not disparate enough + # we would've caught that in the previous condition block + if bins > len(df.index): + self.bins = max(len(df)-1, 1) + + # make a temporary binning DF + bin_df = pd.concat([df[col], + pd.DataFrame(columns=['centroid', 'upper', + 'lower', 'bin'])]) + bin_df.rename(columns={0: col}, inplace=True) + + # establish parameters necessary for the while loop + spread = minSpread - 1 + nbins = self.bins + + # while the minimum spread hasn't been exceeded + # and the number of bins haven't been exhausted + # keep attempting to cluster - if these conditions + # haven't been met, try again with one fewer bin + while spread < minSpread or nbins == 1: + df = input_df.copy() + + # initialize the centroids - note that the + # random_state argument implies a static seed + # for the random processes/distribution-draws + # used in the kmeans function + centroids_obj = KMeans( + n_clusters=nbins, random_state=0, max_iter=1000, n_init=n_init, + ).fit(df[[col]].to_numpy(), sample_weight = df['Summer.capacity'].to_numpy()) + + #need to convert array of length-one arrays to one long array + centroids = [ i[0] for i in centroids_obj.cluster_centers_] + + # create a list of unique centroids + centroids = list(set(centroids)) + + # make the binning matrix + k = pd.DataFrame(index=df[col], columns=centroids).reset_index() + k = k.set_index('HR') + + # compute the difference between the observed heat rate and the centroid + for c in centroids: + k[c] = abs(k.index - c) + + # select the closest centroid + k['centroid'] = k.columns[k[centroids].values.argmin(1)] + + # Merge centroids onto original DF + k_bins = k.centroid.drop_duplicates().reset_index().copy() + k_bins['bin'] = k_bins.index + 1 + k_map = k.reset_index().merge(k_bins[['centroid', 'bin']], + on='centroid', + how='left').set_index('HR') + + # find the minimum deviation across all heat rate combinations + if len(centroids)>1: + spread = min([abs(j-i) for i, j in zip(k['centroid'].unique()[:-1], + k['centroid'].unique()[1:])]) + # if there is only one centroid, + # set the conditional to exit the while loop + else: + spread = minSpread + 1 + + #reset the index for formatting + k_map.reset_index(inplace=True) + + nbins -= 1 + #end of while loop for nbins and spread < minspread check + + # merge the binned heat rates with the original plant data + # this will be the output to the kmeans function + self.centers = df.merge(k_map.drop_duplicates()[[col, 'centroid', 'bin']], + how='left', on=col) + + + def kmeans(self, nbins, input_df, *args, **kwargs): + ''' + bin and return the centroids or breakpoints of each bin + + df (DataFrame object): Pandas Dataframe containing data for binning + col (str): the column name that is to be binned + bins (int): the number of desired bins + *args: collects unused positional arguments to simplify code + **kwargs: collects unused keyword arguments to simplify code + ''' + print("Starting kmeans clustering of existing generators") + print("using {} bins and a minimum deviation of {} mmBTU/MWh \n".format( + nbins, kwargs['minSpread'])) + print("Note that the clustering can result in warnings if the heat rates") + print("or number of unique plants exceeds the bins specified in the loop") + tdat=pd.DataFrame() + + # for all unique BA/technology combinations... + for i in input_df.id.unique(): + tdat = pd.concat([tdat, + self._kmeans(input_df[input_df.id == i], + 'HR', + nbins, + minSpread=int(kwargs['minSpread']) + ).centers]) + return tdat + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== +def main(reeds_path, inputs_case): + print('Starting WriteHintage.py') + + # #%% Settings for testing + # reeds_path = os.path.expanduser('~/github/ReEDS-2.0') + # inputs_case = os.path.join( + # reeds_path,'runs','v20231027_yamM0_Z45_h_d_365_transreg_z69_core','inputs_case') + + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + + nBin = int(sw.numhintage) + retscen = sw.retscen + mindev = int(sw.mindev) + GSw_WaterMain = sw.GSw_WaterMain + GSw_RetireYears_Coal = int(sw.GSw_RetireYears_Coal) + GSw_RetireYears_Thermal = int(sw.GSw_RetireYears_Thermal) + GSw_Clean_Air_Act = int(sw.GSw_Clean_Air_Act) + + #%% + # Inflation factor 1987$ to 2004$ + inflator = 1.69 + + # Dictionary of relevant technology groups + TECH = { + # This is not all technologies that do not having cooling, but technologies + # that are (or could be) in the plant database. + 'no_cooling':['upv', 'pvb', 'gas-ct', 'geohydro_allkm', + 'battery_li', 'pumped-hydro', 'pumped-hydro-flex', + 'hydUD', 'hydUND', 'hydD', 'hydND', 'hydSD', 'hydSND', 'hydNPD', + 'hydNPND', 'hydED', 'hydEND', 'wind-ons', 'wind-ofs' + ] + } + + # Import mapping files + r_county = pd.read_csv( + os.path.join(inputs_case,'r_county.csv'), index_col='county').squeeze(1) + + # Import generator database + indat = pd.read_csv(os.path.join(inputs_case,'unitdata.csv'), + low_memory=False + ) + + # Map counties to modeled regions + indat['r'] = indat.FIPS.map(r_county) + + # Apply inflation to VOM costs + indat['T_VOM'] *= inflator + indat['T_CCSV'] = indat.T_VOM + inflator * indat.T_CCSV + + # Apply inflation and add capital expenditures to FOM costs + indat['T_FOM'] = inflator * (indat.T_FOM + indat.T_CAPAD) + indat['T_CCSF'] = indat.T_FOM + inflator * indat.T_CCSF + + # Concatenate tech names based on whether water analysis is on -or- leave them alone + if GSw_WaterMain == '1': + # If techs do not have a cooling technology, then replace coolingwatertech with the tech name + indat.loc[indat['tech'].isin(TECH['no_cooling']), + 'coolingwatertech'] = indat.loc[indat['tech'].isin(TECH['no_cooling']),'tech'] + indat['tech'] = indat.coolingwatertech + + ### NOTE: New addition for columns AO:AR, AW:AX in the plant file + ad = indat[["tech", "r", "ctt", "summer_power_capacity_MW", "TC_WIN", retscen, + "StartYear", "IsExistUnit", "HeatRate", "T_VOM", "T_FOM", + "T_CCSROV", "T_CCSF", "T_CCSV", "T_CCSHR", "T_CCSCAPA", "T_CCSLOC"]].copy() + + # Rename columns in ad + rename = { + 'tech' : 'TECH', + 'r' : 'r', + 'ctt' : 'ctt', + 'summer_power_capacity_MW' : 'Summer.capacity', + 'TC_WIN' : 'Winter.capacity', + retscen : 'RetireYear', + 'StartYear' : 'onlineyear', + 'IsExistUnit' : 'EXIST', + 'HeatRate' : 'HR', + 'T_VOM' : 'VOM', + 'T_FOM' : 'FOM', + 'T_CCSROV' : 'CCS_Retro_OvernightCost', + 'T_CCSF' : 'CCS_Retro_FOM', + 'T_CCSV' : 'CCS_Retro_VOM', + 'T_CCSHR': 'CCS_Retro_HR', + 'T_CCSCAPA': 'CCS_Retro_CapAdjust', + 'T_CCSLOC': 'CCS_Retro_LocFactor' + } + ad.rename(columns=rename, inplace=True) + + # Subset only generators that exist + ad = ad[ad.EXIST] + + # Only want those with a heat rate - all other binning is arbitrary + # because the only data we get from generator database is the capacity and heat + # rate but O&M costs are assumed + df = ad[(~ad.HR.isna()) & (~ad.TECH.isin(['geohydro_allkm', 'CofireNew']))] + + # Adjust retirement dates of coal specifically or thermal techs generally based on switch + + tech_table = pd.read_csv( + os.path.join(inputs_case, 'tech-subset-table.csv')).set_index('Unnamed: 0') + coal_techs = [x.lower() for x in tech_table[tech_table['COAL'] == 'YES'].index.values.tolist()] + thermal_techs = [x.lower() for x in tech_table[(tech_table['COAL'] == 'YES') | + (tech_table['GAS'] == 'YES') | + (tech_table['NUCLEAR'] == 'YES') | + (tech_table['OGS'] == 'YES')].index.values.tolist()] + + current_yr = datetime.date.today().year + + if GSw_RetireYears_Thermal == 0: + # Thermal retirements are not adjusted, data is straight from EIA unit database + # Coal retirement can be adjusted separately when GSw_RetireYears_Thermal = 0 + # if GSw_RetireYears_Coal = 0, coal retirements are not adjusted, data is straight from EIA unit database + if GSw_RetireYears_Coal < 0: + # For coal units with retire year before current_yr - GSw_RetireYears_Coal, they are forced to retire in current_yr. + # For coal units with retire year after or equal to current_yr - GSw_RetireYears_Coal, their lifetime is shorted by |GSw_RetireYears_Coal|. + df.loc[(df['RetireYear'] < current_yr - GSw_RetireYears_Coal) & (df['RetireYear'] > current_yr) + & (df['TECH'].isin(coal_techs)), 'RetireYear'] = current_yr + df.loc[(df['RetireYear'] >= current_yr - GSw_RetireYears_Coal) & (df['TECH'].isin(coal_techs)), + 'RetireYear'] += GSw_RetireYears_Coal + + elif GSw_RetireYears_Coal > 0: + # Lifetime of all currently operating coal units is extended by GSw_RetireYears_Coal + df.loc[(df['RetireYear'] > current_yr) & (df['TECH'].isin(coal_techs)), + 'RetireYear'] += GSw_RetireYears_Coal + + # Adjust thermal techs' retirement dates when GSw_RetireYears_Thermal != 0 + elif GSw_RetireYears_Thermal < 0: + # For thermal units with retire year before current_yr - GSw_RetireYears_Thermal, they are forced to retire in current_yr. + # For thermal units with retire year after or equal to current_yr - GSw_RetireYears_Thermal, their lifetime is shorted by |GSw_RetireYears_Thermal|. + df.loc[(df['RetireYear'] < current_yr - GSw_RetireYears_Thermal) & (df['RetireYear'] > current_yr) + & (df['TECH'].isin(thermal_techs)), 'RetireYear'] = current_yr + df.loc[(df['RetireYear'] >= current_yr - GSw_RetireYears_Thermal) & (df['TECH'].isin(thermal_techs)), + 'RetireYear'] += GSw_RetireYears_Thermal + + elif GSw_RetireYears_Thermal > 0: + # Lifetime of all currently operating thermal units is extended by GSw_RetireYears_Thermal + df.loc[(df['RetireYear'] > current_yr) & (df['TECH'].isin(thermal_techs)), + 'RetireYear'] += GSw_RetireYears_Thermal + + # Group up similar generators + dat = df.groupby([ + 'TECH', 'r', 'HR', 'onlineyear', + 'RetireYear', 'VOM', 'FOM',"CCS_Retro_OvernightCost", "CCS_Retro_FOM", + "CCS_Retro_VOM", "CCS_Retro_HR", "CCS_Retro_CapAdjust", "CCS_Retro_LocFactor", + ])[['Summer.capacity','Winter.capacity']].sum().reset_index() + + # Remove 'others' category + dat = dat[dat.TECH != 'others'].copy() + + # Remove some generators based on retire year and online year + dat = dat[(dat.RetireYear >= 2010) & (dat['onlineyear'] < 2010)].copy() + + # Make unique ID column for generators + id_delimiter = '' + dat['id'] = dat.TECH + id_delimiter + dat.r + + # Bin hintage data - this leverages the kmeans function in + # the grouping class to perform the operations in the _kmeans sub-class + # and returns the 'dat' dataframe with the additional 'bin' column + # this needs to be done separately since coal techs are regulated at the unit level + dat_non_coal = dat[~dat.TECH.isin(coal_techs)] # all non-coal plants + dat_coal = dat[dat.TECH.isin(coal_techs)] # coal plants + + # treat the non-coal options regularly + tdat_non_coal = grouping(nBin, dat_non_coal, 'HR', minSpread=mindev).df + + # coal plants are grouped at the unit level if GSw_Clean_Air_Act is enabled + if GSw_Clean_Air_Act == 1: + tdat_coal = grouping('unit', dat_coal, 'HR', minSpread=mindev).df + else: + tdat_coal = grouping(nBin, dat_coal, 'HR', minSpread=mindev).df + + tdat = pd.concat([tdat_non_coal, tdat_coal], ignore_index=True, axis=0) + + # calculate the maximum hintage number, to be used in b_inputs.gms, and export it + max_hintage_number = tdat['bin'].max() + max_hintage_number_text = f'scalar max_hintage_number "--number-- the maximum number of bins used in this ReEDS run" /{max_hintage_number}/ ;' + with open(os.path.join(inputs_case,'max_hintage_number.txt'), 'w') as file: + file.write(f'{max_hintage_number_text}') + + # calculate the capacity-weighted average heat rate for each bin + # by taking the product of the sum of the capacity and the centroid of the bin + tdat['wHR'] = tdat.HR * tdat['Summer.capacity'] + tdat['wVOM'] = tdat.VOM * tdat['Summer.capacity'] + tdat['wFOM'] = tdat.FOM * tdat['Summer.capacity'] + tdat['solveYearOnline'] = tdat.onlineyear * tdat['Summer.capacity'] + tdat['wCCS_Retro_OvernightCost'] = tdat.CCS_Retro_OvernightCost * tdat['Summer.capacity'] + tdat['wCCS_Retro_FOM'] = tdat.CCS_Retro_FOM * tdat['Summer.capacity'] + tdat['wCCS_Retro_VOM'] = tdat.CCS_Retro_VOM * tdat['Summer.capacity'] + tdat['wCCS_Retro_HR'] = tdat.CCS_Retro_HR * tdat['Summer.capacity'] + tdat['wCCS_Retro_CapAdjust'] = tdat.CCS_Retro_CapAdjust * tdat['Summer.capacity'] + tdat['wCCS_Retro_LocFactor'] = tdat.CCS_Retro_LocFactor * tdat['Summer.capacity'] + + zout = pd.DataFrame() + level_cols = ['wHR', 'wVOM', 'wFOM', 'solveYearOnline','wCCS_Retro_OvernightCost', + 'wCCS_Retro_FOM','wCCS_Retro_VOM','wCCS_Retro_HR', + 'wCCS_Retro_CapAdjust','wCCS_Retro_LocFactor'] + + combine_cols = level_cols + ['Winter.capacity'] + +# Adjust the HR, VOM, FOM, solveYearOnline, and winter capacity + for i in list(range(2010, tdat.RetireYear.max() + 1)): + # Subset on years earlier than i + ydat = tdat.loc[tdat.RetireYear > i, ['id','bin','Summer.capacity'] + combine_cols] + + # Sum up the parameters by id and bin + ydat = ydat.groupby(['id','bin']).sum() + + # Levelize parameters + for j in level_cols: + ydat[j] /= ydat['Summer.capacity'] + + ydat['year'] = i + # Paste dataframes together + zout = pd.concat([zout, ydat]) + + # Parse id + zout.reset_index(inplace=True) + if GSw_WaterMain == '1': + zout['tech'] = zout.id.str.rsplit(id_delimiter, n=1, expand=True)[0] + zout['r'] = zout.id.str.rsplit(id_delimiter, n=1, expand=True)[1] + else: + zout['tech'] = zout.id.str.split(id_delimiter, n=1, expand=True)[0] + zout['r'] = zout.id.str.split(id_delimiter, n=1, expand=True)[1] + zout.drop(columns='id', inplace=True) + + #%%############################### + # -- Get DPV Generators -- # + ################################## + + dpv = pd.read_csv(os.path.join(inputs_case,'distpvcap.csv')).set_index('r') + + # Fill in odd years' values for dpv (only add odd year data if that + # data does not already exist) + firstyr = int(dpv.columns.min()) + lastyr = int(dpv.columns.max()) + oddyrs = [str(x) for x in np.arange(firstyr,lastyr+1) if x % 2 != 0] + for yr in oddyrs: + if yr not in dpv.columns: + dpv[yr] = (dpv[str(int(yr)-1)] + dpv[str(int(yr)+1)]) / 2 + dpv = pd.melt(dpv.reset_index(),id_vars=['r']) + dpv.rename(columns=dict(zip(dpv.columns,['r','year','Summer.capacity'])), + inplace=True) + + # Initialize columns for dpv dataframe + dpv['tech'] = 'distpv' + dpv['wHR'] = 0 + dpv['wVOM'] = 0 + dpv['wFOM'] = 0 + dpv['Winter.capacity'] = dpv['Summer.capacity'] + dpv['bin'] = 1 + dpv['solveYearOnline'] = 2010 + dpv['year'] = dpv['year'].astype(int) + + # Concat dpv and the output dataframes + zout = pd.concat([zout, dpv]) + + #%%############################################################################ + # -- Get forced retirement dataframe and merge onto output dataframe -- # + ############################################################################### + forced_retire = pd.read_csv( + os.path.join(inputs_case, 'forced_retirements.csv'), + header=0, names=['tech','st','retire_year']) + + # Forced retirements are at the state level, so use hierarchy to get the regions + state2r = ( + pd.read_csv( + os.path.join(inputs_case, 'hierarchy.csv'), + usecols=['*r', 'st'] + ) + .rename(columns={'*r': 'r'}) + ) + forced_retire = ( + pd.merge(forced_retire, state2r, on='st') + .drop(columns='st') + .drop_duplicates() + ) + + zout = zout.merge(forced_retire, how='left', on=['tech', 'r']).fillna(9000) + + # Zero out retired generators' capacity + zout.loc[zout.solveYearOnline >= zout.retire_year, 'Summer.capacity'] = 0 + zout.loc[zout.solveYearOnline >= zout.retire_year, 'Winter.capacity'] = 0 + + # Clean up output dataframe + zout['bin_int'] = zout['bin'] # keep integer bins in dataframe for ease of plotting + zout['bin'] = 'init-' + zout['bin'].astype(str) + zout['solveYearOnline'] = zout['solveYearOnline'].round() + zout['wFOM'] *= 1e3 + zout.rename(columns={'Summer.capacity': 'cap', + 'Winter.capacity': 'wintercap', + 'solveYearOnline': 'wOnlineYear', + 'year': 'yr', + 'tech': 'TECH'}, + inplace=True) + + zout.cap = zout.cap.round(decimals=1) + zout.wintercap = zout.wintercap.round(decimals=1) + zout.wHR = zout.wHR.round(decimals=1) + zout.wFOM = zout.wFOM.round(decimals=3) + zout.wVOM = zout.wVOM.round(decimals=3) + zout.wCCS_Retro_OvernightCost = zout.wCCS_Retro_OvernightCost.round(decimals=3) + zout.wCCS_Retro_FOM = zout.wCCS_Retro_FOM.round(decimals=3) + zout.wCCS_Retro_VOM = zout.wCCS_Retro_VOM.round(decimals=3) + zout.wCCS_Retro_HR = zout.wCCS_Retro_HR.round(decimals=1) + zout.wCCS_Retro_CapAdjust = zout.wCCS_Retro_CapAdjust.round(decimals=3) + zout.wCCS_Retro_LocFactor = zout.wCCS_Retro_LocFactor.round(decimals=3) + + #%% Save output dataframe in inputs_case folder + cols = ['TECH', 'bin', 'r', 'yr', 'cap', 'wintercap', 'wHR', + 'wFOM', 'wVOM', 'wOnlineYear', + 'wCCS_Retro_OvernightCost','wCCS_Retro_FOM','wCCS_Retro_VOM', # new addition: revised to include CCS retrofits + 'wCCS_Retro_HR','wCCS_Retro_CapAdjust','wCCS_Retro_LocFactor'] + + zout[cols].dropna().to_csv(os.path.join(inputs_case,'hintage_data.csv'), index=False) + + #%%#################################################################################### + # -- Make plots comparing actual unit heatrates with binned ones, if desired -- # + ####################################################################################### + + make_plots = 0 + + # Make plots comparing actual unit heatrates with binned ones, if desired + if make_plots: + import matplotlib.pyplot as plt + # Create facet plots for heatrate, FO&M, VO&M, and online year + allgens = pd.merge( + tdat.loc[ + (tdat.RetireYear > 2020) & (tdat['onlineyear'] < 2020), + ['TECH','r','bin','HR','FOM','VOM','onlineyear','Summer.capacity']], + zout.loc[zout.yr==2020], + left_on=['r','TECH','bin'], + right_on=['r','TECH','bin_int'], + how='left') + + ## Summary scatter plot for all techs + plt.close() + plt.scatter(allgens['HR'],allgens['wHR'],c=allgens['Summer.capacity'],alpha=0.15) + plt.rcParams["figure.figsize"] = (7,10) + color_bar = plt.colorbar() + color_bar.set_label('Summer Capacity (MW)') + #Plot an abline of slope 1 for reference + x_vals = np.array((0,45000)) + y_vals = 1 * x_vals + plt.plot(x_vals, y_vals) + plt.title('Hintage Binning Results for All BAs and All Techs') + plt.xlabel("Actual Heat Rate (MMBtu / MWh)") + plt.ylabel("Binned Heat Rate (MMBtu / MWh)") + plt.savefig(os.path.join(inputs_case, 'hintage_data_2020_heatrate_binning_summary.png')) + + + ## Faceted scatter plot showing a fairly random selection of nine BAs for all techs + # Get color axes range to set static across subplots: + color_col = 'onlineyear' + bas_to_plot = ['p1','p2','p3','p50','p51','p52','p110','p120','p130'] + vmin_global = min([ allgens.loc[allgens['r']==ba,color_col].min() for ba in bas_to_plot]) + vgmax_global = max([ allgens.loc[allgens['r']==ba,color_col].max() for ba in bas_to_plot]) + + plt.close() + f = plt.figure() + f, axes = plt.subplots(nrows = 3, ncols = 3, figsize=(12,12), sharex=True, sharey = True) + + axes = axes.ravel() + for i,ba in zip(range(9),bas_to_plot): + im = axes[i].scatter(allgens.loc[allgens['r']==ba,'HR'], + allgens.loc[allgens['r']==ba,'wHR'], + c=allgens.loc[allgens['r']==ba,color_col], + vmin=vmin_global, + vmax=vgmax_global) + + #Plot an abline of slope 1 for reference + x_vals = np.array((5000,25000)) + y_vals = 1 * x_vals + axes[i].plot(x_vals, y_vals) + axes[i].set_title(ba) + + # Add common axis labels: + f.add_subplot(111, frame_on=False) + plt.tick_params(labelcolor="none", bottom=False, left=False) + plt.xlabel("Actual Heat Rate (MMBtu / MWh)") + plt.ylabel("Binned Heat Rate (MMBtu / MWh)",labelpad=20) + + f.subplots_adjust(right=0.8) + cbar_ax = f.add_axes([0.85,0.1,0.03,0.8]) + color_bar = f.colorbar(im, cax=cbar_ax) + color_bar.set_label(color_col) + + plt.savefig(os.path.join(inputs_case, 'hintage_data_2020_BA_binning_examples_all_techs.png')) + + + ## Faceted scatter plot showing the BA with the highest number of units for each tech + # (i.e. where our binning assumptions have the most impact) + color_col = 'onlineyear' + allgens.groupby(["r", "TECH"]).count().groupby('TECH').max() + bas_with_max_num_units_by_tech = ( + allgens.groupby(["r", "TECH"]).count() + .groupby('TECH').idxmax()['bin_x'].tolist() + ) + + # Get color axes range to set static across subplots: + vmin_global = min( + [allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech), color_col].min() + for ba,tech in bas_with_max_num_units_by_tech]) + vgmax_global = max( + [allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech), color_col].max() + for ba,tech in bas_with_max_num_units_by_tech]) + + # Create plot: + plt.close() + f = plt.figure() + num_cols = math.floor(math.sqrt(len(bas_with_max_num_units_by_tech))) + add_row = math.ceil(math.sqrt(len(bas_with_max_num_units_by_tech)) % num_cols) + + f, axes = plt.subplots( + nrows=(num_cols + add_row), ncols=num_cols, figsize=(14,12), sharex=True, sharey=True) + + axes = axes.ravel() + i=0 + for ba,tech in bas_with_max_num_units_by_tech: + im = axes[i].scatter( + allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),'HR'], + allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),'wHR'], + c=allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),color_col], + vmin=vmin_global, + vmax=vgmax_global) + + #Plot an abline of slope 1 for reference + x_vals = np.array((5000,25000)) + y_vals = 1 * x_vals + axes[i].plot(x_vals, y_vals) + + num_units = len(allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),'HR']) + axes[i].set_title(f'{tech} in {ba}: {num_units} units') + + i += 1 + + # Add common axis labels: + f.add_subplot(111, frame_on=False) + plt.tick_params(labelcolor="none", bottom=False, left=False) + plt.xlabel("Actual Heat Rate (MMBtu / MWh)") + plt.ylabel("Binned Heat Rate (MMBtu / MWh)",labelpad=20) + + f.subplots_adjust(right=0.8) + cbar_ax = f.add_axes([0.85,0.1,0.03,0.8]) + color_bar = f.colorbar(im, cax=cbar_ax) + color_bar.set_label(color_col) + + plt.savefig( + os.path.join( + inputs_case, 'hintage_data_2020_BAs_with_max_num_units_of_each_tech.png')) + + + corrcoef = allgens['HR'].corr(allgens['wHR']) + print(f'Pearson correlation coefficient between actual and binned heat rates is {corrcoef}') + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== +if __name__ == '__main__': + + ### Parse arguments + parser = argparse.ArgumentParser() + parser.add_argument('reeds_path', type=str, help='Path to local ReEDS repo') + parser.add_argument('inputs_case', type=str) + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, logpath=os.path.join(inputs_case,'..','gamslog.txt')) + + #%% Run it + main(reeds_path=reeds_path, inputs_case=inputs_case) + + reeds.log.toc(tic=tic, year=0, process='inputs/WriteHintage.py', + path=os.path.join(inputs_case,'..')) + + print('Finished WriteHintage.py') + \ No newline at end of file diff --git a/reeds/input_processing/__init__.py b/reeds/input_processing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reeds/input_processing/aggregate_regions.py b/reeds/input_processing/aggregate_regions.py new file mode 100644 index 00000000..f750cd2c --- /dev/null +++ b/reeds/input_processing/aggregate_regions.py @@ -0,0 +1,1183 @@ +""" +prbrown 20220421 +Notes to user: +-------------- +* This script loops over files in runs/{}/inputs_case/ and agg/disaggregates + them based on the directions given in runfiles.csv. + * If new files have been added to inputs_case, you'll need to add rows with + processing directions to runfiles.csv. + * The column names should be self-explanatory; most likely there's also at least + one similarly-formatted file in inputs_case that you can copy the settings for. +* Some files are agg/disaggregated in other scripts: + * WriteHintage.py (these files are handled upstream since + aggregation affects the clustering of generators into (b/h/v)intages): + * hintage_data.csv +""" + +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import numpy as np +import os +import pandas as pd +import gdxpds +import shutil +import sys +import datetime +from glob import glob +from warnings import warn +import h5py +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +## Time the operation of this script +tic = datetime.datetime.now() + + +#%% =========================================================================== +### --- FUNCTIONS AND DICTIONARIES --- +### =========================================================================== + +the_unnamer = {'Unnamed: {}'.format(i): '' for i in range(1000)} + +aggfuncmap = { + 'mode': pd.Series.mode, +} + +def logprint(filepath, message): + print('{:-<45}> {}'.format(filepath+' ', message)) + + +def refilter_regions(df, region_cols, region_col, val_r_all): + ''' + This function is used to filter by region again for cases when only a subset of a model + balancing area is represented + ''' + dfout = df.copy() + if ('*r' in region_cols) and ('rr' in region_cols): + dfout = dfout.loc[dfout.index.get_level_values('*r').isin(val_r_all)] + dfout = dfout.loc[dfout.index.get_level_values('rr').isin(val_r_all)] + else: + dfout = dfout.loc[dfout.index.get_level_values(region_col).isin(val_r_all)] + + return dfout + + +def aggreg_methods( + df, row, aggfunc, region_cols, region_col, + r2aggreg, r_ba, disagg_data, sw, indexnames, columns, +): + df1 = df.copy() + ### Pre-aggregation: Map old regions to new regions + if aggfunc in ['sum','mean','first','min','sc_cat','resources']: + # Exception for dr_shed_hourly becuase the wide column contains tech|region + if 'dr_shed_hourly' in row.name: + # Separate tech and region from the 'wide' column + df1[['tech', 'region']] = df1['wide'].str.split('|', expand=True) + # Map regions to aggregated regions + df1['region'] = df1['region'].map(r2aggreg) + # Recombine tech and aggregated region into the 'wide' column + df1['wide'] = df1['tech'] + '|' + df1['region'] + else: + for c in region_cols: + df1[c] = df1[c].map(lambda x: r2aggreg.get(x,x)) + if row.i_col: + df1[row.i_col] = df1[row.i_col].map(lambda x: new_classes.get(x,x)) + + if aggfunc == 'sc_cat': + ## Weight cost by cap; if there's no cap, use 1 MW as weight + for cost_type in sc_cost_types: + ## Geothermal doesn't have all sc_cost_types + if cost_type in df1: + df1[f'cap_times_{cost_type}'] = df1['cap'].fillna(1).replace(0,1) * df1[cost_type] + ## Sum everything + df1 = df1.groupby(row.fix_cols+[region_col]).sum() + ## Divide cost*cap by cap + for cost_type in sc_cost_types: + if cost_type in df1: + df1[cost_type] = df1[f'cap_times_{cost_type}'] / df1['cap'].fillna(1).replace(0,1) + df1.drop([f'cap_times_{cost_type}'], axis=1, inplace=True) + elif aggfunc == 'trans_lookup': + ## Get data for anchor zones + for c in region_cols: + df1 = df1.loc[df1[c].isin(aggreg2anchorreg)].copy() + ## Map to aggregated regions + for c in region_cols: + df1[c] = df1[c].map(anchorreg2aggreg) + elif aggfunc == 'mean_cap': + df1 = ( + df1.rename(columns={'value':columns[-1]}) + .merge((rscweight_nobin.rename(columns={'i':'*i'}) if '*i' in row.fix_cols + else rscweight_nobin), + on=['r',('*i' if '*i' in row.fix_cols else 'i')], how='left') + ## There are some nan's because we subtract existing capacity from the supply curve. + ## Fill them with 1 MW for now, but it would be better to change that procedure. + ## Note also that the weighting will be off + .fillna(1) + ) + ### Similar procedure as above for aggfunc == 'sc_cat' + if row.i_col: + df1[row.i_col] = df1[row.i_col].map(lambda x: new_classes.get(x,x)) + df1 = ( + df1.assign(r=df1.r.map(r_ba)) + .assign(cap_times_cf=df1.cf*df1.MW) + .groupby(row.fix_cols+region_cols).sum() + ) + df1.cf = df1.cap_times_cf / df1.MW + df1 = df1.drop(['cap_times_cf','MW'], axis=1) + elif aggfunc == 'resources': + ### Special case: Rebuild the 'resources' column as {tech}|{region} + df1 = ( + df1.assign(resource=df1.i+'|'+df1.r) + .drop_duplicates() + ) + ### Special case: If calculating capacity credit by r, replace ccreg with r + if sw['capcredit_hierarchy_level'] == 'r': + df1 = df1.assign(ccreg=df1.r).drop_duplicates() + elif aggfunc in ['recf', 'csp']: + ## Get correct rscweight_nobin tech value + rsctech = os.path.splitext(row.name)[0].split('_')[1] + rscweight_nobin_tech = rscweight_nobin.loc[rscweight_nobin['i'].str.contains(rsctech)] + ### Region is embedded in the 'resources' column as {tech}|{region} + col2r = dict(zip(columns, [c.split('|')[-1] for c in columns])) + col2i = dict(zip(columns, [c.split('|')[0] for c in columns])) + df1 = df1.rename(columns={'value':'cf'}) + df1['r'] = df1[region_col].map(col2r) + df1['i'] = df1[region_col].map(col2i) + ## rscweight_nobin data from writesupplycurves.py has tech values of {rsctech}|{class} + ## so replicate this in order to merge for capacities + df1['i'] = f'{rsctech}_' + df1['i'] + + ## Get capacities + df1 = df1.merge(rscweight_nobin_tech, on=['r','i'], how='left') + ## Similar procedure as above for aggfunc == 'sc_cat' + df1['i'] = df1['i'].map(lambda x: new_classes.get(x,x)) + df1 = df1.rename(columns={'value':'cf'}) + df1 = ( + df1 + .assign(r=df1.r.map(r_ba)) + .assign(cap_times_cf=df1.cf*df1.MW) + .groupby(indexnames + ['i','r']).sum() + ) + df1.cf = df1.cap_times_cf / df1.MW + df1 = df1.rename(columns={'cf':'value'}).reset_index() + # Revert i column so rsctech is not included in the name. + # This ensures the resource h5 files will be in the same format when read in recf.py + # regardless of the spatial resolution + df1['i'] = df1['i'].str.replace(f'{rsctech}_','') + ### Remake the resources (column names) with new regions + df1['wide'] = df1.i + '|' + df1.r + df1 = df1.set_index(indexnames + ['wide'])[['value']].astype(np.float32) + elif aggfunc in ['sum','mean','first','min']: + # Exception for dr_shed_hourly becuase the wide column contains tech|region + if 'dr_shed_hourly' in row.name : + # Group by relevant columns and aggregate values + df1 = df1.groupby(['datetime', 'wide', 'year'], as_index=False).sum().drop(columns=['tech', 'region']) + else: + df1 = df1.groupby(row.fix_cols+region_cols).agg(aggfunc) + + ### Disaggregation methods -------------------------------------------------------------------- + elif aggfunc == 'uniform': + for rcol in region_cols: + df1 = ( + df1.merge(r_ba, left_on=rcol, right_on='ba', how='inner') + .drop(columns=[rcol,'ba']) + .rename(columns={'FIPS':rcol}) + ) + # if the fixed column is wide, then 'wide' needs to be an index as well + if (len(row.fix_cols) == 1) and (row.fix_cols[0] == 'wide'): + df1.set_index([region_col,'wide'],inplace=True) + else: + df1.set_index(row.fix_cols+region_cols,inplace=True) + df1 = refilter_regions(df1, region_cols,region_col, val_r_all) + elif aggfunc in ['population','geosize','hydroexist']: + if 'sc_cat' in columns: + # Split cap and cost + df1_cap = df1[df1['sc_cat']=='cap'] + df1_cost = df1[df1['sc_cat']=='cost'] + + # Disaggregate cap using the selected aggfunc + fracdata= disagg_data[aggfunc] + rcol = region_cols[0] + df1cols = (df1.columns) + valcol = df1cols[-1] + # Identify the columns to merge from the fracdata + fracdata_mergecols = (['PCA_REG'] + [col for col in fracdata.columns + if col not in ['PCA_REG','nonUS_PCA','FIPS','fracdata']]) + # Identify the columns to merge from the original data + df1_mergecols = [rcol] + [col for col in df1cols if col in fracdata_mergecols] + # Merge the datasets using PCA_REG + df1_cap = pd.merge(fracdata, df1_cap, left_on='PCA_REG', right_on= df1_mergecols, how='inner') + # Apply the weights to create a new value + df1_cap['new_value'] = (df1_cap['fracdata'].multiply(df1_cap[valcol], axis='index')) + # Clean up dataframe before grabbing final values + df1_cap.drop(columns=[valcol]+[rcol],inplace=True) + df1_cap.rename(columns={'new_value':valcol,'FIPS':rcol},inplace=True) + new_index = df1cols[:-1] + df1_cap = df1_cap.set_index(new_index.to_list()) + # Keep cost uniform, so map costs to all subregions with the PCA_REG + df1_cost = pd.merge(fracdata, df1_cost, left_on='PCA_REG', right_on= df1_mergecols, how='inner') + df1_cost['new_value'] = df1_cost[valcol] + df1_cost.drop(columns=[valcol]+[rcol],inplace=True) + df1_cost.rename(columns={'new_value':valcol,'FIPS':rcol},inplace=True) + new_index = df1cols[:-1] + df1_cost = df1_cost.set_index(new_index.to_list()) + # Combine cap and cost to get back into original format + df1 = pd.concat([df1_cap, df1_cost]) + df1 = refilter_regions(df1, region_cols, region_col,val_r_all) + + elif row.name =='dr_shed_hourly.h5' : + # separate tech | region + rcol = df1.wide.str.split('|',expand=True)[1] + fracdata = disagg_data[aggfunc] + df1cols = (df1.columns) + valcol = df1cols[-1] + # Identify the columns to merge from the fracdata + fracdata_mergecols = ['PCA_REG'] + [col for col in fracdata.columns + if col not in ['PCA_REG','nonUS_PCA','FIPS','fracdata']] + # Identify the columns to merge from the original data + df1_mergecols = [rcol] + [col for col in df1cols if col in fracdata_mergecols] + # Merge the datasets using PCA_REG + df1 = pd.merge(fracdata, df1, left_on=fracdata_mergecols, right_on=df1_mergecols, how='inner') + # Multiply BA shed by population fraction + df1['new_value'] = (df1['fracdata'].multiply(df1[valcol], axis='index')) + # Clean up dataframe + df1['wide'] = df1.wide.str.split('|',expand=True)[0] +'|' + df1['FIPS'] + df1 = (df1.drop(columns=[valcol,'PCA_REG','fracdata','FIPS']) + .rename(columns={'new_value':valcol})) + else: + # Disaggregate cap using the selected aggfunc + fracdata = disagg_data[aggfunc] + rcol = region_cols[0] + df1cols = (df1.columns) + valcol = df1cols[-1] + # Identify the columns to merge from the fracdata + fracdata_mergecols = ['PCA_REG'] + [col for col in fracdata.columns + if col not in ['PCA_REG','nonUS_PCA','FIPS','fracdata']] + # Identify the columns to merge from the original data + df1_mergecols = [rcol] + [col for col in df1cols if col in fracdata_mergecols] + # Merge the datasets using PCA_REG + df1 = pd.merge(fracdata, df1, left_on=fracdata_mergecols, right_on=df1_mergecols, how='inner') + # Clean up dataframe before grabbing final values + df1['new_value'] = (df1['fracdata'].multiply(df1[valcol], axis='index')) + df1 = (df1.drop(columns=[valcol]+[rcol]) + .rename(columns={'new_value':valcol,'FIPS':rcol})) + new_index = df1cols[:-1] + df1 = df1.set_index(new_index.to_list()) + df1 = refilter_regions(df1, region_cols,region_col, val_r_all) + + else: + raise ValueError(f'Invalid choice of aggfunc: {aggfunc} for {row.name}') + + ## Filter by regions again for cases when only a subset of a model balancing area is represented + if agglevel == 'county': + if ('*r' in region_cols) and ('rr' in region_cols): + df1 = df1.loc[df1.index.get_level_values('*r').isin(val_r_all)] + df1 = df1.loc[df1.index.get_level_values('rr').isin(val_r_all)] + elif row.name =='dr_shed_hourly.h5': + # separate tech | region to filter + df1 = df1.loc[df1.wide.str.split('|',expand=True)[1].isin(val_r_all)] + else: + df1 = df1.loc[df1.index.get_level_values(region_col).isin(val_r_all)] + + ################################ + ### Put back in original format ### + + if row.name == 'dr_shed_hourly.h5': + dfout = df1.set_index(['datetime','year','wide']).unstack('wide')['value'].reset_index() + elif (aggfunc == 'sc_cat') and (not row.wide): + dfout = df1.stack().rename(row.key).reset_index()[columns] + elif (aggfunc == 'sc_cat') and row.wide and (len(row.fix_cols) == 1): + dfout = df1.stack().rename('value').unstack('wide').reset_index()[columns].fillna(0) + elif not row.wide: + dfout = df1.reset_index()[columns] + elif (row.wide) and (region_col != 'wide') and (len(row.fix_cols) == 1) and (row.fix_cols[0] == 'wide'): + dfout = df1.unstack('wide')['value'].reset_index() + elif (row.wide) and (region_col != 'wide') and (len(row.fix_cols) > 1) and ('wide' in row.fix_cols): + # In some cases disaggregating to county level can lead to empty + # dataframes, so address that here + if df1.empty: + dfout = pd.DataFrame(columns = columns) + else: + dfout = df1.unstack('wide')['value'].reset_index()[columns] + elif row.wide and (region_col == 'wide') and len(row.fix_cols): + if (len(row.fix_cols) == 1): + dfout = ( + df1.reset_index() + .set_index(row.fix_cols) + .pivot(columns='wide', values='value') + .reset_index() + ) + else: + dfout = df1.unstack('wide')['value'].reset_index() + + ### Drop rows where r and rr are the same + if row.key == 'drop_dup_r': + dfout = dfout.loc[dfout[region_cols[0]] != dfout[region_cols[1]]].copy() + + ### Other special-case processing + if (row.name == 'hierarchy.csv') and (sw['capcredit_hierarchy_level'] == 'r'): + dfout = dfout.assign(ccreg=dfout[region_col]).drop_duplicates() + dfout['ccreg'].to_csv(os.path.join(inputs_case, 'ccreg.csv'), index=False) + + dfout.rename(columns=the_unnamer, inplace=True) + + return dfout + + +def reshape_to_long( + dfin, + filepath, + row, + indexnames, + aggfunc, + region_col, + region_cols, +): + if (aggfunc == 'sc_cat') and (not row.wide): + ### Supply-curve format. Expect an sc_cat column with 'cap' and 'cost' values. + ## 'cap' values are summed; 'cost' values use the 'cap'-weighted mean + df = dfin.pivot( + index=row.fix_cols+region_cols, + columns='sc_cat', + values=row.key, + ).reset_index() + elif (aggfunc == 'sc_cat') and row.wide and (len(row.fix_cols) == 1): + ### Supply-curve format. Expect an sc_cat column with 'cap' and 'cost' values. + ## Some value other than region is in wide format + ## So turn region and the wide value into the index + df = dfin.set_index(region_cols+['sc_cat']).stack().rename('value') + df.index = df.index.rename(region_cols+['sc_cat','wide']) + ## Make value columns for 'cap' and 'cost' + df = df.unstack('sc_cat').reset_index() + elif not row.wide: + ### File is already long so don't do anything + df = dfin.copy() + elif ( + row.wide + and (region_col != 'wide') + and (len(row.fix_cols) == 1) + and (row.fix_cols[0] == 'wide') + ): + ## Some value other than region is in wide format + ## So turn region and the wide value into the index + df = dfin.set_index(region_col).stack().rename('value') + df.index = df.index.rename([region_col, 'wide']) + ## Turn index into columns + df = df.reset_index() + elif ( + row.wide + and (region_col != 'wide') + and (len(row.fix_cols) > 1) + and ('wide' in row.fix_cols) + ): + ## Some value other than region is in wide format + ## So turn region, other fix_cols, and the wide value into the index + df = ( + dfin + .set_index([region_col]+[c for c in row.fix_cols if c != 'wide']) + .stack() + .rename('value') + ) + df.index = df.index.rename([region_col]+row.fix_cols) + ## Turn index into columns + df = df.reset_index() + elif row.wide and (region_col == 'wide') and len(row.fix_cols): + ### File has some fixed columns and then regions in wide format + df = ( + ## Turn all identifying columns into indices, with region as the last index + dfin.set_index(row.fix_cols).stack() + ## Name the region column 'wide' + .rename_axis(row.fix_cols+['wide']).rename('value') + ## Turn index into columns + .reset_index() + ) + + return df + + +def separate_wide_mixed_data(dfin,columns,fix_cols,agglevel_list): + # Separate mixed BA and county data in wide format + + reg_cols = [col for col in columns if col not in fix_cols] + if all('|' in col for col in reg_cols): + #Find columns with regions that are being solved at BA or aggreg resolution + # Some inputs files have tech and regions combined as column header and + # need to be filtered differently + keep_col = [] + for col in dfin.columns: + new_col = col.split('|') + for part in new_col: + if part in agglevel_list : + keep_col.append(col) + + df_sep_in = dfin[fix_cols + keep_col] + + else: + col_list = fix_cols + agglevel_list + #Check if data exists for all BAs in agglevel list + #Loop through each ba in ba region list and add to region_list if it doesn't appear in input data + regions_list = [] + for ba in agglevel_list : + if ba not in dfin.columns : + regions_list.append(ba) + #If there is a ba for which there is no data, exclude this ba from the column list + if len(regions_list) >0 : + reduced_ba_regions = [x for x in agglevel_list if x not in regions_list] + df_sep_in = dfin[fix_cols + reduced_ba_regions] + #If not, rewrite col_list to exclude BAs for which there is no data + else: + df_sep_in = dfin[col_list] + + return df_sep_in + + +def agg_disagg(filepath, r2aggreg_glob, r_ba_glob, runfiles_row): + """ + filepath: input file to be aggregated/disaggregated/ignored + r2aggreg_glob: ba to aggreg mapping needed for single resolution aggreg cases. + For mixed resolutions runs the r2aggreg parameter is re-defined separately + within the agg_disagg function for data that are being aggregated and disaggregated + r_ba_glob: r to ba mapping for single resolution runs. For mixed resolution runs r_ba is re-defined + separately within the agg_disagg function for data that are aggregated and disaggregated + row : consists of data in runfiles.csv row that correspond with the filepath + + """ + #%% Continue loop + row = runfiles_row.copy() + filetic = datetime.datetime.now() + filename = row.name + if row['aggfunc']=='ignore' and row['disaggfunc']=='ignore': + if verbose > 1: + logprint(filepath, 'ignored') + return + ### Ensure the correct aggfunc/disaggfunc is chosen for the given agglevel + # This will never be true for mixed resolution runs where agglevel is assigned more than one value + elif ((agglevel in ['ba','aggreg'] and row['aggfunc']=='ignore') + or (agglevel in ['county'] and row['disaggfunc']=='ignore')): + if verbose > 1: + logprint(filepath, 'ignored') + return + elif (row['aggfunc']!='ignore') or (row['disaggfunc']!='ignore'): + pass + + # In mixed resolution runs, some of the inputfiles will contain mixed ba-county data + # created in copy_files. This data does not need to be aggregated or disaggregated + # if the resolution selected is ['ba','county'], so skip the file. + if agglevel_variables['lvl'] == 'mult': + list_check = ['county','ba'] + list_check= sorted(list_check) + agglevel_check = sorted(agglevel) + if list_check == agglevel_check : + if row['disaggfunc']=='ignore': + return + + # If the file isn't in inputs_case, skip it + if filename not in inputfiles: + if verbose > 1: + logprint(filepath, 'skipped since not in inputs_case') + return + + #%%############## + ### Settings ### + + ### header: 0 if file has column labels, otherwise 'None' + header = (None if row['header'] in ['None','none','',None,np.nan] + else 'keepindex' if row['header'] == 'keepindex' + else int(row['header'])) + ### region_col: usually 'r', 'rb', or 'region', or 'wide' if file uses regions as columns + region_col = row['region_col'] + # Some datasets have both rb regions and cendiv regions. Only use the r + # regions for these datasets + if region_col == 'r_cendiv': + region_col = 'r' + region_cols = region_col.split(',') + # Assign variable to track if region data exists in two columns + two_col = False + if region_col == '*r,rr' or region_col =='r,rr' or region_col == 'transgrp,transgrpp': + two_col = True + region_col = region_col.split(',') + + # If solving at mixed resolutions, both disagg and agg functions could be required + # Check if one of the desired spatial resolutions is county + if agglevel_variables['lvl'] == 'mult': + for val in agglevel: + if val in ['county']: + aggfunc_agg = aggfuncmap.get(row['aggfunc'], row['aggfunc']) + aggfunc_disagg = aggfuncmap.get(row['disaggfunc'], row['disaggfunc']) + #Single resolution procedure + else: + ### Set aggfunc to the aggregation setting if using ba or aggreg, + ### and set to the disaggregation setting if using county + if agglevel in ['ba','aggreg']: + aggfunc = aggfuncmap.get(row['aggfunc'], row['aggfunc']) + elif agglevel in ['county']: + aggfunc = aggfuncmap.get(row['disaggfunc'], row['disaggfunc']) + + ### wide: 1 if any parameters are in wide format, otherwise 0 + row.wide = int(row.wide) + ### Get the filetype of the input file from the filename string + filetype = os.path.splitext(filename)[1].strip('.') + ### key: only used for gdx files, indicating the parameter name. + ### gdx files need a separate line in runfiles.csv for each parameter. + ### fix_cols: indicate columns to use as for fields that should be projected + ### independently to future years (e.g. r, szn, tech) + row.fix_cols = ( + [] if row.fix_cols in ['None', 'none', '', None, np.nan] + else row.fix_cols.split(',') + ) + ### i_col: indicate technology column. Only used/needed if aggregating techs. + if row.i_col in ['None', 'none', '', np.nan]: + row.i_col = None + # indexnames will get overwritten for h5 files but needs to be defined for the aggreg_methods function + indexnames = None + + + #%%################### + ### Load the file ### + + if filetype in ['csv', 'gz']: + # Some csv files are empty and therefore cannot be opened. If that is + # the case, then skip them. + try: + dfin = pd.read_csv( + os.path.join(inputs_case, filepath), header=header, + dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, + ) + except pd.errors.EmptyDataError: + return + elif filetype == 'h5': + # Skip empty files + with h5py.File(os.path.join(inputs_case, filepath), 'r') as f: + if len(f.keys()) == 0: + return + + dfin = reeds.io.read_file(os.path.join(inputs_case, filepath)).copy() + if header == 'keepindex': + indexnames = (dfin.index.names) + if (len(indexnames) == 1) and (not indexnames[0]): + indexnames = ['index'] + dfin = dfin.reset_index() + elif filetype == 'gdx': + ### Read in the full gdx file, but only change the 'key' parameter + ### given in runfiles. That's wasteful, but there are currently no + ### active gdx files. + dfall = gdxpds.to_dataframes(os.path.join(inputs_case, filepath)) + dfin = dfall[row.key] + else: + raise Exception(f'Unsupported filetype: {filepath}') + + dfin.rename(columns={c:str(c) for c in dfin.columns}, inplace=True) + columns = dfin.columns.tolist() + ### Stop now if it's empty + if dfin.empty: + if verbose > 1: + logprint(filepath, 'empty') + return + + + #%%############################ + ### Reshape to long format ### + + ########## Mixed Resolution Procedure ########## + if agglevel_variables['lvl'] == 'mult': + ########## BA ########## + # If desired resolution is ['county', 'ba'], then BA data are already in correct format + # Filter dataframe to regions being solved at BA resolution + for val in agglevel: + if val in ['ba']: + if region_col != 'wide': + if two_col : + df_agg_in = dfin[dfin[region_col[0]].isin( + agglevel_variables['ba_regions'] + agglevel_variables['ba_transgrp'])] + df_agg_in = df_agg_in[ df_agg_in[region_col[1]].isin(agglevel_variables['ba_regions'])] + else: + df_agg_in = dfin[dfin[region_col].isin(agglevel_variables['ba_regions'])] + else: + col_list = row.fix_cols + agglevel_variables['ba_regions'] + # Check if data exists for all BAs in agglevel_variables['ba_regions'] list + # Loop through each ba in ba region list and add to region_list if it doesn't appear in input data + regions_list = [] + for ba in agglevel_variables['ba_regions'] : + if ba not in dfin.columns : + regions_list.append(ba) + # If there is a ba for which there is no data, exclude this ba from the column list + if len(regions_list) >1 : + reduced_ba_regions = [x for x in agglevel_variables['ba_regions'] if x not in regions_list] + df_agg_in = dfin[row.fix_cols + reduced_ba_regions] + #If not, rewrite col_list to exclude BAs for which there is no data + else: + df_agg_in = dfin[[c for c in col_list if c in dfin]].copy() + + df_agg = df_agg_in + + elif val in ['aggreg']: + # Subset to include regions being solved at Aggreg + # If column headers are region names, need to filter after reformatting + if region_col != 'wide': + if two_col : + df_agg_in = dfin[dfin[region_col[0]].isin( + agglevel_variables['ba_regions'] + agglevel_variables['ba_transgrp'])] + df_agg_in = df_agg_in[ df_agg_in[region_col[1]].isin(agglevel_variables['ba_regions'])] + else: + df_agg_in = dfin[dfin[region_col].isin(agglevel_variables['ba_regions'])] + + # Clause to separate mixed BA and county data in wide format + elif region_col == 'wide': + df_agg_in = separate_wide_mixed_data( + dfin, + columns, + row.fix_cols, + agglevel_variables['ba_regions'], + ) + + #Set aggfunc to aggregation function + aggfunc = aggfunc_agg + + ##### Reformat BA Data #### + df_agg = reshape_to_long( + df_agg_in, + filepath, + row, + indexnames, + aggfunc, + region_col, + region_cols, + ) + + ########## County ########## + # When aggfunc_disagg is set to ignore the county-level data do not need to be reformatted. + # Filter county data from BA data that will be aggregated to aggreg + if aggfunc_disagg == 'ignore': + if region_col != 'wide': + if two_col: + # County transmission data will have county-ba interfaces created in copy_files + # To maintain these interfaces the filtering of the data needs to ensure that BA-BA interfaces + # are dropped but county-county and county-BA interfaces are kept + df_disagg_list = [] + for idx, tx_row in dfin.iterrows(): + cond1 = ((tx_row[region_col[0]] in agglevel_variables['ba_regions']+agglevel_variables['ba_transgrp']) + and (tx_row[region_col[1]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp'])) + cond2 = ((tx_row[region_col[1]] in agglevel_variables['ba_regions']+ agglevel_variables['ba_transgrp']) + and (tx_row[region_col[0]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp'])) + cond3 = ((tx_row[region_col[0]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp']) + and (tx_row[region_col[1]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp'])) + + if cond1 or cond2 or cond3: + df_disagg_list.append(tx_row) + + df_disagg_in = pd.DataFrame(df_disagg_list).drop_duplicates() + + elif filename == 'county2zone.csv': + df_disagg_in = dfin[dfin[region_col].isin(agglevel_variables['county_regions2ba'])] + else: + df_disagg_in = dfin[dfin[region_col].isin(agglevel_variables['county_regions'])] + # Clause to separate mixed BA and county data in wide format + elif region_col == 'wide': + df_disagg_in = separate_wide_mixed_data( + dfin, + columns, + row.fix_cols, + agglevel_variables['county_regions'], + ) + + # Files where aggfunc_disagg is 'ignore' but aggfunc_agg is NOT 'ignore' + # indicate that the file will already have county-level data + # (e.g. csp.h5, recf.h5, load.h5). Copy the separated county-level columns + # to df_disagg so the script can add them back to dfout once the ba-level data + # is aggregated + if aggfunc_agg != 'ignore': + df_disagg = df_disagg_in.copy() + + else: + # Need to read in input data at BA level to be disaggregated + # If column headers are region names, need to filter after reformatting + if region_col != 'wide': + if two_col : + df_disagg_in = dfin[dfin[region_col[0]].isin(agglevel_variables['county_regions2ba'])] + df_disagg_in = df_disagg_in[df_disagg_in[region_col[1]] + .isin(agglevel_variables['county_regions2ba'])] + else: + df_disagg_in = dfin[dfin[region_col].isin(agglevel_variables['county_regions2ba'])] + else: + col_list = row.fix_cols + agglevel_variables['county_regions2ba'] + # Check if data exists for all BAs in county_regions2ba list + regions_list = [] + for ba in agglevel_variables['county_regions2ba']: + if ba not in dfin.columns : + regions_list.append(ba) + # If there is a ba for which there is no data, exclude this ba from the column list + if len(regions_list) >1 : + reduced_ba_regions = [x for x in agglevel_variables['county_regions2ba'] if x not in regions_list] + df_disagg_in = dfin[row.fix_cols + reduced_ba_regions] + else: + df_disagg_in = dfin[col_list] + + #Set aggfunc to disaggregation function + aggfunc = aggfunc_disagg + + #####Reformat county Data #### + df_disagg = reshape_to_long( + df_disagg_in, + filepath, + row, + indexnames, + aggfunc, + region_col, + region_cols, + ) + + ########### Single Resolution Procedure ########### + else: + df = reshape_to_long( + dfin, + filepath, + row, + indexnames, + aggfunc, + region_col, + region_cols, + ) + + #If the file is empty, move on to the next one as there is nothing to aggregate + if df.empty: + if verbose > 1: + logprint(filepath, 'empty') + return + + #%%####################################### + ### Aggregate/Dissaggregate by Region ### + + ########## Mixed Resolution Procedure ########## + if agglevel_variables['lvl'] == 'mult': + ########## BA, Aggreg ########## + # If there are no BA level data, set BA addition to False + # This will prevent appending an empty dataframe to the county level data below + if df_agg.empty: + ba_addition = False + else: + ba_addition = True + # Set aggregation function to aggfunc column value (Done in Settings section above) + aggfunc = aggfunc_agg + # Check if solving at aggreg, otherwise aggregating BA data to lower resolution is not necessary + if 'aggreg' in agglevel: + r2aggreg = r_aggreg + r_ba = r_aggreg + + # If aggfunc is not 'ignore', perform aggregation + if aggfunc != 'ignore': + df_agg = aggreg_methods( + df_agg, row, aggfunc, region_cols, region_col, + r2aggreg, r_ba, disagg_data, sw, indexnames, columns, + ) + + ########## County ########## + + # If there are no county level data, set county addition to False + # This will prevent appending an empty dataframe to the BA level data below + if df_disagg.empty: + county_addition = False + else: + county_addition = True + # Set aggregation function to aggfunc column value (Done in Settings section above) + aggfunc = aggfunc_disagg + # If aggfunc is not 'ignore', perform disaggregation + if aggfunc != 'ignore': + # Ensure correct r_ba is used + r_ba = r_ba_for_county + r2aggreg = r_ba + + # Disagg for county + df_disagg = aggreg_methods( + df_disagg, row, aggfunc, region_cols, region_col, + r2aggreg, r_ba, disagg_data, sw, indexnames, columns, + ) + + # Combine aggregated and disaggregated data + if ba_addition and county_addition: + # Combined BA and county data + if region_col == 'wide': + dfout = pd.merge(df_agg, df_disagg, how='inner', on=row.fix_cols) + else: + dfout = pd.concat([df_agg,df_disagg]) + # Aggregated data only + if ba_addition and not county_addition: + dfout = df_agg.copy() + # Disaggregated data only + if not ba_addition and county_addition: + dfout = df_disagg.copy() + + # If neither data exists, skip file + if not ba_addition and not county_addition: + if verbose > 1: + logprint(filepath, 'empty') + return + + + ########## Single Resolution Procedure ########## + else: + if agglevel_variables['lvl'] == 'county': + r2aggreg = r_county + else: + r2aggreg = r2aggreg_glob + r_ba = r_ba_glob + dfout = aggreg_methods( + df, row, aggfunc, region_cols, region_col, + r2aggreg, r_ba, disagg_data, sw, indexnames, columns, + ) + + #%%#################### + ### Data Write-Out ### + + if filetype in ['csv','gz']: + dfout.round(decimals).to_csv( + os.path.join(inputs_case, filepath), + header=(False if header is None else True), + index=False, + ) + elif filetype == 'h5': + if header == 'keepindex': + dfwrite = dfout.sort_values(indexnames).set_index(indexnames) + dfwrite.columns.name = None + else: + dfwrite = dfout + reeds.io.write_profile_to_h5(dfwrite, filepath, inputs_case) + elif filetype == 'gdx': + ### Overwrite the projected parameter + dfall[row.key] = dfout.round(decimals) + ### Write the whole file + gdxpds.to_gdx(dfall, inputs_case+filepath) + + if verbose > 1: + now = datetime.datetime.now() + logprint( + filepath, + 'aggregated ({:.1f} seconds)'.format((now-filetic).total_seconds())) + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +#%% Parse arguments +parser = argparse.ArgumentParser(description='Extend inputs to arbitrary future year') +parser.add_argument('reeds_path', help='path to ReEDS directory') +parser.add_argument('inputs_case', help='path to inputs_case directory') + +args = parser.parse_args() +reeds_path = args.reeds_path +inputs_case = os.path.join(args.inputs_case) + +# #%%## Settings for testing +# reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# inputs_case = os.path.join( +# reeds_path,'runs','v20250301_hydroM0_Pacific','inputs_case') + +#%% Settings for debugging +### Set debug == True to copy the original files to a new folder (inputs_case_original). +### If debug == False, the original files are overwritten. +debug = True +### missing: 'raise' or 'warn' +missing = 'raise' +### verbose: 0, 1, 2 +verbose = 2 + +#%%################# +### FIXED INPUTS ### +decimals = 6 +### anchortype: 'load' sets rb with largest 2010 load as anchor reg; +### 'size' sets largest rb as anchor reg +anchortype = 'size' +### Types of cost data in files that use sc_cat +sc_cost_types = ['cost', 'cost_cap', 'cost_trans'] + +###################### +### DERIVED INPUTS ### +### Get the case name (ReEDS-2.0/runs/{casename}/inputscase/) +casename = inputs_case.split(os.sep)[-3] + +#%% Set up logger +log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case, '..', 'gamslog.txt'), +) + +#%% Inputs from switches +sw = pd.read_csv( + os.path.join(inputs_case, 'switches.csv'), header=None, index_col=0).squeeze(1) +endyear = int(sw.endyear) +GSw_CSP_Types = [int(i) for i in sw.GSw_CSP_Types.split('_')] + +scalars = pd.read_csv( + os.path.join(inputs_case, 'scalars.csv'), + header=None, usecols=[0,1], index_col=0).squeeze(1) + +# Use agglevel_variables function to obtain spatial resolution variables +agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) +agglevel = agglevel_variables['agglevel'] +# Regions present in the current run +val_r_all = sorted( + pd.read_csv( + os.path.join(inputs_case, 'val_r_all.csv'), header=None, + ).squeeze(1).tolist() +) +#%% +#DEBUG: Copy the original inputs_case files +if debug and (agglevel != 'ba'): + print('Copying original inputs_case file...') + import distutils.dir_util + os.makedirs(inputs_case+'_original', exist_ok=True) + distutils.dir_util.copy_tree(inputs_case, inputs_case+'_original', verbose=0) +#%% +### Mixed Resolution Procedure ### +if agglevel_variables['lvl'] == 'mult' : + # Get the various region maps created in copy_files.py + #Need to store separate r_ba values for county and BA data + r_county = pd.read_csv(os.path.join(inputs_case,'r_county.csv'), index_col='county').squeeze() + r_ba = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')) + r_ba_for_county = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')).rename(columns={'r':'FIPS'}) + r_ba_for_ba = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')).set_index('ba').squeeze() + + for val in agglevel_variables['agglevel'] : + if val == 'aggreg': + ### Get map from BA to aggreg + r_aggreg = pd.read_csv(os.path.join(inputs_case,'rb_aggreg.csv')).set_index('ba')['aggreg'].to_dict() + + +### Single Resolution Procedure ### +else: + # Get the various region maps created in copy_files.py + r_county = pd.read_csv(os.path.join(inputs_case,'r_county.csv'), index_col='county').squeeze() + r_ba = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')) + # r_ba needs to be in different formats depending on whether you are aggregating + # or disaggregating + if agglevel in ['county']: + r_ba.rename(columns={'r':'FIPS'}, inplace=True) + elif agglevel in ['ba','aggreg']: + r_ba = r_ba.set_index('ba').squeeze() + ### Make all-regions-to-aggreg map + r2aggreg = pd.concat([r_county, r_ba]) + +##################################################### +### If using default 134 regions, exit the script ### + +if agglevel == 'ba': + print('all valid regions are BA, so skip aggregate_regions.py') + quit() +else: + print('Starting aggregate_regions.py', flush=True) + ## Read in disaggregation data + disagg_data = { + 'population': pd.read_csv( + os.path.join(inputs_case,'disagg_population.csv'), + header=0, + dtype={'fracdata':np.float32}, + usecols=["PCA_REG", "FIPS", "fracdata"] + ), + 'geosize': pd.read_csv( + os.path.join(inputs_case,'disagg_geosize.csv'), + header=0, + usecols=["PCA_REG", "FIPS", "fracdata"] + ), + 'hydroexist': pd.read_csv( + os.path.join(inputs_case,'disagg_hydroexist.csv'), + header=0, + usecols=["PCA_REG", "FIPS", "i", "fracdata"] + ) + } + +#%%###################################################### +### Get the "anchor" zone for each aggregation region ### + +# For transmission we want to use the old endpoints to avoid requiring a new run of the +# reV least-cost-paths procedure. + +if 'aggreg' in agglevel: + if agglevel_variables['lvl'] == 'mult': + r_ba = r_aggreg + + # Written in reeds.io.get_zonemap() + reeds.io.get_zonemap(reeds.io.standardize_case(inputs_case)) + aggreg2anchorreg = pd.read_csv(os.path.join(inputs_case, 'aggreg2anchorreg.csv')) + aggreg2anchorreg = aggreg2anchorreg.set_index('aggreg') + aggreg2anchorreg = aggreg2anchorreg.squeeze() + anchorreg2aggreg = pd.Series(index=aggreg2anchorreg.values, data=aggreg2anchorreg.index) + + ### Get RSC VRE available capacity to use in capacity-weighted averages + ### We need the original un-aggregated supply curves, so run writesupplycurves again + # rscweight = pd.read_csv(os.path.join(inputs_case, 'rsc_combined.csv')) + + # Read generator database and create rsc_wsc (for use in writesupplycurves function call below) + gendb = pd.read_csv(os.path.join(inputs_case,'unitdata.csv')) + import writecapdat + from writecapdat import create_rsc_wsc + # Set the 'r' column for the generator database + # Create the 'r_col' column + gendb = gendb.assign(r=gendb.reeds_ba.map(r_ba)) + startyear = int(sw.startyear) + rsc_wsc = create_rsc_wsc(gendb, TECH=writecapdat.TECH, scalars=scalars,startyear=startyear) + + import writesupplycurves + rscweight = writesupplycurves.main( + reeds_path, inputs_case, AggregateRegions=0, rsc_wsc_dat=rsc_wsc, write=False) + rscweight = ( + rscweight.loc[(rscweight.sc_cat=='cap')] + .rename(columns={'*i':'i'}) + .drop_duplicates(subset=['i','r','rscbin']) + [['i','r','rscbin','value']].rename(columns={'value':'MW'}) + ).copy() + + ## Add PVB values to rscweight_nobin in case we need them + rscweight_nobin = rscweight.groupby(['i','r'], as_index=False).sum(numeric_only=True) + pvbtechs = [f'pvb{i}' for i in sw.GSw_PVB_Types.split('_')] + tocopy = rscweight_nobin.loc[rscweight_nobin.i.str.startswith('upv')].copy() + rscweight_nobin = pd.concat( + [rscweight_nobin] + + [tocopy.assign(i=tocopy.i.str.replace('upv',pvbtech)) for pvbtech in pvbtechs] + ) + ## Remove duplicate CSP values for different solar multiples + rscweight_nobin.i.replace( + {f'csp{i+1}_{c+1}': f'csp_{c+1}' + for i in GSw_CSP_Types + for c in range(int(sw.GSw_NumCSPclasses))}, + inplace=True + ) + rscweight_nobin.drop_duplicates(['i','r'], inplace=True) + +# rscweight_nobin required to be defined for aggreg_methods function call to work +else: + rscweight_nobin=None + +#%% Get the mapping to reduced-resolution technology classes +original_num_classes = {**{f'csp{i}':12 for i in range(1,5)}} +new_classes = {} +for tech in [f'csp{i}' for i in range(1,5)]: + GSw_NumClasses = int(sw['GSw_Num{}classes'.format(tech.upper().strip('1234'))]) + ## Spread the new classes roughly evenly out over the old classes + num_in_step = original_num_classes[tech] // GSw_NumClasses + remainder = original_num_classes[tech] % GSw_NumClasses + new_classes[tech] = sorted( + list(np.ravel([[i]*num_in_step for i in range(1,GSw_NumClasses+1)])) + + list(range(1,remainder+1)) + ) + new_classes[tech] = dict(zip( + [f'{tech}_{i}' for i in range(1,original_num_classes[tech]+1)], + [f'{tech}_{i}' for i in new_classes[tech]] + )) +### Combine all the new classes into one dictionary +new_classes = {k:v for d in new_classes for k,v in new_classes[d].items()} + +#%% Get the settings file +runfiles = ( + pd.read_csv( + os.path.join(reeds_path, 'reeds', 'input_processing', 'runfiles.csv'), + dtype={'fix_cols':str}, index_col='filename', + comment='#', + ).fillna({'fix_cols':''}) + .rename(columns={'wide (1 if any parameters are in wide format)':'wide', + 'header (0 if file has column labels)':'header'}) + ) +#%% If any files are missing, stop and alert the user +inputfiles = sorted([ + f.split('inputs_case'+os.sep)[1] + for f in glob(os.path.join(inputs_case,'**'), recursive=True) + if 'metadata' not in f +]) +## Drop the directories and backup h17 files +inputfiles = [f for f in inputfiles if (('.' in f) and not f.endswith('_h17.csv'))] +missingfiles = [f for f in inputfiles if (os.path.basename(f) not in runfiles.index.values) +] +if any(missingfiles): + if missing == 'raise': + raise Exception( + 'Missing aggregation method for:\n{}\n' + '>>> Need to add entries for these files to runfiles.csv' + .format('\n'.join(missingfiles)) + ) + else: + from warnings import warn + warn( + 'Missing aggregation directions for:\n{}\n' + '>>> For this run, these files are copied without modification' + .format('\n'.join(missingfiles)) + ) + for f in missingfiles: + shutil.copy(os.path.join(inputs_case, f), os.path.join(inputs_case, f)) + print(f'copied {f}, which is missing from runfiles.csv') + +#%% Maps (special case) +mapsfile = os.path.join(inputs_case, 'maps.gpkg') +if os.path.exists(mapsfile): + os.remove(mapsfile) +dfmap = reeds.io.get_dfmap(os.path.dirname(inputs_case)) +for level in dfmap: + dfmap[level].rename_axis(level).to_file(mapsfile, layer=level) + +dfmap = reeds.io.get_dfmap(os.path.dirname(inputs_case)) + +### Aggregate or disaggregate the 'r' map; none of the rest should change +# Mixed resolution maps are patched together in the get_zonemap() function +if agglevel_variables['lvl'] == 'mult' : + pass + +#Single resolution procedure +else: + match agglevel: + case 'aggreg': + r2aggreg = pd.read_csv( + os.path.join(inputs_case, 'hierarchy_original.csv') + ).rename(columns={'ba':'r'}).set_index('r').aggreg + case 'county': + aggreg2anchorreg = r2aggreg = r_county.copy() + + + dfmap_r_agg = dfmap['r'].reset_index().rename(columns={'rb':'r', 'ba':'r'}) + dfmap_r_agg.r = dfmap_r_agg.r.map(r2aggreg) + dfmap_r_agg = dfmap_r_agg.dissolve('r').loc[aggreg2anchorreg.index].copy() + + ## Map endpoints to anchor regions + for j in ['x','y']: + dfmap_r_agg[j] = dfmap['r'][j].loc[dfmap_r_agg[j].index.map(aggreg2anchorreg)].values + dfmap_r_agg[f'centroid_{j}'] = dfmap_r_agg.centroid.x if j == 'x' else dfmap_r_agg.centroid.y + + ## Overwrite the non-aggregated zone map + dfmap['r'] = dfmap_r_agg.drop(columns='county', errors='ignore') + + ## Write the aggregated maps + mapsfile = os.path.join(inputs_case, 'maps.gpkg') + if os.path.exists(mapsfile): + os.remove(mapsfile) + for level in dfmap: + ( + dfmap[level] + .drop(columns='aggreg', errors='ignore') + .rename_axis(level) + .to_file(mapsfile, layer=level) + ) + +#%% +if agglevel_variables['lvl'] == 'mult' or agglevel == 'county': + r2aggreg_glob = None +else: + r2aggreg_glob = r2aggreg +r_ba_glob = r_ba + +# loop over inputfiles from runfiles and call aggregation/disaggregation function +for filepath in inputfiles: + ### For debugging: Specify a file + # filepath = '' + ### Get the appropriate row from runfiles + row = runfiles.loc[os.path.basename(filepath)] + try: + agg_disagg(filepath, r2aggreg_glob, r_ba_glob, row) + except Exception as err: + print(f"Error processing {filepath}") + raise Exception(err) + +#%% Finish +reeds.log.toc(tic=tic, year=0, process='inputs/aggregate_regions.py', + path=os.path.join(inputs_case,'..')) + +print('Finished aggregate_regions.py') diff --git a/reeds/input_processing/calc_financial_inputs.py b/reeds/input_processing/calc_financial_inputs.py new file mode 100644 index 00000000..60dc7e0e --- /dev/null +++ b/reeds/input_processing/calc_financial_inputs.py @@ -0,0 +1,527 @@ +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import pandas as pd +import numpy as np +import os +import sys +import datetime +# Time the operation of this script +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds + + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +def calc_financial_inputs(inputs_case): + """ + Write the following files to runs/{batch_case}/inputs_case/: + - cap_cost_mult_noITC.csv + - co2_capture_incentive.csv + - crf.csv + - crf_co2_incentive.csv + - crf_h2_incentive.csv + - depreciation_schedules.csv + - h2_ptc.csv + - inflation.csv + - itc_fractions.csv + - ivt.csv + - ptc_values.csv + - ptc_value_scaled.csv + - pvf_onm_int.csv + - pvf_cap.csv + - reg_cap_cost_diff.csv + - retail_eval_period.h5 + - retail_depreciation_sch.h5 + """ + print('Starting calculation of financial parameters') + + # #%% Settings for testing + # reeds_path = '/Users/pbrown/github/ReEDS-2.0/' + # inputs_case = os.path.join(reeds_path,'runs','v20220621_NTPm0_ercot_seq_test','inputs_case') + + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + sw['endyear'] = int(sw['endyear']) + sw['sys_eval_years'] = int(sw['sys_eval_years']) + + scalars = reeds.io.get_scalars(inputs_case) + + #%% Import some general data and maps + + # Import inflation (which includes both historical and future inflation). + # Used for adjusting currency inputs to the specified dollar year, and financial calculations. + inflation_df = pd.read_csv(os.path.join(inputs_case,'inflation.csv')) + + # Import tech groups. Used to expand data inputs + # (e.g., 'UPV' expands to all of the upv subclasses, like upv_1, upv_2, etc) + tech_groups = reeds.techs.import_tech_groups(os.path.join(inputs_case, 'tech-subset-table.csv')) + + # Set up scen_settings object + scen_settings = reeds.financials.scen_settings( + dollar_year=int(sw['dollar_year']), tech_groups=tech_groups, inputs_case=inputs_case, + sw=sw) + + + #%% Ingest data, determine what regions have been specified, and build df_ivt + # Build df_ivt (for calculating various parameters at subscript [i,v,t]) + + techs = pd.read_csv(os.path.join(inputs_case,'techs.csv')) + techs = reeds.techs.expand_GAMS_tech_groups(techs) + vintage_definition = pd.read_csv(os.path.join(inputs_case, 'ivt.csv')).rename(columns={'Unnamed: 0':'i'}) + + annual_degrade = pd.read_csv(os.path.join(inputs_case,'degradation_annual.csv'), + header=None, names=['i','annual_degradation']) + annual_degrade = reeds.techs.expand_GAMS_tech_groups(annual_degrade) + ### Assign the PV+battery values to values for standalone batteries + annual_degrade = reeds.financials.append_pvb_parameters( + dfin=annual_degrade, + tech_to_copy='battery_li') + + years, modeled_years, year_map = reeds.financials.ingest_years( + inputs_case, sw['sys_eval_years'], sw['endyear']) + + df_ivt = reeds.financials.build_dfs(years, techs, vintage_definition, year_map) + print('df_ivt created') + + #%% Import and merge data onto df_ivt + + # Import system-wide real discount rates, calculate present-value-factors, merge onto df's + financials_sys = reeds.financials.import_sys_financials( + sw['financials_sys_suffix'], inflation_df, modeled_years, + years, year_map, sw['sys_eval_years'], scen_settings, scalars['co2_capture_incentive_length'],scalars['h2_ptc_length']) + financials_sys.to_csv(os.path.join(inputs_case,'financials_sys.csv'),index=False) + df_ivt = df_ivt.merge( + financials_sys[['t', 'pvf_capital', 'crf', 'crf_co2_incentive','crf_h2_incentive','d_real', 'd_nom', 'interest_rate_nom', + 'tax_rate', 'debt_fraction', 'rroe_nom']], + on=['t'], how='left') + + # The script only works for USA currently. Something needs to be added here + # to expand to other countries, if needed. + df_ivt['country'] = 'usa' + + + # Merge inflation into investment df + df_ivt = df_ivt.merge(inflation_df, on=['t'], how='left', ) + + # Merge annual degradation into investment df + df_ivt = df_ivt.merge(annual_degrade, on=['i'], how='left') + df_ivt['annual_degradation'] = df_ivt['annual_degradation'].fillna(0.0) + + ### Import financial assumptions + financials_tech = reeds.financials.import_data( + file_root='financials_tech', file_suffix=sw['financials_tech_suffix'], + indices=['i','country','t'], scen_settings=scen_settings) + # Apply the values for standalone batteries to PV+B batteries + financials_tech = reeds.financials.append_pvb_parameters( + dfin=financials_tech, + tech_to_copy='battery_li') + # If the battery in PV+B gets the ITC, it gets 5-year MACRS depreciation as well + if float(scen_settings.sw['GSw_PVB_BatteryITC']) >= 0.75: + financials_tech.loc[ + financials_tech.i.str.startswith('pvb') & (financials_tech.country == 'usa'), + 'depreciation_sch' + ] = 5 + + ### Project financials_tech forward + financials_tech_projected = financials_tech.pivot( + index=['i','country','depreciation_sch','eval_period','construction_sch'], + columns=['t'])['finance_diff_real'] + lastdatayear = max(financials_tech_projected.columns) + for addyear in range(lastdatayear+1, sw['endyear']+1): + financials_tech_projected[addyear] = financials_tech_projected[lastdatayear] + # Overwrite with projected values + financials_tech = financials_tech_projected.stack().rename('finance_diff_real').reset_index() + # Merge with df_ivt + df_ivt = df_ivt.merge(financials_tech, on=['i', 't', 'country'], how='left') + + # Calculate multipliers to account for evaluation periods. If a tech's eval period is + # the system-wide default, this will be 1. If not, the capital costs are adjusted accordingly. + financials_sys['sys_pvf_eval_period_sum'] = (1 - (1 / (financials_sys['d_real'])**(sw['sys_eval_years']-1))) / (financials_sys['d_real']-1.0) + 1 + df_ivt['pvf_eval_period_sum'] = (1 - (1 / (df_ivt['d_real'])**(df_ivt['eval_period']-1))) / (df_ivt['d_real']-1.0) + 1 + df_ivt = df_ivt.merge(financials_sys[['sys_pvf_eval_period_sum', 't']], on='t', how='left') + df_ivt['eval_period_adj_mult'] = df_ivt['sys_pvf_eval_period_sum'] / df_ivt['pvf_eval_period_sum'] + + #%% Process incentives + + # Import incentives, shift eligibility by safe harbor, merge incentives + incentive_df = reeds.financials.import_and_mod_incentives( + incentive_file_suffix=sw['incentives_suffix'], + inflation_df=inflation_df, scen_settings=scen_settings) + df_ivt = df_ivt.merge(incentive_df, on=['i', 't', 'country'], how='left') + df_ivt['safe_harbor'] = df_ivt['safe_harbor'].fillna(0.0) + df_ivt['co2_capture_value_monetized'] = df_ivt['co2_capture_value_monetized'].fillna(0.0) * (1 / (1 - df_ivt['tax_rate'])) + df_ivt['h2_ptc_value_monetized'] = df_ivt['h2_ptc_value_monetized'].fillna(0.0) * (1 / (1 - df_ivt['tax_rate'])) + + ### Calculate the tax impacts of the PTC, and calculate the adjustment to reflect the + # difference between the PTC duration and ReEDS evaluation period + df_ivt = reeds.financials.adjust_ptc_values(df_ivt) + + # Expand co2_capture_value by the duration of the incentive. + co2_capture_value = df_ivt[['i', 'v', 't', 'co2_capture_value_monetized', 'co2_capture_dur']].copy() + co2_capture_value = co2_capture_value[co2_capture_value['co2_capture_value_monetized']>0] + if len(co2_capture_value) > 0: + dur_list = [] # create year expander + for n in list(co2_capture_value['co2_capture_dur'].drop_duplicates()): + dur_df = pd.DataFrame() + dur_df['year_adder'] = np.arange(0,n) + dur_df['co2_capture_dur'] = n + dur_list += [dur_df.copy()] + expander = pd.concat(dur_list, ignore_index=True, sort=False) + co2_capture_value = co2_capture_value.merge(expander, on='co2_capture_dur', how='left') + co2_capture_value['t'] = co2_capture_value['t'] + co2_capture_value['year_adder'] + else: + co2_capture_value = df_ivt[['i', 'v', 't', 'co2_capture_value_monetized', 'co2_capture_dur']].iloc[0:5,:] + co2_capture_value = co2_capture_value.drop_duplicates(['i', 'v', 't']) + co2_capture_value['v'] = ['new%s' % v for v in co2_capture_value['v']] + co2_capture_value['t'] = co2_capture_value['t'].astype(int) + + # Expand h2_ptc_value by the duration of the incentive. + h2_ptc_value = df_ivt[['i', 'v', 't', 'h2_ptc_value_monetized', 'h2_ptc_dur']].copy() + h2_ptc_value = h2_ptc_value[h2_ptc_value['h2_ptc_value_monetized']>0] + if len(h2_ptc_value) > 0: + dur_list = [] # create year expander + for n in list(h2_ptc_value['h2_ptc_dur'].drop_duplicates()): + dur_df = pd.DataFrame() + dur_df['year_adder'] = np.arange(0,n) + dur_df['h2_ptc_dur'] = n + dur_list += [dur_df.copy()] + expander = pd.concat(dur_list, ignore_index=True, sort=False) + h2_ptc_value = h2_ptc_value.merge(expander, on='h2_ptc_dur', how='left') + h2_ptc_value['t'] = h2_ptc_value['t'] + h2_ptc_value['year_adder'] + else: + h2_ptc_value = df_ivt[['i', 'v', 't', 'h2_ptc_value_monetized', 'h2_ptc_dur']].iloc[0:5,:] + h2_ptc_value = h2_ptc_value.drop_duplicates(['i', 'v', 't']) + h2_ptc_value['v'] = ['new%s' % v for v in h2_ptc_value['v']] + h2_ptc_value['t'] = h2_ptc_value['t'].astype(int) + + # Expand the various ptc values by the duration of the incentive. + # We are tracking various ptc_values (e.g. with and without tax grossups) + # because different downstream processes require different forms of the ptc's value + ptc_values_df = df_ivt[['i', 'v', 't', 'ptc_value', 'ptc_dur', 'ptc_value_monetized', 'ptc_tax_equity_penalty', + 'ptc_value_monetized_posttax', 'ptc_grossup_value', 'ptc_value_scaled']].copy() + ptc_values_df = ptc_values_df[ptc_values_df['ptc_value']>0] + if len(ptc_values_df) > 0: + dur_list = [] # create year expander + for n in list(ptc_values_df['ptc_dur'].drop_duplicates()): + dur_df = pd.DataFrame() + dur_df['year_adder'] = np.arange(0,n) + dur_df['ptc_dur'] = n + dur_list += [dur_df.copy()] + expander = pd.concat(dur_list, ignore_index=True, sort=False) + ptc_values_df = ptc_values_df.merge(expander, on='ptc_dur', how='left') + ptc_values_df['t'] = ptc_values_df['t'] + ptc_values_df['year_adder'] + else: + ptc_values_df = df_ivt[['i', 'v', 't', 'ptc_value', 'ptc_dur', 'ptc_value_monetized', 'ptc_tax_equity_penalty', + 'ptc_value_monetized_posttax', 'ptc_grossup_value', 'ptc_value_scaled']].iloc[0:5,:] # this is just a hack because pjg didn't know how to have gams handle empty files + ptc_values_df = ptc_values_df.drop_duplicates(['i', 'v', 't']) + ptc_values_df['v'] = ['new%s' % v for v in ptc_values_df['v']] + ptc_values_df['t'] = ptc_values_df['t'].astype(int) + + + + #%% + # Import schedules for financial calculations + construction_schedules = pd.read_csv(os.path.join(inputs_case,'construction_schedules.csv')) + depreciation_schedules = pd.read_csv(os.path.join(inputs_case,'depreciation_schedules.csv')) + + ### Calculate financial multipliers + print('Calculating financial multipliers...') + df_ivt = reeds.financials.calc_financial_multipliers( + df_ivt, construction_schedules, depreciation_schedules, sw['timetype']) + + + #%%### Calculate financial multipliers for transmission + ### Load transmission data + dftrans = pd.read_csv(os.path.join(inputs_case,'financials_transmission.csv')) + ### Get transmission capital recovery period (CRP) from input scalars + dftrans['eval_period'] = int(scalars['trans_crp']) + ### Get online year + dftrans['t'] = dftrans.t_start_construction + dftrans.construction_time + ### Get ITC monetization + dftrans['itc_frac_monetized'] = dftrans.itc_frac * (1 - dftrans.itc_tax_equity_penalty) + ### Get sys financials + dftrans = dftrans.merge(financials_sys.dropna(how='any'), on='t', how='right') + ### Get financial multipliers + dftrans = reeds.financials.calc_financial_multipliers( + df_inv=dftrans, construction_schedules=construction_schedules, + depreciation_schedules=depreciation_schedules, timetype=sw['timetype'], + ) + + ### Get the CRF for transmission + dftrans['crf_tech'] = reeds.financials.calc_crf(dftrans['d_real'], dftrans['eval_period']) + + ### Get the final capital cost multiplier (including the CRF scaler above) + dftrans['cap_cost_mult'] = reeds.financials.calc_final_capital_cost_multiplier(dftrans) + dftrans['cap_cost_mult_noITC'] = reeds.financials.calc_final_capital_cost_multiplier(dftrans, mult_type='finMult_noITC') + + ### The transmission ITC is not meant to apply to currently-planned transmission. + ### So for years before firstyear_trans, use cap_cost_mult_noITC; + ### i.e. only start applying the ITC once the model switches to endogenous transmission. + firstyear_trans = int(scalars['firstyear_trans_longterm']) + dftrans.loc[dftrans.t lastdatayear: + dfout[endyear] = dfout.loc[:,lastdatayear] + # If data start after 2010, add a column for 2010 + if (2010 not in dfout.columns) and all(dfout.columns.values > 2010): + ## For UnappWaterSeaAnnDistr we give the new values directly rather than as a ratio, + ## so we backfill with the earliest available data + if infile == 'UnappWaterSeaAnnDistr': + dfout[2010] = dfout.iloc[:,0] + ## Otherwise we fill with 1 (i.e. no change). Note that since we interpolate to the + ## next value, the years between 2010 and the first year with data will not be 1. + else: + dfout[2010] = 1. + ## Move 2010 column to the front of the dataframe + dfout.sort_index(axis=1, inplace=True) + # Interpolate to missing years + dfinterp = ( + dfout + ## Switch column names from integer years to timestamps + .rename(columns={c: pd.Timestamp(str(c)) for c in dfout.columns}) + ## Add empty columns at year-starts between existing data (mean doesn't do anything) + .resample('YS', axis=1).mean() + ## Interpolate linearly to fill the new columns + .T.interpolate('linear').T + ) + dfout = ( + ## Switch back to integer year column names + dfinterp.rename(columns={c: c.year for c in dfinterp.columns}) + ## Drop extra data after the model end year + .loc[:,:endyear] + ) + + # Files indexed by month undergo additional processing in hourly_writetimeseries. Create + # intermediate filenames for these files and melt them back to long format + file_prefix = 'temp' if 'month' in dfout.index.names else 'climate' + keepindex = False if file_prefix == 'temp' else True + if file_prefix == 'temp': + dfout = pd.melt( + dfout.reset_index(), id_vars=index[infile], value_vars=dfout.columns, + var_name='t', value_name='Value' + ) + + # Write it to output folder + dfout.round(decimals).to_csv(os.path.join(inputs_case, f'{file_prefix}_{infile}.csv'), + index=keepindex) + + return dfout + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if GSw_ClimateWater: + print('Writing annual and seasonal water climate multipliers') + for infile in ['UnappWaterMult','UnappWaterMultAnn','UnappWaterSeaAnnDistr']: + readwrite(infile=infile) + +if GSw_ClimateHydro: + print('Writing annual and seasonal hydro climate multipliers') + for infile in ['hydadjann','hydadjsea']: + readwrite(infile=infile) + +if not any([GSw_ClimateWater,GSw_ClimateHydro]): + print("All climate switches are off.") + +reeds.log.toc(tic=tic, year=0, process='inputs/climateprep.py', + path=os.path.join(inputs_case,'..')) + +print('Finished climateprep.py') diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py new file mode 100644 index 00000000..08d80dc7 --- /dev/null +++ b/reeds/input_processing/copy_files.py @@ -0,0 +1,1683 @@ +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== +import os +import sys +import datetime +import numpy as np +import pandas as pd +import argparse +import shutil +import yaml +import json +import h5py +from pathlib import Path +# Local Imports +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds + + +#%% =========================================================================== +### --- General Read Functions--- +### =========================================================================== +def is_required_file(runfiles_row, sw): + """ + Determine whether or not the file corresponding to the provided row of + runfiles.csv is required using the row's "required_if" value. + + Note that the code snippets assume that a variable 'sw' has been + initialized and holds the result of reeds.io.get_switches(), which is why + this function takes 'sw' as an argument despite 'sw' not being used + explicitly. + """ + required_if_value = runfiles_row['required_if'] + is_required = eval(required_if_value) + if is_required not in [True, False, 1, 0]: + raise ValueError( + "The 'required_if' value must evaluate to a true/false statement." + f"Update the entry for {runfiles_row['filename']} in " + "runfiles.csv." + ) + return is_required + + +def read_runfiles(reeds_path, inputs_case, sw, agglevel_variables): + """ + Read runfiles.csv and return the runfiles dataframe + Identify files that have a region index versus those that do not. + """ + runfiles = ( + pd.read_csv( + os.path.join(reeds_path, 'reeds', 'input_processing', 'runfiles.csv'), + dtype={'fix_cols':str, + 'depends_on_switch':str, + 'depends_on_switch_value': str}, + comment='#', + ).fillna({'fix_cols':'', + 'depends_on_switch':'', + 'depends_on_switch_value':''}) + ) + + runfiles['file_is_required'] = runfiles.apply( + axis=1, + func=is_required_file, + args=(sw,), + ) + + # If a filepath isn't specified, that means it is already in the + # inputs_case folder, otherwise use the filepath + # We leave the 'lvl' portion of 'full_filepath' unformatted because + # we may need to read multiple 'lvl' variants of the file at once later + runfiles['full_filepath'] = runfiles.apply( + axis=1, + func=lambda row: os.path.join(inputs_case, row['filename']) + if pd.isna(row['filepath']) + else os.path.join(reeds_path, row['filepath'].format(**{**sw, **{'lvl': '{lvl}'}})) + ) + + # Create a copy of runfiles that specifies the 'lvl' that applies to each file + # (only used to determine missing files, the original runfiles is used later). + # In general, the 'lvl' corresponds to GSw_RegionResolution. + # For mixed resolution, each 'lvl'-indexed file is split into two rows - + # one for the BA-level file and the other for the county-level file. + runfiles_with_lvls = runfiles.assign(lvl='') + lvl_indexed_file_mask = ( + (runfiles_with_lvls.filepath.notna()) + & (runfiles_with_lvls.filepath.str.contains('{lvl}')) + ) + if agglevel_variables['lvl'] == 'mult': + runfiles_with_lvls.loc[lvl_indexed_file_mask, 'lvl'] = 'ba,county' + runfiles_with_lvls['lvl'] = runfiles_with_lvls['lvl'].str.split(',') + runfiles_with_lvls = runfiles_with_lvls.explode('lvl') + else: + runfiles_with_lvls.loc[lvl_indexed_file_mask, 'lvl'] = agglevel_variables['lvl'] + + # Determine existence of each file + runfiles_with_lvls['full_filepath'] = runfiles_with_lvls.apply( + axis=1, + func=lambda x: x['full_filepath'].format(**{'lvl': x['lvl']}) + ) + runfiles_with_lvls['file_exists'] = ( + runfiles_with_lvls['full_filepath'].apply(lambda x: os.path.exists(x)) + ) + + # Raise an error if any of the required files with specified filepaths are missing + missing_required_files = list( + runfiles_with_lvls.loc[( + runfiles_with_lvls['file_is_required'] + & ~runfiles_with_lvls['file_exists'] + & ~runfiles_with_lvls['filepath'].isna() + )]['filepath'] + ) + if len(missing_required_files) > 0: + raise FileNotFoundError( + 'The following required files are missing. Add them ' + 'to the inputs directory or update runfiles.csv to specify optionality:\n{}\n' + .format('\n'.join(missing_required_files)) + ) + + # Add file existence information to runfiles (for lvl-indexed files, the file must exist + # at all resolutions required for the run). + # We have to add this to runfiles rather than using runfiles_with_lvls because later + # sections require the 'lvl' placeholder in the filename to be unformatted and the file + # to be represented by one row rather than the multiple split up rows in runfiles_with_lvls. + runfiles['file_exists'] = ( + runfiles['filename'].map(runfiles_with_lvls.groupby('filename')['file_exists'].min()) + ) + + # Non-region files that need copied either do not have an entry in region_col + # or have 'ignore' as the entry. They also have a filepath specified. + non_region_files = ( + runfiles[ + ( + (runfiles['region_col'].isna()) + | (runfiles['region_col'] == 'ignore') + ) + & (~runfiles['filepath'].isna())] + ) + + # Region files are those that have a region and do not specify 'ignore' + # Also ignore files that are created after this script runs (i.e., post_copy = 1) + region_files = ( + runfiles[ + (~runfiles['region_col'].isna()) + & (runfiles['region_col'] != 'ignore') + & (runfiles['post_copy'] != 1)] + ) + + return runfiles, non_region_files, region_files + + +def get_source_deflator_map(reeds_path): + """ + Get the deflator for each input file + """ + # Inflation-adjusted inputs + sources_dollaryear = pd.read_csv( + os.path.join(reeds_path,'docs','sources.csv'), + usecols=["RelativeFilePath", "DollarYear"] + ) + deflator = pd.read_csv( + os.path.join(reeds_path,'inputs','financials','deflator.csv'), + header=0, names=['Dollar.Year','Deflator'], index_col='Dollar.Year').squeeze(1) + # Create a mapping between inputs' relative filepaths and their deflation + # multipliers based on the dollar years their monetary values are in + sources_dollaryear = ( + # Filter out rows that don't contain a valid dollar year + sources_dollaryear[pd.to_numeric(sources_dollaryear['DollarYear'], errors='coerce').notnull()] + # Note: We must remove the backslash that prepends each relative filepath + # for compatibility with the 'os' package (otherwise it is treated as an absolute path) + .assign(RelativeFilePath=sources_dollaryear["RelativeFilePath"].str[1:]) + .astype({"DollarYear": "int64"}) + .rename(columns={"DollarYear": "Dollar.Year"}) + .merge(deflator,on="Dollar.Year",how="left") + ) + + source_deflator_map = dict(zip(sources_dollaryear["RelativeFilePath"], sources_dollaryear["Deflator"])) + + return source_deflator_map + +def get_regions_and_agglevel( + reeds_path, + inputs_case, + save_regions_and_agglevel=True, +): + """ + Create a regional mapping to help filter for specific regions and aggregation levels. + This function reads various input files, processes them to create mappings of regions + at different levels of aggregation, and writes these mappings to csv files. + + If save_regions_and_agglevel is False do not save intermediate files + (You just want the mapping) + """ + sw = reeds.io.get_switches(inputs_case) + + ## TEMPORARY 20260402: Load the full regions list + ## Use the line below once we make the switch + # hierarchy = reeds.io.assemble_hierarchy(inputs_case) + hierarchy = pd.read_csv( + Path(reeds.io.reeds_path, 'inputs', 'zones', sw.GSw_ZoneSet, 'hierarchy_from134.csv') + ) + hierarchy['offshore'] = 0 + # Append offshore zones if using + if int(sw.GSw_OffshoreZones): + hierarchy_offshore = reeds.io.assemble_hierarchy( + fpath=os.path.join(reeds_path, 'inputs', 'zones', 'hierarchy_offshore.csv'), + extra=False, + ).assign(offshore=1) + hierarchy = pd.concat([hierarchy, hierarchy_offshore], ignore_index=True) + + # Save the original hierarchy file: used in recf.py and hourly_*.py scripts + if save_regions_and_agglevel: + hierarchy.to_csv( + os.path.join(inputs_case,'hierarchy_original.csv'), + index=False, header=True + ) + + # Add a row for each county + ## TEMPORARY 20260402: Use the old 134-zone county2zone until the aggregation approach is updated + county2zone = ( + reeds.io.get_county2zone(GSw_ZoneSet='z134', as_map=False) + .rename(columns={'r':'ba'}) + ) + county2zone['county'] = 'p' + county2zone.FIPS + county2zone.to_csv( + os.path.join(inputs_case, 'county2zone_original.csv'), + index=False + ) + + # Add county info to hierarchy + hierarchy = hierarchy.merge(county2zone.drop(columns=['FIPS','state']), on='ba', how='outer') + + # Subset hierarchy for the region of interest (based on the GSw_Region switch) + # Parse the GSw_Region switch. If it includes a '/' character, it has the format + # {column of hierarchy.csv}/{period-delimited entries to keep from that column}. + hier_sub = pd.DataFrame() + # allow the list defined by the user to include multiple spatial resolutions + region_groups = sw['GSw_Region'].split('//') if '//' in sw['GSw_Region'] else [sw['GSw_Region']] + # separate lists associated with each spatial resolution + for region_group in region_groups: + GSw_RegionLevel, GSw_Region = region_group.split('/') + GSw_Region = GSw_Region.split('.') + + hier_sub_partial = pd.concat([ + hierarchy[hierarchy[GSw_RegionLevel] == region] for region in GSw_Region + ]) + + hier_sub = pd.concat([hier_sub, hier_sub_partial]) + + # Read region resolution switch to determine agglevel + agglevel = sw['GSw_RegionResolution'].lower() + + # Check if desired spatial resolution is mixed + if agglevel == 'mixed': + #Set value in resolution column of hier_sub to match value assigned in modeled_regions.csv + region_def = pd.read_csv( + os.path.join(reeds_path,'inputs','userinput','modeled_regions.csv') + )[['r', sw.GSw_ZoneSet]] + + res_map = region_def.set_index('r').squeeze(1).to_dict() + hier_sub['resolution'] = hier_sub['ba'].map(res_map) + else: + hier_sub['resolution'] = agglevel + + + # Write out all unique aggregation levels present in the hierarchy resolution column + agglevels = hier_sub['resolution'].unique() + + # Write agglevel + if save_regions_and_agglevel: + pd.DataFrame(agglevels, columns=['agglevels']).to_csv( + os.path.join(inputs_case, 'agglevels.csv'), index=False) + + + # Create an r column at the front of the dataframe and populate it with the + # county-level regions (overwritten if needed) + hier_sub.insert(0,'r',hier_sub['county']) + + # Overwrite the regions with the ba, state, or aggreg values as specififed + for level in ['ba','aggreg']: + hier_sub.loc[hier_sub['resolution'] == level, 'r'] = ( + hier_sub.loc[hier_sub['resolution'] == level, level]) + + # Write out mappings of r and ba to all counties + r_county = hier_sub[['r','county']].dropna(subset='county') + ba_county = hier_sub[['ba','county']] + + # Rewrite county2zone for this case + county2zone_agg = county2zone.merge(r_county, on='county') + county2zone_agg.to_csv( + os.path.join(inputs_case, 'county2zone.csv'), + index=False + ) + + if save_regions_and_agglevel: + r_county.to_csv( + os.path.join(inputs_case, 'r_county.csv'), index=False) + + # Write out a mapping of r to ba regions + hier_sub[['r','ba']].drop_duplicates().to_csv( + os.path.join(inputs_case, 'r_ba.csv'), index=False) + # Write out mapping of r to census divisions + hier_sub[['r','cendiv']].drop_duplicates().to_csv( + os.path.join(inputs_case, 'r_cendiv.csv'), index=False) + + # Write out mapping of rb to aggreg (for writesupplycurves.py) + hier_sub[['ba','aggreg']].drop_duplicates().to_csv( + os.path.join(inputs_case, 'rb_aggreg.csv'), index=False) + + # Write out val_county and val_ba before collapsing to unique regions + hier_sub['county'].dropna().to_csv( + os.path.join(inputs_case, 'val_county.csv'), header=False, index=False) + hier_sub['ba'].drop_duplicates().to_csv( + os.path.join(inputs_case, 'val_ba.csv'), header=False, index=False) + + # Find all the unique elements that might define a region + val_r_all = [] + for column in hier_sub.columns.drop('offshore', errors='ignore'): + val_r_all.extend(hier_sub[column].dropna().unique().tolist()) + + # Converting to a set ensures that only unique values are kept + val_r_all = sorted(list(set(val_r_all))) + + if save_regions_and_agglevel: + pd.Series(val_r_all).to_csv( + os.path.join(inputs_case, 'val_r_all.csv'), header=False, index=False) + + # Rename columns and save as hierarchy_with_res.csv for use in agglevel_variables function + hier_sub.drop(columns='offshore', errors='ignore').rename(columns={'r':'*r'}).to_csv( + os.path.join(inputs_case, 'hierarchy_with_res.csv'), index=False) + + # Drop county name and resolution columns + hier_sub = hier_sub.drop(['county_name','resolution'],axis=1) + + + # Collapse to only unique regions + hier_sub = hier_sub.drop_duplicates(subset=['r']) + + # Sort hier_sub by r so that "ord(r)" commands in GAMS result in the properly + # ordered outputs + hier_sub['numeric_value'] = hier_sub['r'].str.extract('(\d+)').astype(float) + hier_sub = hier_sub.sort_values(by='numeric_value').drop('numeric_value', axis=1) + + # Output the itlgrp files for mixed and county resolution + + if sw.GSw_RegionResolution == 'aggreg': + hier_sub['itlgrp'] = hier_sub['aggreg'] + else: + hier_sub['itlgrp'] = hier_sub['ba'] + + if sw.GSw_RegionResolution == 'mixed': + mod_reg = pd.read_csv( + os.path.join(reeds_path,'inputs','userinput','modeled_regions.csv')) + if 'aggreg' in mod_reg[sw.GSw_ZoneSet].tolist(): + hier_sub['itlgrp'] = hier_sub['aggreg'] + hier_sub[['r','itlgrp']].rename(columns={'r':'*r'}).to_csv( + os.path.join(inputs_case, 'hierarchy_itlgrp.csv'), index=False) + + # save the itlgrp values + hier_sub[['itlgrp']].drop_duplicates().to_csv( + os.path.join(inputs_case, 'val_itlgrp.csv'), header=False, index=False) + + # Drop any substate region columns as these will no longer be needed + hier_sub = hier_sub.drop(['county', 'ba', 'itlgrp'], axis=1) + + # Populate val_st as unique states (not st_int) from the subsetted hierarchy table + # Also include "voluntary" state for modeling voluntary market REC trading + val_st = list(hier_sub['st'].unique()) + ['voluntary'] + + # Write out the unique values of each column in hier_sub to val_[column name].csv + # Note the conversion to a pd Series is necessary to leverage the to_csv function + if save_regions_and_agglevel: + for i in hier_sub.columns.drop('offshore', errors='ignore'): + pd.Series(hier_sub[i].unique()).to_csv( + os.path.join(inputs_case,'val_' + i + '.csv'),index=False,header=False) + + # Overwrite val_st with the val_st used here (which includes 'voluntary') + pd.Series(val_st).to_csv( + os.path.join(inputs_case, 'val_st.csv'), header=False, index=False) + + # Rename columns and save as hierarchy.csv + ( + hier_sub + .rename(columns={'r':'*r'}) + .drop(columns=['aggreg','offshore'], errors='ignore') + ).to_csv(os.path.join(inputs_case, 'hierarchy.csv'), index=False) + + # Write offshore zones + hier_sub.loc[hier_sub.offshore == 1, 'r'].to_csv( + os.path.join(inputs_case, 'offshore.csv'), index=False, header=False, + ) + + levels = [i for i in hier_sub if i != 'offshore'] + valid_regions = {level: list(hier_sub[level].unique()) for level in levels} + + val_r = sorted(valid_regions['r']) + + # Export region files + if save_regions_and_agglevel: + pd.Series(val_r).to_csv( + os.path.join(inputs_case, 'val_r.csv'), header=False, index=False) + + regions_and_agglevel = { + "valid_regions": valid_regions, + "val_r_all": val_r_all, + "val_st": val_st, + "r_county": r_county, + "ba_county": ba_county, + "levels": levels + } + + return regions_and_agglevel + + +def read_banned_tech_file(full_path, filepath, inputs_case, r_county): + """ + Parses the list of regionally (state/county-level) banned techs from the + provided YAML file and reformats it as a GAMS-compatible table. + Regions banning nuclear are exported as their own list, as nuclear bans + have their own switches and functionalities that are handled separately. + """ + if not (full_path.endswith('yaml') or full_path.endswith('yml')): + raise TypeError(f'filetype for {full_path} is not .yaml/.yml') + + with open(full_path) as f: + techs_banned = yaml.safe_load(f) + + hierarchy = pd.read_csv(os.path.join(inputs_case, 'hierarchy.csv')) + df = pd.DataFrame(data=0, columns=hierarchy['*r'], index=techs_banned.keys()) + + # Nuclear bans are handled specially in the model, + # so we create a separate dataframe for those regions. + nuclear_ban_regions = pd.DataFrame(data=[], columns=['*r']) + for tech, ban_lists in techs_banned.items(): + ban_regions = [] + + if not all([x in ['st', 'county'] for x in ban_lists.keys()]): + raise NotImplementedError( + "The regional scope of banned techs must be either 'st' or 'county'. " + f"Update the nested keys in {filepath}." + ) + + # Apply state-wide bans to all of the regions belonging to those states + if 'st' in ban_lists.keys(): + ban_states = ban_lists['st'] + state_ban_regions = list(hierarchy.loc[hierarchy.st.isin(ban_states)]['*r']) + ban_regions.extend(state_ban_regions) + + # Apply county-wide bans to regions where all of the counties have banned the tech + if 'county' in ban_lists.keys(): + ban_counties = ['p' + str(fips).zfill(5) for fips in ban_lists['county']] + r_ban_counties_map = ( + r_county.loc[r_county.county.isin(ban_counties)] + .groupby('r') + ['county'] + .apply(list) + .apply(sorted) + ) + r_all_counties_map = ( + r_county.groupby('r') + ['county'] + .apply(list) + .apply(sorted) + ) + county_ban_regions = list( + r_ban_counties_map + .loc[(r_ban_counties_map.isin(r_all_counties_map))] + .index + ) + ban_regions.extend(county_ban_regions) + + if tech == 'nuclear': + nuclear_ban_regions['*r'] = ban_regions + else: + df.loc[tech, ban_regions] = 1 + + df = df.reset_index(names=['i']) + + return df, nuclear_ban_regions + + +def read_special_h5file(full_path): + """ + Read .h5 file and make special-case adjustments if necessary: + - recf_distpv: drop 'distpv|' from column titles + - transmission_cost_ac: reset index and decode strings + - transmission_distance: stack from wide into long and decode strings + """ + filename = os.path.basename(full_path) + df = reeds.io.read_file(full_path, parse_timestamps=True) + + if filename.startswith('recf_distpv'): + df.columns = df.columns.str.replace('distpv|','') + elif filename.startswith('transmission_cost_ac'): + df = df.reset_index() + for col in ['r', 'rr', 'tscbin']: + df[col] = df[col].str.decode('utf-8') + elif filename.startswith('transmission_distance'): + df = df.stack().rename('miles').reset_index() + df['r'] = df['r'].str.decode('utf-8') + + return df + + +def subset_to_valid_regions( + sw, + region_file_entry, + agglevel_variables, + regions_and_agglevel, + inputs_case=None, + agg=True, +): + """ + Filter data for valid regions and return a dataframe + """ + levels = regions_and_agglevel["levels"] + val_r_all = regions_and_agglevel["val_r_all"] + val_st = regions_and_agglevel["val_st"] + valid_regions = regions_and_agglevel["valid_regions"] + + # Read file and return dataframe filtered for valid regions + filepath = region_file_entry['filepath'] + filename = region_file_entry['filename'] + full_path = region_file_entry['full_filepath'] + + # Get the filetype of the input file from the filepath string + filetype_in = os.path.splitext(filepath)[1].strip('.') + + region_col = region_file_entry['region_col'] + fix_cols = [i for i in region_file_entry['fix_cols'].split(',') if i != ''] + + sc_point_gid_index = False + if ( + filename.startswith('supplycurve') + or filename.startswith('exog_cap') + or filename.startswith('prescribed_builds') + ): + sc_point_gid_index = True + + # When running at mixed resolution we need to copy both ba and county resolution data + if (agglevel_variables['lvl'] == 'mult') and ('lvl' in filepath) and (not sc_point_gid_index): + full_path_ba = full_path.replace('{lvl}', 'ba') + full_path_county = full_path.replace('{lvl}', 'county') + match filetype_in: + case 'h5': + df_ba = read_special_h5file(full_path_ba) + df_county = read_special_h5file(full_path_county) + case 'csv': + df_ba = pd.read_csv( + full_path_ba, + dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, comment='#', + ) + df_county = pd.read_csv( + full_path_county, + dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, comment='#', + ) + case _: + raise TypeError(f'filetype for {full_path} is not .csv or .h5') + + # Single resolution procedure + else: + # Replace '{switchnames}' in full_path with corresponding switch values + full_path = full_path.format(**{**sw, **{'lvl':agglevel_variables['lvl']}}) + ## Filename conditions + if filename.startswith('supplycurve'): + df = reeds.io.assemble_supplycurve( + full_path, + case=os.path.dirname(os.path.normpath(inputs_case)), + agg=agg, + ).reset_index() + elif filename.startswith('exog_cap'): + df = reeds.io.assemble_exog_cap( + full_path, + case=os.path.dirname(os.path.normpath(inputs_case)), + ) + elif filename.startswith('prescribed_builds'): + df = reeds.io.assemble_prescribed_builds( + full_path, + case=os.path.dirname(os.path.normpath(inputs_case)), + ) + elif filename == 'techs_banned.csv': + df, nuclear_ban_regions = read_banned_tech_file( + full_path, + filepath, + inputs_case, + r_county=regions_and_agglevel['r_county'] + ) + nuclear_ban_regions.to_csv( + os.path.join(inputs_case,'nuclear_ba_ban_list.csv'), + index=False + ) + ## Filetype conditions + elif filetype_in == 'h5': + df = read_special_h5file(full_path) + elif filetype_in == 'csv': + df = pd.read_csv(full_path, dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, comment='#') + else: + raise ValueError(f'Unmatched filename ({filename}) or filetype ({filetype_in})') + + # ---- Filter data to valid regions ---- + # If running at mixed resolution we need to remove BA level data for regions that are being solved at county resolution + if (agglevel_variables['lvl'] == 'mult') and ('lvl' in filepath) and (not sc_point_gid_index): + hier = pd.read_csv(os.path.join(inputs_case,'hierarchy_with_res.csv')).rename(columns = {'*r':'r'}) + # Filter function parameters to only include BA resolution regions + valid_regions_ba = {level: list(hier[hier['r'] + .isin(agglevel_variables['ba_regions'])][level].unique()) for level in levels} + val_st_ba = valid_regions_ba['st'] + val_r_all_ba = [] + for value in valid_regions_ba.values(): + val_r_all_ba.extend(value) + val_r_all_ba = list(set(val_r_all_ba)) + # Add BA regions associated with states and aggregs being run at BA resolution + val_r_all_ba.extend(x for x in agglevel_variables['ba_regions']if x not in val_r_all_ba) + df_ba = filter_data( + df_ba, + region_col, + fix_cols,levels, + val_r_all=val_r_all_ba, + valid_regions=valid_regions_ba, + val_st=val_st_ba, + filename=filename + ) + + + # Transmission files need to be filtered differently to allow interfaces between BA and county resolution regions + transmission_files = [ + 'transmission_cost_ac.csv', + 'transmission_cost_dc.csv', + 'transmission_distance.csv', + ] + + if filename in transmission_files: + # Filter county data to include regions being solved at both BA and county resolution + df_county = filter_data( + df_county, + region_col, + fix_cols, + levels, + val_r_all, + valid_regions, + val_st, + filename=filename + ) + # Assign the counties that are not being solved at county resolution to the correct BA + tx_region_col = '*r' if '*r' in df_county.columns.values else 'r' + for idx, region in df_county[tx_region_col].items(): + if region not in agglevel_variables['county_regions']: + df_county.loc[idx, tx_region_col] = agglevel_variables['BA_2_county'][df_county.loc[idx,tx_region_col]] + + for idx,region in df_county['rr'].items(): + if region not in agglevel_variables['county_regions']: + df_county.loc[idx, 'rr'] = agglevel_variables['BA_2_county'][df_county.loc[idx, 'rr']] + # Drop if *r and rr are same region + df_county = df_county.drop(df_county[df_county[tx_region_col]==df_county['rr']].index) + # Drop if line is BA-to-BA + drop_list = [] + for idx,region in df_county.iterrows(): + if ( + (region[tx_region_col] in agglevel_variables['ba_regions']) + and (region['rr'] in agglevel_variables['ba_regions']) + ): + drop_list.append(idx) + + df_county = df_county.drop(drop_list) + # Group lines going between same BA and county + if 'distance' in filename: + df_county = df_county.groupby([tx_region_col,'rr'] + fix_cols).mean().reset_index() + elif 'cost_ac' in filename: + df_county = df_county.groupby([tx_region_col,'rr'] + fix_cols).sum().reset_index() + # Drop reverse interfaces + # Keep only the interface where the first region is alphabetically first + df_county['region_pair'] = df_county.apply( + lambda x: '||'.join(sorted([x[tx_region_col], x['rr']])), axis=1) + df_county = df_county.sort_values(by=['region_pair','tscbin']) + df_county = df_county.drop_duplicates(subset=['region_pair','tscbin'], keep='first') + df_county = df_county.drop(columns=['region_pair']) + else: + df_county = df_county.groupby([tx_region_col,'rr'] + fix_cols).sum().reset_index() + + df_county['interface'] = df_county[tx_region_col] + '||'+df_county['rr'] + df_county.reset_index(drop=True,inplace=True) + + else: + # Filter function parameters to only include county resolution regions + valid_regions_county = {level: (hier[hier['r'] + .isin(agglevel_variables['county_regions'])][level].unique()) for level in levels} + val_st_county = valid_regions_county['st'] + val_r_all_county = [] + for value in valid_regions_county.values(): + val_r_all_county.extend(value) + val_r_all_county = list(dict.fromkeys(val_r_all_county)) + + df_county = filter_data( + df_county, + region_col, + fix_cols, + levels, + val_r_all=val_r_all_county, + valid_regions=valid_regions_county, + val_st=val_st_county, + filename=filename + ) + + # Combine BA and county data + + # The filter data function returns a dataframe with NAN values to prevent empty H5 files + # If either the BA data or county data are populated we can drop the nan data + if df_county.isna().all().all() and not df_ba.isna().all().all(): + df = df_ba + elif not df_county.isna().all().all() and df_ba.isna().all().all(): + df = df_county + else: + # Combine BA and county data + if region_file_entry['wide'] == 1 : + df = pd.concat([df_ba,df_county],axis =1) + else: + df = pd.concat([df_ba,df_county]) + + # Single resolution procedure + # Or procedure for input data that exist at single resolution and + # are aggregated/disaggregated later + else: + df = filter_data( + df, + region_col, + fix_cols, + levels, + val_r_all, + valid_regions, + val_st, + filename=filename + ) + + return df + + +#%% =========================================================================== +### --- General Write Functions--- +### =========================================================================== +def write_empty_file(filepath): + filetype = os.path.splitext(filepath)[1].strip('.') + if filetype == 'h5': + with h5py.File(filepath, 'w'): + pass + else: + open(filepath, 'a').close() + + return + + +def scalar_csv_to_txt(path_to_scalar_csv): + """ + Write a scalar csv to GAMS-readable text + Format of csv should be: scalar,value,comment + """ + # Load the csv + dfscalar = pd.read_csv( + path_to_scalar_csv, + header=None, names=['scalar','value','comment'], index_col='scalar').fillna(' ') + # Create the GAMS-readable string (comments can only be 255 characters long) + scalartext = '\n'.join([ + 'scalar {:<30} "{:<5.255}" /{}/ ;'.format( + i, row['comment'], row['value']) + for i, row in dfscalar.iterrows() + ]) + # Write it to a file, replacing .csv with .txt in the filename + with open(path_to_scalar_csv.replace('.csv','.txt'), 'w') as w: + w.write(scalartext) + + return dfscalar + + +def param_csv_to_txt(path_to_param_csv, writelist=True): + """ + Write a parameter csv to GAMS-readable text + Format of csv should be: parameter(indices),units,comment + """ + # Load the csv + dfparams = pd.read_csv( + path_to_param_csv, + index_col='param', comment='#', + ) + # Create the GAMS-readable param definition string (comments must be ≤255 characters) + paramtext = '\n'.join([ + f'parameter {i:<50} "--{row.units}-- {row.comment:.255}" ;' + # Don't define parameters with an input flag because they already exist + for i, row in dfparams.loc[dfparams.input != 1].iterrows() + ]) + # Write it to a file, replacing .csv with .gms in the filename + param_gms_path = path_to_param_csv.replace('.csv','.gms') + with open(param_gms_path, 'w') as w: + w.write(paramtext) + # Write the list of parameters if desired + if writelist: + # Create the GAMS-readable list of parameters (without indices) + paramlist = '\n'.join(dfparams.index.map(lambda x: x.split('(')[0]).tolist()) + param_list_path = ( + path_to_param_csv.replace('params','paramlist').replace('.csv','.txt')) + with open(param_list_path, 'w') as w: + w.write(paramlist) + + return dfparams + +# Function to filter data to valid regions +def filter_data( + df, + region_col, + fix_cols, + levels, + val_r_all, + valid_regions, + val_st, + filename +): + if region_col == 'wide': + # Check to see if the regions are listed in the columns. If they are, + # then use those columns + + # Need check for case where there are no data for val_r_all (but not because of the column headr formatting) and empty dataframe needs to be returned + if (len([x for x in val_r_all if x in df.columns])==0) & ( not any('|' in col for col in df.columns)) & ( not any('_' in col for col in df.columns)): + df = df.loc[:,df.columns.isin(fix_cols + val_r_all)] + elif df.columns.isin(val_r_all).any(): + df = df.loc[:,df.columns.isin(fix_cols + val_r_all)] + else: + # Checks if regions are in columns as '[class]|[region]' or '[class]_[region]' (e.g. in 8760 RECF data). + # This 'try' attempts to split each column name using '|' and '_' as delimiters and checks the second + # value for any regions. + # If it can't do so, it will instead use a blank dataframe. + try: + if any('|' in col for col in df.columns): + delim = '|' + elif '_' in df.columns[0]: + delim = '_' + else: + raise ValueError(f"Cannot split columns in {filename} by '|' or '_' (example: {df.columns[0]}).") + column_mask = (df.columns.str.split(delim,expand=True) + .get_level_values(1) + .isin(val_r_all) + .tolist() + ) + df = df.loc[:, column_mask | df.columns.isin(fix_cols)] + # Empty h5 files cannot be read in, causing errors in recf.py. + # In the case that val_r_all filters out all columns, leaving an empty dataframe, + # fill a single column with NaN to preserve the file index for use in recf.py + if len(df.columns) == 0: + df = pd.DataFrame(np.nan, index = df.index,columns = val_r_all) + except Exception: + df = pd.DataFrame() + + # If there is a region-to-region mapping set + elif region_col.strip('*') in ['r,rr','transgrp,transgrpp']: + # make sure both the r and rr regions are in val_r + r,rr = region_col.split(',') + df = df.loc[df[r].isin(val_r_all) & df[rr].isin(val_r_all)] + + # Subset on the valid regions except for r regions + # (r regions might also include s regions, which complicates things...) + elif ((region_col.strip('*') in levels) & (region_col.strip('*') != 'r')): + df = df.loc[df[region_col].isin(valid_regions[region_col.strip('*')])] + + # Subset both column of 'st' and columns of state if st_st + elif (region_col.strip('*') == 'st_st'): + # make sure both the state regions are in val_st + df = df.loc[df['st'].isin(val_st)] + df = df.loc[:,df.columns.isin(fix_cols + val_st)] + + elif (region_col.strip('*') == 'r_cendiv'): + # Make sure both the r is in val_r_all and cendiv is in val_cendiv + val_cendiv = valid_regions['cendiv'] + df = df.loc[df['r'].isin(val_r_all)] + df = df.loc[:,df.columns.isin(["r"] + val_cendiv)] + + # Subset on val_{level} if region_col == 'wide_{level}' + elif (region_col.split('_')[0] == 'wide') and (region_col.split('_')[1] in valid_regions.keys()): + # Check to see if the region values are listed in the columns. If they are, + # then use those columns + val_reg = valid_regions[region_col.split('_')[1]] + if df.columns.isin(val_reg).any(): + df = df.loc[:,df.columns.isin(fix_cols + val_reg)] + # Otherwise just use an empty dataframe + else: + df = pd.DataFrame() + + # If region_col is not wide, st, or aliased.. + else: + df = df.loc[df[region_col].isin(val_r_all)] + + return df + + +def write_scalars(scalars, inputs_case): + """ + Write modified scalars.csv file + Special-case handling of scalars.csv: turn years_until into firstyear + """ + toadd = scalars.loc[scalars.index.str.startswith('years_until')].copy() + toadd.index = toadd.index.map(lambda x: x.replace('years_until','firstyear')) + toadd.value += scalars.loc['this_year','value'] + scalars_write = pd.concat([scalars, toadd], axis=0) + + # Trim trailing decimal zeros + scalars_write.value = scalars_write.value.astype(str).replace('\.0+$', '', regex=True) + scalars_write.to_csv(os.path.join(inputs_case, 'scalars.csv'), header=False) + + # Rewrite the scalar tables as GAMS-readable definition + scalar_csv_to_txt(os.path.join(inputs_case,'scalars.csv')) + + +def write_GAMS_sets(runfiles, reeds_path, inputs_case): + """ + Write GAMS-readable sets to the inputs_case directory + """ + casedir = os.path.dirname(inputs_case) + + # Create Sets folder + shutil.copytree( + os.path.join(reeds_path,'inputs','sets'), + os.path.join(inputs_case,'sets'), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns('README*','readme*'), + ) + + # Write commands to load sets + sets = runfiles.loc[runfiles.GAMStype=='set'].copy() + settext = '$offlisting\n' + '\n\n'.join([ + f"set {row.GAMSname} '{row.comment:.255}'" + '\n/' + f"\n$include inputs_case%ds%{row.filename}" + '\n/ ;' + for i, row in sets.iterrows() + ]) + '\n$onlisting\n' + # Write to file + with open(os.path.join(casedir,'b_sets.gms'), 'w') as f: + f.write(settext) + + +def write_non_region_file(filename, filepath, src_file, dir_dst, sw, regions_and_agglevel, source_deflator_map): + """ + Copy a non-region specific file (filename) from src_file to dir_dst + """ + # Check if source file exists and is not rev_paths.csv + if (os.path.exists(src_file)) and (filename!='rev_paths.csv'): + + # Special Case: Values in load_multiplier.csv need to be rounded prior to copy + if filename == 'load_multiplier.csv': + df_load_multiplier = pd.read_csv(src_file).round(6) + df_load_multiplier.to_csv(os.path.join(dir_dst,'load_multiplier.csv'),index=False) + + elif filename == 'h2_exogenous_demand.csv': + # h2_exogenous_demand.csv has a path in runfiles.csv (considered a non-region file) + df=pd.read_csv(src_file,index_col=['p','t']) + df[sw['GSw_H2_Demand_Case']].round(3).rename_axis(['*p','t']).to_csv( + os.path.join(dir_dst,'h2_exogenous_demand.csv') + ) + + elif filename in ['energy_communities.csv', 'nuclear_energy_communities.csv']: + # Map energy communities to regions and compute the percentage of energy communities + # within each region to assign a weighted bonus. + + # Rename column in energy_communities.csv and map county to r, save as energy_communities.csv + energy_communities = pd.read_csv(src_file) + energy_communities.rename(columns={'County Region': 'county'}, inplace=True) + + r_county = regions_and_agglevel ['r_county'] + # Map energy communities to regions + e_df = pd.merge(energy_communities, r_county, on='county', how='left').dropna() + + # Group energy community regions and count the number of counties in each + energy_county_counts = e_df.groupby('r')['county'].nunique() + + # Group all regions and count the number of counties in each + total_county_counts = r_county.groupby('r')['county'].nunique() + + # Calculate the percentage of counties that are energy communities in each region + e_df = (energy_county_counts / total_county_counts).round(3).reset_index().dropna() + + # Rename columns from ['r','county'] to ['r','percentage_energy_communities'] + e_df.columns = ['r', 'percentage_energy_communities'] + + e_df.to_csv(os.path.join(dir_dst,filename),index=False) + + elif filename == 'co2_site_char.csv': + # Adjust for inflation + df = pd.read_csv(src_file) + df[f"bec_{sw['GSw_CO2_BEC']}"] *= source_deflator_map[filepath] + df.to_csv(os.path.join(dir_dst, 'co2_site_char.csv'), index=False) + + else: + shutil.copy(src_file, os.path.join(dir_dst,filename)) + + if filename == 'e_report_params.csv': + # Rewrite e_report_params as GAMS-readable definition + param_csv_to_txt(os.path.join(dir_dst,'e_report_params.csv')) + + if filename == 'scalars.csv': + # Rewrite scalars.csv as GAMS-readable definition + scalars = reeds.io.get_scalars(full=True) + write_scalars(scalars, dir_dst) + + +def write_non_region_files(non_region_files, sw, inputs_case, regions_and_agglevel, source_deflator_map): + """ + Copy non-region specific files to the input case directory. + """ + print('Copying non-region-indexed files') + + for _, row in non_region_files.iterrows(): + if row['filepath'].split('/')[0] in ['inputs','postprocessing','tests']: + dir_dst = inputs_case + else: + dir_dst = os.path.dirname(inputs_case) + + # If the file is missing and not required, + # an empty file is written with the given filename. + if (not row['file_exists']) and (not row['file_is_required']): + print(f'...writing empty file {row.filename}') + write_empty_file(os.path.join(dir_dst,row['filename'])) + else: + print(f'...copying {row.filename}') + filename = row['filename'] + filepath = row['filepath'] + src_file = row['full_filepath'] + write_non_region_file(filename, filepath, src_file, dir_dst, sw, regions_and_agglevel, source_deflator_map) + + +def calculate_county_fractions(df, county2zone): + """ + Calculates the values associated with each county as a percentage + of the total values for the county's state, BA, and model region + (where "BA" means a zone from the set of default 134 zones and + "model region" means a zone from the set of zones specific to this run). + Note the calculation of the county-to-BA fractions will eventually + be deprecated once the 134-zone structure is removed from all spatial + inputs (see https://github.nrel.gov/ReEDS/ReEDS-2.0/issues/1889). + The provided dataframe must have columns 'FIPS' and 'value'. + """ + required_columns = ['FIPS', 'value'] + missing_columns = [col for col in required_columns if col not in df.columns] + if len(missing_columns) > 0: + raise KeyError(f"Provided dataframe is missing required columns {missing_columns}") + + df = ( + df.merge( + county2zone.drop(columns='FIPS') + .rename(columns={'county': 'FIPS'}) + ) + .rename(columns={'ba': 'PCA_REG'}) + ) + df['fracdata'] = ( + df.groupby('PCA_REG') + ['value'] + .transform(lambda x: x / x.sum()) + ) + for col in ['r', 'state']: + df[f'{col}_frac'] = ( + df.groupby(col) + ['value'] + .transform(lambda x: x / x.sum()) + ) + + df = ( + df.dropna(subset='r') + [['PCA_REG', 'FIPS', 'fracdata', 'r', 'state', 'r_frac', 'state_frac']] + ) + + return df + +def write_disagg_data_files(runfiles, inputs_case): + """ + Write files that will be used for disaggregation. + """ + # Get the county2zone file specific to this run (a mapping from counties + # to model regions) and the original county2zone file (including all + # counties) and combine them. The former is needed to calculate model + # region-to-county fractions, and the latter is needed to calculate + # state-to-county and BA-to-county fractions. + county_r_map = reeds.io.get_county2zone(os.path.dirname(inputs_case)) + ## TEMPORARY 20260402: Use the old 134-zone county2zone until the aggregation approach is updated + county2zone = ( + reeds.io.get_county2zone(GSw_ZoneSet='z134', as_map=False) + .rename(columns={'r':'ba'}) + ) + county2zone['county'] = 'p' + county2zone['FIPS'].astype(str).str.zfill(5) + county2zone['r'] = county2zone['FIPS'].map(county_r_map) + + filename_filepath_map = runfiles.set_index('filename')['full_filepath'] + for filename in [ + 'disagg_geosize.csv', + 'disagg_population.csv', + 'disagg_state_lpf.csv' + ]: + if filename == 'disagg_geosize.csv': + # Calculate county land areas from the county shapefile + dfcounty = reeds.io.get_countymap(exclude_water_areas=True) + df = ( + dfcounty.set_index('rb') + .rename_axis('FIPS') + .area + .rename('value') + .reset_index() + ) + else: + # Read the raw data file for the disagg variable (e.g., + # county population data for disagg_population.csv) + filepath = filename_filepath_map[filename] + df = pd.read_csv(filepath) + + # Calculate state/region/BA-to-county fractions for the + # disagg variable and write to inputs_case + df = calculate_county_fractions(df, county2zone) + df.to_csv(os.path.join(inputs_case, filename), index=False) + + return + +def map_and_aggregate( + df, + regions_and_agglevel, + region_file_entry, + region_col, + aggfunc=None +): + ''' + Maps counties to BAs and aggregates according to aggfunc if provided. + ''' + merged = ( + df.set_index(region_col) + .merge(regions_and_agglevel['ba_county'], left_index=True, right_on='county') + .drop('county', axis=1) + .rename(columns={'ba': region_col}) + ) + + if aggfunc: + fix_cols = region_file_entry['fix_cols'].split(',') + if all([fix_col in merged.columns for fix_col in fix_cols]): + groupby_cols = list(set(fix_cols + [region_col])) + df = merged.groupby(groupby_cols, as_index=False).agg(aggfunc) + else: + df = merged.groupby(region_col, as_index=False).agg(aggfunc) + + + return df + +def upscale_from_county_to_ba( + df, + region_file_entry, + agglevel_variables, + regions_and_agglevel, + aggfunc=None +): + """ + Changes the resolution of the provided region_col from county to BA + or mixed resolution and aggregates according to the provided aggfunc. + """ + original_cols = df.columns + region_col = region_file_entry['region_col'] + + # Exception for cendiv + if region_col.strip('*') == 'r_cendiv': + val_cendiv = regions_and_agglevel['valid_regions']['cendiv'] + df = df.loc[df['r'].isin(regions_and_agglevel['r_county']['county'])] + df = df.loc[:, df.columns.isin(["r"] + val_cendiv)].reset_index(drop=True) + region_col = 'r' + + # Aggregate values to ba resolution if not running county-level resolution + # and if county level is not a desired resolution in mixed resolution runs + if 'county' not in agglevel_variables['agglevel']: + df = map_and_aggregate(df,regions_and_agglevel,region_file_entry,region_col,aggfunc) + + + # Mixed resolution procedure + elif agglevel_variables['lvl'] == 'mult': + df_ba = df[df[region_col].isin(agglevel_variables['BA_county_list'])] + df_ba = map_and_aggregate(df_ba,regions_and_agglevel,region_file_entry,region_col,aggfunc) + + # Filter out county regions + df_county = df[df[region_col].isin(agglevel_variables['county_regions'])] + # Combine county and BA + df = pd.concat([df_ba, df_county]) + + else: + pass + + df = df[original_cols] + + return df + + +def write_region_indexed_file( + df, + dir_dst, + source_deflator_map, + sw, + region_file_entry, + regions_and_agglevel, + agglevel_variables +): + """ + Write a single region-indexed file to the dir_dst directory + """ + filename = region_file_entry['filename'] + # Get the filetype of the output file from the filename string + filetype_out = os.path.splitext(filename)[1].strip('.') + + #---- Write data to dir_dst (inputs_case) folder ---- + if filetype_out == 'h5': + reeds.io.write_profile_to_h5(df, filename, dir_dst) + else: + # Special cases: These files' values need to be adjusted to copy + filepath = region_file_entry['filepath'] + match filename: + case 'bio_supplycurve.csv': + # Adjust for inflation + df['price'] = df['price'].astype(float) * source_deflator_map[filepath] + case ( + 'can_exports.csv' + | 'can_imports.csv' + | 'demonstration_plants.csv' + | 'distpvcap.csv' + | 'h2_ba_share.csv' + | 'regional_cap_cost_diff.csv' + | 'cendivweights.csv' + | 'cap_existing_psh.csv' + ): + # The upscale_from_county_to_ba function correctly handles the different spatial resolution options + # This sections just needs to check if the run is at pure county resolution + # The above listed data need to be upscaled if the run includes anything coarser than county resolution + if agglevel_variables['lvl'] != 'county': + df = upscale_from_county_to_ba( + df=df, + region_file_entry=region_file_entry, + agglevel_variables=agglevel_variables, + regions_and_agglevel=regions_and_agglevel, + aggfunc=region_file_entry.aggfunc + ) + case 'unitdata.csv': + fips_ba_map = regions_and_agglevel['ba_county'].dropna().set_index('county')['ba'] + df['reeds_ba'] = df['FIPS'].map(fips_ba_map) + ## If using offshore zones, map offshore wind units from land to offshore zones + if int(sw.GSw_OffshoreZones): + df = reeds.spatial.assign_to_offshore_zones(df) + num_units_missing_bas = len(df.loc[df.reeds_ba.isna()]) + if num_units_missing_bas > 0: + raise ValueError( + f"{num_units_missing_bas} units were not mapped to any BAs." + ) + case _: + pass + + df.to_csv(os.path.join(dir_dst,filename), index=False) + + +def write_region_indexed_files( + inputs_case, + sw, + region_files, + regions_and_agglevel, + agglevel_variables, + source_deflator_map +): + """ + Filter and copy data for files with regions + """ + print('Copying region-indexed files: filtering for valid regions') + for _, region_file_entry in region_files.iterrows(): + # If the file is missing and not required, + # an empty file is written with the given filename. + if ( + (not region_file_entry['file_exists']) + and (not region_file_entry['file_is_required']) + ): + print(f'...writing empty file {region_file_entry.filename}') + write_empty_file(os.path.join(inputs_case,region_file_entry['filename'])) + else: + print(f'...copying {region_file_entry.filename}') + # Read file and return dataframe filtered for valid regions + df = subset_to_valid_regions( + sw, + region_file_entry, + agglevel_variables, + regions_and_agglevel, + inputs_case + ) + write_region_indexed_file( + df, + inputs_case, + source_deflator_map, + sw, + region_file_entry, + regions_and_agglevel, + agglevel_variables + ) + + +def write_miscellaneous_files( + sw, + regions_and_agglevel, + agglevel_variables, + inputs_case, + reeds_path +): + """ + Handle miscellaneous files. + Many of these files are not in the non_region_files and region_files set + (runfiles.csv - from function read_runfiles). + """ + # ---- Miscellaneous files not in non_region_files or region_files ---- + pd.DataFrame( + {'*pvb_type': [f'pvb{i}' for i in sw['GSw_PVB_Types'].split('_')], + 'ilr': [np.around(float(c) / 100, 2) for c in sw['GSw_PVB_ILR'].split('_') + ][0:len(sw['GSw_PVB_Types'].split('_'))]} + ).to_csv(os.path.join(inputs_case, 'pvb_ilr.csv'), index=False) + + pd.DataFrame( + {'*pvb_type': [f'pvb{i}' for i in sw['GSw_PVB_Types'].split('_')], + 'bir': [np.around(float(c) / 100, 2) for c in sw['GSw_PVB_BIR'].split('_') + ][0:len(sw['GSw_PVB_Types'].split('_'))]} + ).to_csv(os.path.join(inputs_case, 'pvb_bir.csv'), index=False) + + # Constant value if input is float, otherwise named profile + # Methane leakage rate: + try: + rate = float(sw['GSw_MethaneLeakageScen']) + pd.Series(index=range(2010,2051), data=rate, name='constant').rename_axis('*t').round(5).to_csv( + os.path.join(inputs_case,'methane_leakage_rate.csv')) + except ValueError: + pd.read_csv( + os.path.join(reeds_path,'inputs','emission_constraints','methane_leakage_rate.csv'), + index_col='t', + )[sw['GSw_MethaneLeakageScen']].rename_axis('*t').round(5).to_csv( + os.path.join(inputs_case,'methane_leakage_rate.csv')) + + # H2 leakage rate: + pd.read_csv( + os.path.join(reeds_path,'inputs','emission_constraints','h2_leakage_rate.csv'), + index_col='i', + )[sw['GSw_H2LeakageScen']].rename_axis('*i').round(5).to_csv( + os.path.join(inputs_case,'h2_leakage_rate.csv')) + + # Add this_year to years_until_endogenous to generate the tech-specific firstyear.csv file + scalars = reeds.io.get_scalars(full=True) + ( + pd.read_csv( + # years_until_endogenous created using function write_non_region_files + os.path.join(inputs_case, 'years_until_endogenous.csv'), + index_col=0, + ).squeeze(1) + + int(scalars.loc['this_year','value']) + ).rename_axis('*i').rename('t').to_csv(os.path.join(inputs_case, 'firstyear.csv')) + + + ### Single column from input table ### + + pd.read_csv( + os.path.join(reeds_path,'inputs','emission_constraints','ng_crf_penalty.csv'), index_col='t', + )[sw['GSw_NG_CRF_penalty']].rename_axis('*t').to_csv( + os.path.join(inputs_case,'ng_crf_penalty.csv') + ) + + gwp = pd.read_csv( + os.path.join(reeds_path,'inputs','emission_constraints','gwp.csv'), + index_col='e', + ) + if sw['GSw_GWP'] in gwp: + gwp_write = gwp[sw['GSw_GWP']].copy() + else: + gwp_ch4, gwp_n2o = [float(i.split('_')[1]) for i in sw['GSw_GWP'].split('/')] + gwp_write = pd.Series({'CO2':1, 'CH4':gwp_ch4, 'N2O':gwp_n2o}) + + gwp_write['H2'] = scalars.loc['h2_gwp','value'].copy() + + gwp_write.to_csv(os.path.join(inputs_case,'gwp.csv'), header=False) + + # Calculate CO2 cap based on GSw_Region chosen (national or sub-national regions) + # Read in national co2 cap + co2_cap = ( + pd.read_csv( + os.path.join(reeds_path, 'inputs', 'emission_constraints', 'co2_cap.csv'), + index_col='t', + ) + .loc[sw['GSw_AnnualCapScen']] + .rename_axis('*t') + .rename('tonne_per_year') + ) + + # Read in 2022 CO2 emission share by county calculated from 2022 eGrid emission data: + em_share = pd.read_csv( + os.path.join( + reeds_path,'inputs','emission_constraints','county_co2_share_egrid_2022.csv'), + index_col=0) + + # Filter the counties that are in chosen GSw_Region + val_county = pd.read_csv(os.path.join(inputs_case,'val_county.csv'),names=['r']) + + # Merge emission share by county with the counties in GSw_Region and calculate emission share of GSw_Region + region_em_share = val_county.merge(em_share, on='r', how='left').fillna(0) + region_em_share = region_em_share['share'].sum() + + # Apply the emission share to national cap to get the emission cap trajectory of GSw_Region + co2_cap *= region_em_share + co2_cap.round(0).to_csv(os.path.join(inputs_case, 'co2_cap.csv')) + + # CO2 tax + pd.read_csv( + os.path.join(reeds_path,'inputs','emission_constraints','co2_tax.csv'), index_col='t', + )[sw['GSw_CarbTaxOption']].rename_axis('*t').round(2).to_csv( + os.path.join(inputs_case,'co2_tax.csv') + ) + + solveyears = reeds.parse.parse_yearset(sw['yearset']) + if int(sw['startyear']) not in solveyears: + solveyears.append(int(sw['startyear'])) + solveyears = sorted(solveyears) + solveyears = [y for y in solveyears if (y >= int(sw['startyear'])) and (y <= int(sw['endyear']))] + pd.DataFrame(columns=solveyears).to_csv( + os.path.join(inputs_case,'modeledyears.csv'), index=False) + + pd.read_csv( + os.path.join(reeds_path,'inputs','national_generation','gen_mandate_trajectory.csv'), + index_col='GSw_GenMandateScen' + ).loc[sw['GSw_GenMandateScen']].rename_axis('*t').round(5).to_csv( + os.path.join(inputs_case,'gen_mandate_trajectory.csv') + ) + + pd.read_csv( + os.path.join(reeds_path,'inputs','national_generation','gen_mandate_tech_list.csv'), + index_col='*i', + )[sw['GSw_GenMandateList']].to_csv( + os.path.join(inputs_case,'gen_mandate_tech_list.csv') + ) + + pd.read_csv( + os.path.join(reeds_path,'inputs','climate','climate_heuristics_yearfrac.csv'), + index_col='*t', + )[sw['GSw_ClimateHeuristics']].round(3).to_csv( + os.path.join(inputs_case,'climate_heuristics_yearfrac.csv') + ) + + pd.read_csv( + os.path.join(reeds_path,'inputs','climate','climate_heuristics_finalyear.csv'), + index_col='*parameter', + )[sw['GSw_ClimateHeuristics']].round(3).to_csv( + os.path.join(inputs_case,'climate_heuristics_finalyear.csv') + ) + + pd.read_csv( + os.path.join(reeds_path,'inputs','upgrades','upgrade_costs_ccs_coal.csv'), + index_col='t', + )[sw['ccs_upgrade_cost_case']].round(3).rename_axis('*t').to_csv( + os.path.join(inputs_case,'upgrade_costs_ccs_coal.csv') + ) + + pd.read_csv( + os.path.join(reeds_path,'inputs','upgrades','upgrade_costs_ccs_gas.csv'), + index_col='t', + )[sw['ccs_upgrade_cost_case']].round(3).rename_axis('*t').to_csv( + os.path.join(inputs_case,'upgrade_costs_ccs_gas.csv') + ) + + ccseason_dates = pd.read_csv( + os.path.join(reeds_path, 'inputs', 'reserves', 'ccseason_dates.csv'), + index_col=['month', 'day'], + )[sw['GSw_PRM_CapCreditSeasons']].rename('ccseason') + ccseason_dates.to_csv(os.path.join(inputs_case, 'ccseason_dates.csv')) + ccseason_dates.drop_duplicates().to_csv( + os.path.join(inputs_case, 'ccseason.csv'), + index=False, header=False, + ) + + prm_profiles = pd.read_csv( + os.path.join(reeds_path,'inputs','reserves','prm_annual.csv'), + ).rename(columns={'*nercr':'nercr'}).set_index(['nercr','t']) + if sw['GSw_PRM_scenario'] in prm_profiles: + prm = prm_profiles[sw['GSw_PRM_scenario']] + else: + prm = pd.Series(index=prm_profiles.index, data=float(sw['GSw_PRM_scenario'])) + + ## Broadcast PRM from nercr to r and backfill missing years + hierarchy = reeds.io.get_hierarchy(reeds.io.standardize_case(inputs_case)) + prm_initial = ( + prm + .unstack('nercr') + .reindex(solveyears) + .bfill().ffill() + [hierarchy['nercr']] + ) + prm_initial.columns = hierarchy.index.rename('*r') + prm_initial = prm_initial.stack('*r').reorder_levels(['*r','t']).round(4).rename('fraction') + prm_initial.to_csv(os.path.join(inputs_case, 'prm_initial.csv')) + for t in solveyears: + stresspath = os.path.join(inputs_case, f'stress{t}i0') + os.makedirs(stresspath, exist_ok=True) + prm_initial.xs(t, 0, 't').to_csv(os.path.join(stresspath, 'prm.csv')) + + # Add capacity deployment limits based on interconnection queue data + cap_queue = pd.read_csv( + os.path.join(reeds_path,'inputs','capacity_exogenous','interconnection_queues.csv')) + # Filter the counties that are in chosen GSw_Region + cap_queue = cap_queue[cap_queue['r'].isin(val_county['r'])] + + # Single resolution procedure + if (agglevel_variables["lvl"] != 'county') and ('county' not in agglevel_variables['agglevel']): + cap_queue = cap_queue.rename(columns={'r':'county'}) + cap_queue = pd.merge(cap_queue, regions_and_agglevel["r_county"], on='county', how='left').dropna() + cap_queue = cap_queue.drop('county', axis=1) + + # Mixed resolution procedure + elif agglevel_variables['lvl'] == 'mult': + # Filter out BA regions and aggregate + cap_queue_ba = cap_queue[cap_queue['r'].isin(agglevel_variables['BA_county_list'])].copy() + if 'aggreg' in agglevel_variables['agglevel'] : + r_county_dict = regions_and_agglevel["r_county"].set_index('county')['r'].to_dict() + cap_queue_ba['r'] = cap_queue_ba['r'].map(r_county_dict) + + else: + cap_queue_ba['r'] = cap_queue_ba['r'].map(agglevel_variables['BA_2_county']) + + # Filter out county regions + cap_queue_county = cap_queue[cap_queue['r'].isin(agglevel_variables['county_regions'])] + + #combine BA and county + cap_queue = pd.concat([cap_queue_ba,cap_queue_county]) + + cap_queue = cap_queue.groupby(['tg','r'],as_index=False).sum() + cap_queue.to_csv(os.path.join(inputs_case,'cap_limit.csv'), index=False) + # ---- Miscelanous files in non_region_files or region_files (in this case we are overwriting them) + # Expand i (technologies) set if modeling water use. Overwrite originals. + if int(sw['GSw_WaterMain']): + pd.concat([ + pd.read_csv( + os.path.join(inputs_case,'i.csv'), + comment='*', header=None).squeeze(1), + pd.read_csv( + os.path.join(inputs_case,'i_coolingtech_watersource.csv'), + comment='*', header=None).squeeze(1), + pd.read_csv( + os.path.join(inputs_case,'i_coolingtech_watersource_upgrades.csv'), + comment='*', header=None).squeeze(1), + ]).to_csv(os.path.join(inputs_case,'i.csv'), header=False, index=False) + + ## Unit sizes for ReEDS2PRAS + fpath_out = os.path.join(inputs_case, 'unitsize.csv') + if sw['pras_unitsize_source'] == 'atb': + shutil.copy( + os.path.join(reeds_path, 'inputs', 'plant_characteristics', 'unitsize_atb.csv'), + fpath_out, + ) + elif sw['pras_unitsize_source'] == 'r2x': + fpath_in = os.path.join(reeds_path, 'inputs', 'plant_characteristics', 'pcm_defaults.json') + with open(fpath_in) as f: + pcm_defaults = json.load(f) + unitsize = pd.Series( + index=pcm_defaults.keys(), + data=[pcm_defaults[tech]['avg_capacity_MW'] for tech in pcm_defaults.keys()], + name='MW', + ).rename_axis('tech').dropna().astype(int) + unitsize.to_csv(fpath_out) + + +def generate_maps_gpkg(inputs_case): + """ + Write maps.gpkg to speed up map visualization in postprocessing. + If using region dis/aggregation, maps.gpkg is overwritten in aggregation_regions.py. + """ + mapsfile = os.path.join(inputs_case, 'maps.gpkg') + if os.path.exists(mapsfile): + os.remove(mapsfile) + + dfmap = reeds.io.get_dfmap(os.path.abspath(os.path.join(inputs_case,'..'))) + for level in dfmap: + dfmap[level].to_file(mapsfile, layer=level) + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== +def main(reeds_path, inputs_case): + """ + Run copy_files.py for use in the ReEDS workflow + + Parameters: + reeds_path : str (Path to the ReEDS directory) + inputs_case : str (Path to the run/inputs_case directory) + + Returns: + None (Writes files to the inputs_case directory) + """ + #%% =========================================================================== + ### --- Gather dataframes and dictionaries necessary for the script execution --- + ### =========================================================================== + # Obtain data necessary to filter and aggregate regions + regions_and_agglevel = get_regions_and_agglevel(reeds_path, inputs_case) + # Use agglevel_variables function to obtain spatial resolution variables + agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) + + #%% =========================================================================== + ### --- Copying files --- + ### =========================================================================== + + sw = reeds.io.get_switches(inputs_case) + + runfiles, non_region_files, region_files = read_runfiles( + reeds_path, + inputs_case, + sw, + agglevel_variables + ) + + # Write general GAMS files + # Write GAMS-readable sets to the inputs_case directory + write_GAMS_sets(runfiles, reeds_path, inputs_case) + + # Rewrite the switches tables as GAMS-readable definition + # (gswitches.csv is first written at run.py) + scalar_csv_to_txt(os.path.join(inputs_case,'gswitches.csv')) + + source_deflator_map = get_source_deflator_map(reeds_path) + + # Copy non-region files + write_non_region_files(non_region_files, sw, inputs_case, regions_and_agglevel, source_deflator_map) + + # Copy region files + write_region_indexed_files( + inputs_case, + sw, + region_files, + regions_and_agglevel, + agglevel_variables, + source_deflator_map + ) + + # Write files used for disaggregation + write_disagg_data_files(runfiles, inputs_case) + + # Create a maps.gpkg for this run + # Skip if using region dis/aggregation, maps will be written in aggregation_regions.py. + # Run if using mixed resolution aggreg-county combination + if agglevel_variables['lvl'] == 'ba' or ( + agglevel_variables['lvl'] == 'mult' and 'aggreg' in agglevel_variables['agglevel']): + generate_maps_gpkg(inputs_case) + + #%% =========================================================================== + ### --- Exceptions --- + ### =========================================================================== + # Handle miscellaneous files not included in non_region_files, region_files. + # Needs to run after copy of non-region files + write_miscellaneous_files( + sw, + regions_and_agglevel, + agglevel_variables, + inputs_case, + reeds_path + ) + + +#%% Procedure +if __name__ == '__main__' and not hasattr(sys, 'ps1'): + # ---- Parse arguments ---- + parser = argparse.ArgumentParser(description="Copy files needed for this run") + parser.add_argument('reeds_path', help='ReEDS directory') + parser.add_argument('inputs_case', help='output directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + # #%% Settings for testing ### + # reeds_path = reeds.io.reeds_path + # inputs_case = os.path.join(reeds_path,'runs','v20260305_itlM0_USA_defaults','inputs_case') + + + # ---- Set up logger ---- + tic = datetime.datetime.now() + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + print('Starting copy_files.py') + main(reeds_path, inputs_case) + + reeds.log.toc(tic=tic, year=0, process='inputs/copy_files.py', + path=os.path.join(inputs_case,'..')) + print('Finished copy_files.py') diff --git a/reeds/input_processing/forecast.py b/reeds/input_processing/forecast.py new file mode 100644 index 00000000..8ceb654d --- /dev/null +++ b/reeds/input_processing/forecast.py @@ -0,0 +1,526 @@ +""" +prbrown 20201109 16:22 + +Notes to user: +-------------- +* This script loops over files in runs/{}/inputs_case/ and projects them forward +based on the directions given in inputs/userinput/futurefiles.csv. +If new files have been added to inputs_case, you'll need to add rows with +processing directions to futurefiles.csv. +The column names should be self-explanatory; most likely there's also at least +one similarly-formatted file in inputs_case that you can copy the settings for. +""" + +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import pandas as pd +import numpy as np +import gdxpds +import os +import sys +import shutil +from glob import glob +from warnings import warn +import datetime +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +# Time the operation of this script +tic = datetime.datetime.now() + +#%%################# +### FIXED INPUTS ### +decimals = 6 + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +the_unnamer = {f'Unnamed: {i}' : '' for i in range(1000)} + +def interpolate_missing_years(df, forecast_fit, method='linear'): + """ + Interpolates data for only the years required to perform the given file's forecast_fit. + e.g. if forecast_fit == 'linear_5', then interpolate any missing years' data for the + 5 years leading up to the last model year. + + Inputs + ------ + df : pd.DataFrame with columns as integer years + method : interpolation method [default = 'linear'] + """ + # Extract the number of years prior to last data year required for interpolation from + # forecast_fit string + fitlength = int(forecast_fit.split('_')[-1]) + + ### Gather a list of all years greater than the interpolation startyear up to the last + ### data year, and also include the column of the closest year <= interpolation startyear + interp_startyr = df.columns.max()-fitlength + g_yrs = df.columns[df.columns > (interp_startyr)] + l_eq_yrs = df.columns[df.columns <= (interp_startyr)] + if not l_eq_yrs.empty: + c_yr = l_eq_yrs.max() + interp_yrs = [c_yr] + list(g_yrs) + keep_yrs = l_eq_yrs[:-1] + + dfinterp = ( + ### Use only years required for interpolation + df[interp_yrs] + ### Switch column names from integer years to timestamps + .rename(columns={c: pd.Timestamp(str(c)) for c in df.columns}) + ### Add empty columns at year-starts between existing data + ### (mean doesn't do anything) + .resample('YS', axis=1).mean() + ### Interpolate linearly to fill the new columns + ### (interpolate only works on rows, so pivot, interpolate, pivot again) + .T.interpolate(method).T + ) + ### Switch back to integer-year column names + dfadd = dfinterp.rename(columns={c: c.year for c in dfinterp.columns}) + ### Remove any column in df that is in dfadd + dfout = df[keep_yrs].copy() + ### Add interpolated columns to dfout + dfout = pd.concat([dfout,dfadd], axis=1) + + return dfout + + +def forecast( + dfi, lastdatayear, addyears, forecast_fit, + clip_min=None, clip_max=None): + """ + Project additional year columns and add to df based on directions in forecast_fit. + forecast_fit can be in ['constant','linear_X','yoy_X','cagr_X','log_X'], + where 'X' is the number of years to use for the fit. + 'linear' projects linearly, while 'yoy','cagr','log' (all the same) projects + a constant compound annual growth rate. + """ + dfo = dfi.copy() + if forecast_fit == 'constant': + ### Assign each future year to last data year + for addyear in addyears: + dfo[addyear] = dfo[lastdatayear].copy() + elif forecast_fit.startswith('linear'): + ### Get the number of years to fit from the futurefiles entry + fitlength = int(forecast_fit.split('_')[1]) + fityears = list(range(lastdatayear-fitlength, lastdatayear+1)) + ### Get linear fits for all rows in parallel + x = np.vstack([fityears, np.ones_like(fityears)]).T + y = dfo[fityears].values + coefs, _, _, _ = np.linalg.lstsq(x, y.T, rcond=None) + # Extract slope and intercept data from result of least squares fit + slope = dict(zip(dfo.index, coefs[0])) + intercept = dict(zip(dfo.index, coefs[1])) + ### Apply the row-specific fits + for addyear in addyears: + ### Clip if desired, otherwise just project the fit + if (clip_min is not None) or (clip_max is not None): + dfo[addyear] = ( + dfo.index.map(intercept) + dfo.index.map(slope) * addyear + ).values.clip(clip_min,clip_max) + else: + dfo[addyear] = dfo.index.map(intercept) + dfo.index.map(slope) * addyear + elif (forecast_fit.startswith('cagr') + or forecast_fit.startswith('yoy') + or forecast_fit.startswith('log')): + ### Get the number of years to fit from the futurefiles entry + fitlength = int(forecast_fit.split('_')[1]) + fityears = list(range(lastdatayear-fitlength, lastdatayear+1)) + ### Fit each row individually. By taking the exp() of the fit to log(y) + ### we get the compound annual growth rate (cagr) + 1. + cagr = {} + for row in dfo.index: + cagr[row] = np.exp(np.polyfit( + x=fityears, y=np.log(dfo.loc[row, fityears].values), deg=1)[0]) + ### Apply the row-specific fits + for addyear in addyears: + ### Clip if desired, otherwise just project the fit + if (clip_min is not None) or (clip_max is not None): + dfo[addyear] = ( + dfo[lastdatayear] * (dfo.index.map(cagr) ** (addyear - lastdatayear)) + ).values.clip(clip_min,clip_max) + else: + dfo[addyear] = ( + dfo[lastdatayear] * (dfo.index.map(cagr) ** (addyear - lastdatayear))) + else: + raise Exception( + 'forecast_fit == {} is not implemented; try constant, linear, or cagr'.format( + forecast_fit)) + + return dfo + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== +if __name__ == '__main__': + + ### Parse arguments + parser = argparse.ArgumentParser(description='Extend inputs to arbitrary future year') + parser.add_argument('reeds_path', help='path to ReEDS directory') + parser.add_argument('inputs_case', help='path to inputs_case directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = os.path.join(args.inputs_case, '') + + # #%% Settings for testing + # reeds_path = os.path.expanduser('~/github/ReEDS-2.0') + # inputs_case = os.path.join(reeds_path,'runs','v20220411_prmM0_USA2060','inputs_case') + + #%% Settings for debugging + ### Set debug == True to write to a new folder (inputs_case/future/), leaving original files + ### intact. If debug == False (default), the original files are overwritten. + debug = False + ### missing: 'raise' or 'warn' + missing = 'raise' + ### verbose: 0, 1, 2 + verbose = 2 + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + endyear = int(sw.endyear) + distpvscen = sw.distpvscen + + #%%#################################### + ### If endyear <= 2050, exit the script + if endyear <= 2050: + print('endyear = {} <= 2050, so skip forecast.py'.format(endyear)) + quit() + else: + print('Starting forecast.py', flush=True) + + #%%################### + ### Derived inputs ### + + ### Get the case name (ReEDS-2.0/runs/{casename}/inputscase/) + casename = inputs_case.split(os.sep)[-3] + + ### DEBUG: Make the outputs directory + if debug: + outpath = os.path.join(inputs_case,'future','') + os.makedirs(outpath, exist_ok=True) + else: + outpath = inputs_case + ### Get the modeled years + tmodel_new = pd.read_csv( + os.path.join(inputs_case,'modeledyears.csv')).columns.astype(int).values + + ### Get the settings file + futurefiles = pd.read_csv( + os.path.join(inputs_case,'futurefiles.csv'), + dtype={ + 'header':'category', 'ignore':int, 'wide':int, + 'year_col':str, 'fix_cols':str, 'header':str, 'clip_min':str, 'clip_max':str, + } + ) + ### Fill in the missing parts of filenames + futurefiles.filename = futurefiles.filename.map( + lambda x: x.format(casename=casename, distpvscen=distpvscen) + ) + ### Fix issue where columns with "None" entries are read in as NaN + for col in ['key','fix_cols','header','clip_min','clip_max']: + futurefiles[col] = futurefiles[col].fillna('None') + ### If any files are missing, stop and alert the user + inputfiles = [os.path.basename(f) for f in glob(os.path.join(inputs_case,'*'))] + missingfiles = [ + f for f in inputfiles if ((f not in futurefiles.filename.values) and ('.' in f))] + if any(missingfiles): + if missing == 'raise': + raise Exception( + 'Missing future projection method for:\n{}\n' + '>>> Need to add entries for these files to futurefiles.csv' + .format('\n'.join(missingfiles)) + ) + else: + from warnings import warn + warn( + 'Missing future directions for:\n{}\n' + '>>> For this run, these files are copied without modification' + .format('\n'.join(missingfiles)) + ) + for filename in missingfiles: + shutil.copy(os.path.join(inputs_case, filename), os.path.join(outpath, filename)) + print('copied {}, which is missing from futurefiles.csv'.format(filename), + flush=True) + + #%% Loop it + for i in futurefiles.index: + filename = futurefiles.loc[i,'filename'] + print(f'{filename}', end='') + + ### If the file isn't in inputs_case, skip it and continue to the next file + if filename not in inputfiles: + if verbose > 1: + print(' -> skipped since not in inputs_case') + continue + + ### if ignore == 0, continue with forecasting procedures + if futurefiles.loc[i,'ignore'] == 0: + pass + ### if ignore == 1, just copy the file to outpath and skip the rest + elif futurefiles.loc[i,'ignore'] == 1: + if debug: + shutil.copy(os.path.join(inputs_case,filename), os.path.join(outpath,filename)) + if verbose > 1: + print(' -> ignored: "ignore" == 1 in futurefiles.csv', flush=True) + continue + ### if ignore == 2, need to project for EFS or copy otherwise + elif futurefiles.loc[i,'ignore'] == 2: + ### Read the file to determine if it's formatted for default or EFS load + dftest = pd.read_csv(os.path.join(inputs_case,filename), header=0, nrows=20) + ### If it has more than 10 columns (indicating EFS), follow the directions + if dftest.shape[1] > 10: + pass + ### Otherwise (indicating default), just copy it + else: + if debug: + shutil.copy(os.path.join(inputs_case,filename), os.path.join(outpath,filename)) + if verbose > 1: + print(' -> EFS special case: {}'.format(filename), flush=True) + continue + + + + #%% Settings + ### header: 0 if file has column labels, otherwise 'None' + header = (None if futurefiles.loc[i,'header'] == 'None' + else 'keepindex' if futurefiles.loc[i,'header'] == 'keepindex' + else int(futurefiles.loc[i,'header'])) + ### year_col: usually 't' or 'year', or 'wide' if file uses years as columns + year_col = futurefiles.loc[i, 'year_col'] + ### forecast_fit: '{method}_{fityears}' or 'constant', with method in + ### ['linear','cagr'] and fityears indicating the number of historical years + ### (counting backwards from the last data year) to use for the projection. + ### If set to 'constant', will use the value from the last data year for + ### all future years. + forecast_fit = futurefiles.loc[i,'forecast_fit'] + ### fix_cols: indicate columns to use as for fields that should be projected + ### independently to future years (e.g. r, szn, tech) + fix_cols = futurefiles.loc[i,'fix_cols'] + fix_cols = (list() if fix_cols == 'None' else fix_cols.split(',')) + ### wide: 1 if any parameters are in wide format, otherwise 0 + wide = futurefiles.loc[i, 'wide'] + ### clip_min, clip_max: Indicate min and max values for projected data. + ### In general, costs should have clip_min = 0 (so they don't go negative) + clip_min, clip_max = futurefiles.loc[i,['clip_min','clip_max']] + clip_min = (None if clip_min.lower()=='none' else int(clip_min)) + clip_max = (None if clip_max.lower()=='none' else int(clip_max)) + filetype = futurefiles.loc[i, 'filetype'] + ### key: only used for gdx files, indicating the parameter name. + ### gdx files need a separate line in futurefiles.csv for each parameter. + key = futurefiles.loc[i,'key'] + efs = False + + ### Load it + if filetype in ['.csv','.csv.gz']: + dfin = pd.read_csv(os.path.join(inputs_case,filename), header=header,) + elif filetype == '.h5': + ### Currently load.h5 and dr_shed_hourly.h5 are the only h5 files we need to + ### project forward, so the procedure is currently specific to these files + dfin = reeds.io.read_file( + os.path.join(inputs_case,filename), + parse_timestamps=True, + ) + # dfin = pd.read_hdf(os.path.join(inputs_case,filename)) + if header == 'keepindex': + indexnames = list(dfin.index.names) + dfin = dfin.reset_index() + # Make a copy of dfin to prevent "PerformanceWarning: DataFrame is highly fragmented." error + dfin = dfin.copy() + ### We only need to do the projection for load.h5 if we're using EFS load, + ### which has a (year,hour) multiindex (which we reset above to columns). + ### If dfin doesn't have 'year' and 'datetime' columns, we can skip this file. + if (('year' in dfin.columns) and ('datetime' in dfin.columns)): + efs = True + else: + if debug: + shutil.copy(os.path.join(inputs_case,filename), os.path.join(outpath,filename)) + if verbose > 1: + print(f' -> ignored: {filename}', flush=True) + continue + elif filetype == '.txt': + dfin = pd.read_csv(os.path.join(inputs_case, filename), header=header, sep=' ') + ### Remove parentheses and commas + for col in [0,1]: + dfin[col] = dfin[col].map( + lambda x: x.replace('(','').replace(')','').replace(',','')) + ### Split the index column on periods + num_indices = len(dfin.loc[0,0].split('.')) + indexcols = ['i{}'.format(index) for index in range(num_indices)] + for index in range(num_indices): + dfin['i{}'.format(index)] = dfin[0].map(lambda x: x.split('.')[index]) + ### Make the data column numeric + dfin[1] = dfin[1].astype(float) + ### Reorder and rename the columns + dfin = dfin[indexcols + [1]].copy() + dfin.columns = list(range(num_indices+1)) + elif filetype == '.gdx': + ### Read in the full gdx file, but only change the 'key' parameter + ### given in futurefiles. That's wasteful, but there are currently no + ### active gdx files. + dfall = gdxpds.to_dataframes(os.path.join(inputs_case,filename)) + dfin = dfall[key] + else: + raise Exception('Unsupported filetype: {}'.format(filename)) + + dfin.rename(columns={c:str(c) for c in dfin.columns}, inplace=True) + columns = dfin.columns.tolist() + + if dfin.empty: + if verbose > 1: + print(' -> Empty dataframe: Skipping...') + continue + if (('year' in dfin.columns) and ('datetime' in dfin.columns)): + dfcheck = dfin.set_index(['datetime','year']) + if dfcheck.empty: + if verbose > 1: + print(' -> Empty dataframe: Skipping...') + continue + + #%% Reshape to wide format with year as column + if (len(fix_cols) == 0) and (wide == 0): + ### File is simply (year,data) + ### So just set year as index and transpose + df = dfin.set_index(year_col).T + elif (len(fix_cols) > 0) and (year_col == 'wide'): + ### File has some fixed columns and then years in wide format + ### Easy - just set the fix_cols as indices and keep years as columns + df = dfin.set_index(fix_cols) + elif (wide) and (year_col != 'wide') and (len(fix_cols) == 0): + ### Some value other than year is in wide format + ### So set years as index and transpose + df = dfin.set_index(year_col).T + elif (wide) and (year_col != 'wide') and (len(fix_cols) > 0): + ### Some value other than year is in wide format + ### So set years (and other fix_cols) as index and transpose + df = (dfin.melt(id_vars=[year_col]+fix_cols, ignore_index=False) + .set_index([year_col]+fix_cols+['variable']) + .unstack(year_col)) + ### Get the value name (in this case for the non-year wide column), then drop it + valuename = df.columns.get_level_values(0).unique().tolist() + if len(valuename) > 1: + raise Exception('Too many data columns: {}'.format(valuename)) + valuename = valuename[0] + df = df[valuename].copy() + elif (len(fix_cols) > 0) and (year_col != 'wide') and (not wide): + ### Tidy format - fix the fix_cols and unstack the year_col + ### same as `df = dfin.pivot(index=fix_cols, columns=year_col)`, but + ### pivot modifies fix_cols for some reason + df = dfin.set_index(fix_cols+[year_col]).unstack(year_col)#.droplevel(0, axis=1) + ### Get the value name, then drop it + valuename = df.columns.get_level_values(0).unique().tolist() + if len(valuename) > 1: + raise Exception('Too many data columns: {}'.format(valuename)) + valuename = valuename[0] + df = df[valuename].copy() + else: + raise Exception('Unknown data type for {}'.format(filename)) + + #%% All columns should now be years + df.rename(columns={c: int(c) for c in df.columns}, inplace=True) + lastdatayear = max([int(c) for c in df.columns]) + ### Create list of non-interpolated years (for filtering out interpolated year data later) + years_orig = df.columns.tolist() + ### Interpolate only required years for linear forecasting. Skip if forecast_fit + ### is 'constant' + if 'linear' in forecast_fit: + df = interpolate_missing_years(df, forecast_fit) + ### Get indices for projection + addyears = list(range(lastdatayear+1, endyear+1)) + ### If file is for EFS hourlyload, only project for years in tmodel_new to save time + if efs: + addyears = [y for y in addyears if y in tmodel_new] + + #%% Do the projection + df = forecast( + dfi=df, lastdatayear=lastdatayear, addyears=addyears, + forecast_fit=forecast_fit, clip_min=clip_min, clip_max=clip_max) + ### Remove years used for interpolation - keep only original and forecasted years + df = df[years_orig + addyears] + + #%% Put back in original format + if (len(fix_cols) == 0) and (wide == 0): + dfout = df.T.reset_index() + elif (len(fix_cols) > 0) and (year_col == 'wide'): + dfout = df.reset_index() + elif (wide) and (year_col != 'wide') and (len(fix_cols) == 0): + dfout = df.T.reset_index() + elif (wide) and (year_col != 'wide') and (len(fix_cols) > 0): + dfout = df.stack().unstack('variable').reset_index()[columns] + elif (len(fix_cols) > 0) and (year_col != 'wide') and (not wide): + dfout = df.stack().rename(valuename).dropna().reset_index()[columns] + + ### Unname any unnamed columns + dfout.rename(columns=the_unnamer, inplace=True) + + #%% Write it + if filetype in ['.csv','.csv.gz']: + dfout.round(decimals).to_csv( + os.path.join(outpath, filename), + header=(False if header is None else True), + index=False, + ) + elif filetype == '.h5': + if header == 'keepindex': + dfwrite = dfout.sort_values(indexnames).set_index(indexnames) + dfwrite.columns.name = None + else: + dfwrite = dfout + ### Special Case: ensure year col for dr_shed_hourly.h5 is same dtype as data + ### to prevent errors in write_profile_to_h5 + if filename == 'dr_shed_hourly.h5': + dfwrite[year_col] = dfwrite[year_col].astype(np.float32) + reeds.io.write_profile_to_h5(dfwrite.round(decimals),filename,outpath) + elif filetype == '.txt': + dfwrite = dfout.sort_index(axis=1) + ### Make the GAMS-readable index + dfwrite.index = dfwrite.apply( + lambda row: '(' + '.'.join([str(row[str(c)]) for c in range(num_indices)]) + ')', + axis=1 + ) + ### Downselect to data column + dfwrite = dfwrite[str(num_indices)].round(decimals) + ### Add commas to data column, remove from last entry + dfwrite = dfwrite.astype(str) + ',' + dfwrite.iloc[-1] = dfwrite.iloc[-1].replace(',','') + ### Write it + dfwrite.to_csv( + os.path.join(outpath, filename), + header=(False if header is None else True), + index=True, sep=' ', + ) + elif filetype == '.gdx': + ### Overwrite the projected parameter + dfall[key] = dfout.round(decimals) + ### Write the whole file + gdxpds.to_gdx(dfall, outpath+filename) + + if verbose > 1: + print( + f' -> Projected from {lastdatayear} to {endyear}', + flush=True) + + reeds.log.toc(tic=tic, year=0, process='inputs/forecast.py', + path=os.path.join(inputs_case,'..')) + + print('Finished forecast.py') + +## ############################## +## ### Initial one-time setup ### +## infiles = pd.DataFrame( +## {'filename': [os.path.basename(f) for f in glob(os.path.join(inputs_case,'*'))]}) +## infiles['filetype'] = infiles.filename.map(lambda x: os.path.splitext(x)[1]) +## infiles.sort_values(['filetype','filename'], inplace=True) +## infiles.to_csv( +## os.path.join(reeds_path, 'inputs', 'userinput', 'futurefiles.csv'), +## index=False +## ) diff --git a/reeds/input_processing/fuelcostprep.py b/reeds/input_processing/fuelcostprep.py new file mode 100644 index 00000000..8a7a0025 --- /dev/null +++ b/reeds/input_processing/fuelcostprep.py @@ -0,0 +1,176 @@ +''' +The purpose of this script is to write out fuel costs for the following fuels at +census division level: + - coal + - uranium + - H2 (for H2-CT/CC tech) + - natural gas +Additionally, this script also writes out natural gas demand (total NG demand as +well as NG demand for electricity generation) and natural gas alphas +''' +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import pandas as pd +import os +import sys +import argparse +import datetime +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +# Time the operation of this script +tic = datetime.datetime.now() + +#%% Parse arguments +parser = argparse.ArgumentParser(description="""This file organizes fuel cost data by techonology""") + +parser.add_argument("reeds_path", help='ReEDS-2.0 directory') +parser.add_argument("inputs_case", help='ReEDS-2.0/runs/{case}/inputs_case directory') + +args = parser.parse_args() +reeds_path = args.reeds_path +inputs_case = args.inputs_case + +# #%% Settings for testing +# reeds_path = 'd:\\Danny_ReEDS\\ReEDS-2.0' +# reeds_path = os.getcwd() +# inputs_case = os.path.join('runs','nd5_ND','inputs_case') + +#%% Set up logger +log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), +) +print("Starting fuelcostprep.py") + +#%% Inputs from switches +sw = reeds.io.get_switches(inputs_case) + +# Load valid regions +val_r = pd.read_csv( + os.path.join(inputs_case, 'val_r.csv'), header=None).squeeze(1).tolist() +val_cendiv = pd.read_csv( + os.path.join(inputs_case, 'val_cendiv.csv'), header=None).squeeze(1).tolist() + +r_cendiv = pd.read_csv(os.path.join(inputs_case,"r_cendiv.csv")) + +dollaryear = pd.read_csv(os.path.join(inputs_case, "dollaryear_fuel.csv")) +deflator = pd.read_csv(os.path.join(inputs_case,'deflator.csv')) +deflator.columns = ["Dollar.Year","Deflator"] +dollaryear = dollaryear.merge(deflator,on="Dollar.Year",how="left") + +#%% =========================================================================== +### --- PROCEDURE: FUEL PRICE CALCULATIONS --- +### =========================================================================== + +#################### +# -- Coal -- # +#################### +coal = pd.read_csv(os.path.join(inputs_case, 'coal_price.csv')) +coal = coal.melt(id_vars = ['year']).rename(columns={'variable':'cendiv'}) +coal = coal.loc[coal['cendiv'].isin(val_cendiv)] + +# Adjust prices to 2004$ +deflate = dollaryear.loc[dollaryear['Scenario'] == sw.coalscen,'Deflator'].values[0] +coal.loc[:,'value'] = coal['value'] * deflate + +coal = coal.merge(r_cendiv,on='cendiv',how='left') +coal = coal.drop('cendiv', axis=1) +coal = coal[['year','r','value']].rename(columns={'year':'t','value':'coal'}) +coal.coal = coal.coal.round(6) + +####################### +# -- Uranium -- # +####################### +uranium = pd.read_csv(os.path.join(inputs_case, 'uranium_price.csv')) + +# Adjust prices to 2004$ +deflate = dollaryear.loc[dollaryear['Scenario'] == sw.uraniumscen,'Deflator'].values[0] +uranium.loc[:,'cost'] = uranium['cost'] * deflate +uranium = pd.concat([uranium.assign(r=i) for i in val_r], ignore_index=True) +uranium = uranium[['year','r','cost']].rename(columns={'year':'t','cost':'uranium'}) +uranium.uranium = uranium.uranium.round(6) + +############################# +# -- H2-Combustion -- # +############################# +# note that these fuel inputs are not used when H2 production is run endogenously in ReEDS (GSw_H2 > 0) +h2fuel = pd.read_csv(os.path.join(inputs_case, 'hydrogen_price.csv'), index_col='year') + +#Adjust prices to 2004$ +deflate = dollaryear.loc[dollaryear['Scenario'] == sw.h2combustionfuelscen,'Deflator'].squeeze() +h2fuel['cost'] = h2fuel['cost'] * deflate +# Reshape from [:,[t,cost]] to [:,[t,r,cost]] +h2fuel = ( + pd.concat({r:h2fuel for r in val_r}, axis=0, names=['r']) + .reset_index().rename(columns={'year':'t','cost':'h2fuel'}) + [['t','r','h2fuel']] + .round(6) +) + +########################### +# -- Natural Gas -- # +########################### + +ngprice = pd.read_csv(os.path.join(inputs_case,'natgas_price_cendiv.csv')) +ngprice = ngprice.melt(id_vars=['year']).rename(columns={'variable':'cendiv'}) +ngprice = ngprice.loc[ngprice['cendiv'].isin(val_cendiv)] + +# Adjust prices to 2004$ +deflate = dollaryear.loc[dollaryear['Scenario'] == sw.ngscen,'Deflator'].values[0] +ngprice.loc[:,'value'] = ngprice['value'] * deflate + +# Save Natural Gas prices by census region +ngprice_cendiv = ngprice.copy() +ngprice_cendiv = ngprice_cendiv.pivot_table(index='cendiv',columns='year',values='value') +ngprice_cendiv = ngprice_cendiv.round(6) + +# Map census regions to model regions +ngprice = ngprice.merge(r_cendiv,on='cendiv',how='left') +ngprice = ngprice.drop('cendiv', axis=1) +ngprice = ngprice[['year','r','value']].rename(columns={'year':'t','value':'naturalgas'}) +ngprice.naturalgas = ngprice.naturalgas.round(6) + +# Combine all fuel data +fuel = coal.merge(uranium,on=['t','r'],how='left') +fuel = fuel.merge(ngprice,on=['t','r'],how='left') +fuel = fuel.merge(h2fuel,on=['t','r'],how='left') +fuel = fuel.sort_values(['t','r']) + +#%%#################################### +### Natural Gas Demand Calculations ### + +# Natural Gas demand +ngdemand = pd.read_csv(os.path.join(inputs_case,'ng_demand_elec.csv'), index_col='year') +ngdemand = ngdemand[ngdemand.columns[ngdemand.columns.isin(val_cendiv)]] +ngdemand = ngdemand.transpose() +ngdemand = ngdemand.round(6) + +# Total Natural Gas demand +ngtotdemand = pd.read_csv(os.path.join(inputs_case, 'ng_demand_tot.csv'), index_col='year') +ngtotdemand = ngtotdemand[ngtotdemand.columns[ngtotdemand.columns.isin(val_cendiv)]] +ngtotdemand = ngtotdemand.transpose() +ngtotdemand = ngtotdemand.round(6) + +### Natural Gas Alphas (already in 2004$) +alpha = pd.read_csv(os.path.join(inputs_case, 'alpha.csv'), index_col='t') +alpha = alpha[alpha.columns[alpha.columns.isin(val_cendiv)]] +alpha = alpha.round(6) + +#%%################### +### Data Write-Out ### +###################### + +fuel.to_csv(os.path.join(inputs_case,'fprice.csv'),index=False) +ngprice_cendiv.to_csv(os.path.join(inputs_case,'gasprice_ref.csv')) + +ngdemand.to_csv(os.path.join(inputs_case,'ng_demand_elec.csv')) +ngtotdemand.to_csv(os.path.join(inputs_case,'ng_demand_tot.csv')) +alpha.to_csv(os.path.join(inputs_case,'alpha.csv')) + +reeds.log.toc(tic=tic, year=0, process='inputs/fuelcostprep.py', + path=os.path.join(inputs_case,'..')) + +print('Finished fuelcostprep.py') diff --git a/reeds/input_processing/h2_storage.py b/reeds/input_processing/h2_storage.py new file mode 100644 index 00000000..83d5c419 --- /dev/null +++ b/reeds/input_processing/h2_storage.py @@ -0,0 +1,142 @@ +''' +This script calculates the H2 storage type for each model region. +Specifically, the script identifies the storage sites that exist in each +zone and associates the zone with its cheapest available storage site type. +''' + +import argparse +import pandas as pd +import os +import sys +import datetime +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== +def main(reeds_path, inputs_case): + print('Starting h2_storage.py') + + # Get model regions + dfzones = reeds.io.get_dfmap( + os.path.dirname(inputs_case), + levels=['r'], + exclude_water_areas=True + )['r'] + dfzones['geometry'] = dfzones['geometry'].buffer(0.) + dfzones['km2'] = dfzones.geometry.area / 1e6 + + for h2_storage_type in ['hardrock', 'salt']: + # Get storage sites of the given type and combine them into one region + h2_storage_sites = reeds.io.get_h2_storage_sites( + h2_storage_type=h2_storage_type + ) + h2_storage_region = ( + h2_storage_sites.dissolve() + .loc[0,'geometry'] + .buffer(0.) + ) + + # Calculate the areas of intersection between model regions and the + # collection of storage sites as percentages of total model region area + dfzones[h2_storage_type] = dfzones.intersection(h2_storage_region) + dfzones[h2_storage_type+'_km2'] = ( + dfzones[h2_storage_type].area / 1e6 + ) + dfzones[h2_storage_type+'_frac'] = ( + dfzones[h2_storage_type+'_km2'] / dfzones['km2'] + ) + + # Determine the H2 storage types available in each model region + # and reformat dataframe + scalars = reeds.io.get_scalars() + dfout = ( + pd.concat( + { + col: pd.Series( + dfzones.loc[( + dfzones[col+'_frac'] + > scalars["h2_storage_area_threshold"] + )] + .index + .values + ) + for col in ['hardrock','salt'] + } + ) + .reset_index(level=1, drop=True) + .rename('rb') + .reset_index() + .rename(columns={'index':'*h2stortype'}) + .assign(exists=1) + .pivot(index='rb',columns='*h2stortype',values='exists') + .reindex(dfzones.index.rename('rb')) + .fillna(0) + .astype(int) + ) + + # Downselect to one row per model region, selecting the cheapeast storage + # type available in the region, assuming salt is cheaper than hardrock. + outname = { + 'hardrock':'h2_storage_hardrock', + 'salt':'h2_storage_saltcavern', + 'underground':'h2_storage_undergroundpipe', + } + dfout['keep'] = ( + dfout.apply( + lambda row: ( + 'salt' if row.get('salt', False) + else 'hardrock' if row.get('hardrock', False) + else 'underground' + ), + axis=1 + ) + .replace(outname) + ) + dfout = ( + dfout.reset_index() + .rename(columns={'keep':'*h2_stor'}) + [['*h2_stor','rb']] + ) + + dfout.to_csv( + os.path.join(inputs_case, 'h2_storage_rb.csv'), + index=False + ) + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if __name__ == '__main__': + # Time the operation of this script + tic = datetime.datetime.now() + + ### Parse arguments + parser = argparse.ArgumentParser( + description='Process H2 storage inputs', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('reeds_path', help='ReEDS-2.0 directory') + parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + #%% Run it + main(reeds_path=reeds_path, inputs_case=inputs_case) + + reeds.log.toc(tic=tic, year=0, process='inputs/h2_storage.py', + path=os.path.join(inputs_case,'..')) + + print('Finished h2_storage.py') diff --git a/reeds/input_processing/hourly_load.py b/reeds/input_processing/hourly_load.py new file mode 100644 index 00000000..77d86e87 --- /dev/null +++ b/reeds/input_processing/hourly_load.py @@ -0,0 +1,783 @@ +''' +This script handles the modification of load data. Specifically, it converts +state-level hourly end-use load to model region-level busbar load by doing +the following: + +- Allocate state load to model regions according to the method specified + in GSw_LoadAllocationMethod +- Apply scenario-specific modifications: + EER scenarios: + - Append historical load for pre-2021 model years + - Interpolate projected load for missing model years + - Apply calibration factors to projected load based on the difference + between historical and projected load in the latest year for which + historical and projected load data exist + Historical: + - Apply annual load growth factors + Other: + - If needed, replicate the dataset to match the number of weather years + specified for this run +- Apply a distribution loss factor + +The script also calculates peak load for each region level. +''' + +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import datetime +import numpy as np +import os +import pandas as pd +from pathlib import Path +import sys +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds + + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +def get_historical_state_load_for_model_year( + historical_state_load_annual: pd.DataFrame, + model_year: int +) -> pd.Series: + """ + Get historical annual state loads in MWh for the model year. + + Args: + historical_state_load_annual: Annual historical state loads in MWh. + model_year: Year to retrieve load values for. + + Returns: + pd.Series + """ + return ( + historical_state_load_annual + .loc[historical_state_load_annual.year == model_year] + .set_index('st') + ['MWh'] + ) + +def scale_historical_hourly_state_load_to_model_year( + historical_state_load_hourly: pd.DataFrame, + historical_state_load_annual: pd.DataFrame, + model_year: int +) -> pd.DataFrame: + """ + Scale historical hourly state load profiles to match historical + annual totals for the specified model year. + + Args: + historical_state_load_hourly: Hourly historical state load profiles + in MWh. + historical_state_load_annual: Annual historical state loads in MWh. + model_year: Year of annual load to scale hourly load by. + + Returns: + pd.DataFrame + """ + historical_model_year_state_load = get_historical_state_load_for_model_year( + historical_state_load_annual, + model_year + ) + # Calculate total state loads for each weather year + # of the historical hourly state load profiles + historical_weather_year_state_loads = ( + historical_state_load_hourly.groupby( + historical_state_load_hourly.index.get_level_values('datetime').year + ) + .transform('sum') + ) + # Scale the historical hourly state load profiles so that total state + # loads for each weather year match state loads for the model year + historical_state_load_hourly_scaled = ( + historical_state_load_hourly + / historical_weather_year_state_loads + * historical_model_year_state_load + ) + + return historical_state_load_hourly_scaled + +def interpolate_missing_model_years( + load_hourly: pd.DataFrame, + endyear: int +) -> pd.DataFrame: + """ + Linearly interpolate hourly load values for missing model years between + the first year of the load profiles and the specified end year. + + Args: + load_hourly: Hourly load profiles. + endyear: Final model year of resulting load profiles. + + Returns: + pd.DataFrame + """ + model_years = [ + year for year in + range(load_hourly.index.get_level_values('year').min(), endyear + 1) + ] + known_model_years = [ + year for year in + model_years if year in load_hourly.index.get_level_values('year') + ] + + dictload = {} + for model_year in model_years: + #find known years that bound this year + for i, known_model_year in enumerate(known_model_years): + if(known_model_year > model_year): + section_end_model_year = known_model_year + section_start_model_year = known_model_years[i-1] + break + + #grab dataframes for linear interpolation + df_load_beg = load_hourly.loc[section_start_model_year] + df_load_end = load_hourly.loc[section_end_model_year] + + #linear interpolation: + # y = y1 + (y2-y1)/(x2-x1)*(x-x1). x is year; y is value + df_load = ( + df_load_beg + + (df_load_end - df_load_beg) + / (section_end_model_year - section_start_model_year) + * (model_year-section_start_model_year) + ) + + dictload[model_year] = df_load + + load_hourly = pd.concat(dictload, names=('year',)) + + return load_hourly + +def calibrate_hourly_state_load_to_historical_annuals( + state_load_hourly: pd.DataFrame, + historical_state_load_annual: pd.DataFrame +) -> pd.DataFrame: + """ + For historical model years, scale hourly state load profiles to match + historical annual totals. For post-historical model years, scale hourly + state load profiles to increase the projected annual totals by the + difference between historical and projected annual totals for the + latest historical model year. + + Args: + state_load_hourly: Hourly state load profiles in MWh. + historical_state_load_annual: Annual historical state loads in MWh. + + Returns: + pd.DataFrame + """ + df_list = [] + + # For the model years for which we have historical annual loads, scale + # state_load_hourly so that its annual totals match each model year's + # historical annual loads + min_projected_model_year = ( + state_load_hourly.index.get_level_values('year').min() + ) + max_historical_model_year = historical_state_load_annual['year'].max() + for model_year in range( + min_projected_model_year, + max_historical_model_year + 1 + ): + model_year_historical_load = get_historical_state_load_for_model_year( + historical_state_load_annual, + model_year + ) + state_load_hourly_model_year = ( + state_load_hourly + .loc[( + state_load_hourly.index.get_level_values('year') == model_year + )] + .copy() + ) + calibration_factors = model_year_historical_load.div( + state_load_hourly_model_year + .groupby( + state_load_hourly_model_year.index + .get_level_values('datetime') + .year + ) + .transform('sum') + ) + state_load_hourly_model_year_scaled = ( + state_load_hourly_model_year.mul(calibration_factors) + ) + df_list.append(state_load_hourly_model_year_scaled) + + # For the latest model year for which we have historical annual loads + # (the calibration year), calculate the differences between + # the historical annual loads and projected annual loads + calibration_year_historical_load = ( + get_historical_state_load_for_model_year( + historical_state_load_annual, + max_historical_model_year + ) + ) + state_load_hourly_calibration_year = ( + state_load_hourly.loc[max_historical_model_year] + ) + calibration_diffs = calibration_year_historical_load.sub( + state_load_hourly_calibration_year + .groupby( + state_load_hourly_calibration_year.index + .get_level_values('datetime') + .year + ) + .transform('sum') + ) + + # For post-historical model years, scale state_load_hourly so that its + # annual totals match the sum of each model year's projected annual loads + # and the historical/projected load differences in the calibration year + max_projected_model_year = ( + state_load_hourly.index.get_level_values('year').max() + ) + for model_year in range( + max_historical_model_year + 1, + max_projected_model_year + 1 + ): + state_load_hourly_model_year = state_load_hourly.loc[model_year] + model_year_projected_load = ( + state_load_hourly_model_year + .groupby( + state_load_hourly_model_year.index + .get_level_values('datetime') + .year + ) + .transform('sum') + ) + calibration_factors = ( + model_year_projected_load.add(calibration_diffs) + .div(model_year_projected_load) + ) + state_load_hourly_model_year_scaled = ( + state_load_hourly_model_year + .mul(calibration_factors) + .assign(year=model_year) + .set_index('year', append=True) + .reorder_levels(['year', 'datetime']) + ) + df_list.append(state_load_hourly_model_year_scaled) + + state_load_hourly = pd.concat(df_list) + + return state_load_hourly + +def prepend_historical_hourly_state_load( + state_load_hourly: pd.DataFrame, + historical_state_load_hourly: pd.DataFrame, + historical_state_load_annual: pd.DataFrame +) -> pd.DataFrame: + """ + Create hourly state load profiles for historical model years and + prepend them to state_load_hourly. + + Args: + state_load_hourly: Hourly state load profiles in MWh. + historical_state_load_hourly: Hourly historical state load profiles + in MWh. + historical_state_load_annual: Annual historical state loads in MWh. + + Returns: + pd.DataFrame + """ + historical_load_dict = {} + + # For historical model years with no projected load profiles, create load + # profiles for each model year by scaling the historical load profiles to + # match annual totals for the model year + min_historical_model_year = historical_state_load_annual['year'].min() + min_projected_model_year = ( + state_load_hourly.index.get_level_values('year').min() + ) + for model_year in range( + min_historical_model_year, min_projected_model_year + ): + historical_state_load_hourly_scaled = ( + scale_historical_hourly_state_load_to_model_year( + historical_state_load_hourly, + historical_state_load_annual, + model_year + ) + ) + historical_load_dict[model_year] = historical_state_load_hourly_scaled + + historical_state_load_hourly = pd.concat( + historical_load_dict, + names=('year',) + ) + state_load_hourly = pd.concat([ + historical_state_load_hourly, + state_load_hourly + ]) + + return state_load_hourly + +def apply_load_growth_factors_to_historical_state_load( + historical_state_load_hourly: pd.DataFrame, + historical_state_load_annual: pd.DataFrame, + inputs_case: str, + solveyears: list[int] | None = None +) -> pd.DataFrame: + """ + Multiply hourly historical load profiles (scaled to match historical + annual totals for a baseline year) by annual load growth factors to + create projected load profiles for each model year. + + Args: + historical_state_load_hourly: Hourly historical state load + profiles in MWh. + historical_state_load_annual: Annual state loads in MWh + for historical years. + inputs_case: Path to the inputs case directory. + solveyears: Optional list of model years to filter load + multipliers down to. + + Returns: + pd.DataFrame + """ + # Read annual state multipliers representing projected load growth + # from a baseline year + load_multiplier = pd.read_csv( + os.path.join(inputs_case, 'load_multiplier.csv') + ) + # Scale the historical load profiles to match annual totals + # for the baseline year + load_multiplier_baseline_year = load_multiplier['year'].min() + historical_state_load_hourly = ( + scale_historical_hourly_state_load_to_model_year( + historical_state_load_hourly, + historical_state_load_annual, + load_multiplier_baseline_year + ) + ) + # Subset load multipliers for solve years only + if solveyears is not None: + load_multiplier = ( + load_multiplier[load_multiplier['year'].isin(solveyears)] + [['year', 'r', 'multiplier']] + ) + # Reformat hourly load profiles to merge with load multipliers + historical_state_load_hourly.reset_index(drop=False, inplace=True) + historical_state_load_hourly = pd.melt( + historical_state_load_hourly, + id_vars=['datetime'], + var_name='r', + value_name='load' + ) + # Merge load multipliers into hourly load profiles + state_load_hourly = historical_state_load_hourly.merge( + load_multiplier, + on=['r'], + how='outer' + ) + state_load_hourly.sort_values( + by=['r', 'year'], + ascending=True, + inplace=True + ) + state_load_hourly['load'] *= state_load_hourly['multiplier'] + state_load_hourly = state_load_hourly[['year', 'datetime', 'r', 'load']] + # Reformat hourly load profiles for GAMS + state_load_hourly = state_load_hourly.pivot_table( + index=['year', 'datetime'], columns='r', values='load') + # Convert 'year' index to integers + state_load_hourly.index = ( + state_load_hourly.index + .set_levels( + [ + state_load_hourly.index.levels[0].astype(int), + state_load_hourly.index.levels[1] + ], + level=['year', 'datetime'] + ) + ) + + return state_load_hourly + +def downselect_to_model_years( + load_hourly: pd.DataFrame, + model_years: list[int] +) -> pd.DataFrame: + """ + Retrieve the subset of hourly load profiles corresponding + to the given model years. + + Args: + load_hourly: Hourly load profiles. + model_years: List of model years used to filter load_hourly. + These years should correspond to load_hourly's "year" + index level. + + Returns: + pd.DataFrame + """ + return ( + load_hourly.loc[( + load_hourly.index + .get_level_values('year') + .isin(model_years) + )] + ) + +def downselect_to_weather_years( + load_hourly: pd.DataFrame, + weather_years: list[int] +) -> pd.DataFrame: + """ + Retrieve the subset of hourly load profiles corresponding + to the given weather years. + + Args: + load_hourly: Hourly load profiles. + weather_years: List of weather years used to filter load_hourly. + These years should correspond to the years of load_hourly's + "datetime" index level. + + Returns: + pd.DataFrame + """ + return ( + load_hourly.loc[( + load_hourly.index + .get_level_values('datetime') + .year + .isin(weather_years) + )] + ) + +def duplicate_weather_years(load_hourly, weather_years): + """ + Replicate hourly load profiles to match the number of weather years. + + Args: + load_hourly: Hourly load profiles with only one weather year of data. + weather_years: List of weather years to replicate load profiles for. + + Returns: + pd.DataFrame + """ + # Copy the load profiles n times for the number of weather years and + # concatenate them + num_years = len(weather_years) + load_hourly_wide = load_hourly.unstack('year') + + if len(load_hourly_wide) != 8760: + raise ValueError( + "The provided dataframe has more than one weather year of data." + ) + + load_hourly = ( + pd.concat([load_hourly_wide] * num_years, axis=0, ignore_index=True) + .rename_axis('hour').stack('year') + .reorder_levels(['year','hour']).sort_index(axis=0, level=['year','hour']) + ) + # Update the time index of the concatenated load profile to contain + # the hours of each weather year + fulltimeindex = pd.Series(reeds.timeseries.get_timeindex(weather_years)) + load_hourly['datetime'] = ( + load_hourly.index.get_level_values('hour').map(fulltimeindex) + ) + load_hourly = load_hourly.set_index('datetime', append=True).droplevel('hour') + + return load_hourly + +def apply_distribution_loss_factor( + load_hourly: pd.DataFrame, + distloss: float = 0.05 +) -> pd.DataFrame: + """ + Adjust hourly end-use load profiles to account for energy + lost during transmission and distribution. + + Args: + load_hourly: Hourly load profiles. + distloss: Percentage of busbar load lost during + transmission and distribution. + + Returns: + pd.DataFrame + """ + return load_hourly / (1 - distloss) + +def calculate_peak_load( + load_hourly: pd.DataFrame, + hierarchy: pd.DataFrame +) -> pd.DataFrame: + """ + Calculate coincident peak demand at all region hierarchy levels. + + Args: + load_hourly: Hourly load profiles. + hierarchy: Model region hierarchy levels. + + Returns: + pd.DataFrame + """ + _peakload = {} + for _level in hierarchy.columns: + _peakload[_level] = ( + ## Aggregate to level + load_hourly.rename(columns=hierarchy[_level]) + .groupby(axis=1, level=0).sum() + ## Calculate peak + .groupby(axis=0, level='year').max() + .T + ) + + ## Also calculate it at r level + _peakload['r'] = load_hourly.groupby(axis=0, level='year').max().T + peakload = pd.concat(_peakload, names=['level','region']).round(3) + + return peakload + +def reaggregate_to_model_regions( + state_load_hourly: pd.DataFrame, + inputs_case: str, + GSw_LoadAllocationMethod: str, + dr_data: bool = False +) -> pd.DataFrame: + """ + Allocate hourly state load to model regions according to the provided + load allocation method (e.g., according to each region's share of + state population). + + Args: + state_load_hourly: Hourly state load profiles. + inputs_case: Path to the inputs case directory. + GSw_LoadAllocationMethod: Method by which to allocate state + load to model regions. + + Returns: + pd.DataFrame + """ + # Get state/region-to-county disaggregation factors + disagg_data = reeds.io.get_disagg_data( + os.path.dirname(inputs_case), + disagg_variable=GSw_LoadAllocationMethod + ) + # Calculate state-to-region aggregation/disaggregation factors + state_region_factors = ( + disagg_data.groupby(['state', 'r'], as_index=False) + ['state_frac'] + .sum() + .pivot(index='state', columns='r', values='state_frac') + .rename_axis(None, axis=1) + .fillna(0) + ) + # Identify regions with aggregation/disaggregation factors of 0 + # and raise an error if any exist + if state_region_factors.sum().min() == 0: + regional_factors = state_region_factors.sum() + no_load_regions = ( + regional_factors.loc[regional_factors == 0].index.tolist() + ) + raise ValueError( + f"Load allocation method {GSw_LoadAllocationMethod} produced the " + "following regions with 0 load. Update GSw_LoadAllocationMethod " + "in your cases file:\n{}\n" + .format('\n'.join(no_load_regions)) + ) + # Demand response data may not be populated for every state + if dr_data: + state_region_factors = state_region_factors.loc[state_region_factors.index.intersection(state_load_hourly.columns), :] + + # Multiply the hourly state load profiles by the state-to-region factors + regional_load_hourly = ( + state_load_hourly[state_region_factors.index] + .dot(state_region_factors) + ) + + return regional_load_hourly + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== +def main(reeds_path, inputs_case): + print('Starting hourly_load.py') + + #%%### Load inputs + ### Load the input parameters + sw = reeds.io.get_switches(inputs_case) + weather_years = sw.resource_adequacy_years_list + scalars = reeds.io.get_scalars(inputs_case) + solveyears = reeds.io.get_years(os.path.dirname(inputs_case)) + hierarchy = reeds.io.get_hierarchy(os.path.dirname(inputs_case)) + + #%%%######################################### + # -- Get load profiles -- # + ############################################# + + state_load_hourly = reeds.io.get_load_hourly(inputs_case) + state_load_hourly = downselect_to_weather_years( + state_load_hourly, + weather_years + ) + historical_state_load_annual = reeds.io.get_historical_state_load_annual() + + match sw.GSw_LoadProfiles: + case _ if ( + sw.GSw_LoadProfiles.startswith('EER') + or Path(sw.GSw_LoadProfiles).is_file() + ): + endyear = int(sw.endyear) + state_load_hourly = interpolate_missing_model_years( + state_load_hourly, + endyear + ) + state_load_hourly = ( + calibrate_hourly_state_load_to_historical_annuals( + state_load_hourly, + historical_state_load_annual + ) + ) + historical_state_load_hourly = reeds.io.get_load_hourly( + GSw_LoadProfiles='historic' + ) + historical_state_load_hourly = downselect_to_weather_years( + historical_state_load_hourly, + weather_years + ) + state_load_hourly = prepend_historical_hourly_state_load( + state_load_hourly, + historical_state_load_hourly, + historical_state_load_annual + ) + state_load_hourly = downselect_to_model_years( + state_load_hourly, + solveyears + ) + case 'historic': + state_load_hourly = ( + apply_load_growth_factors_to_historical_state_load( + state_load_hourly, + historical_state_load_annual, + inputs_case, + solveyears + ) + ) + case _: + state_load_hourly = downselect_to_model_years( + state_load_hourly, + solveyears + ) + if len(state_load_hourly.unstack('year')) == 8760: + state_load_hourly = duplicate_weather_years( + state_load_hourly, + weather_years + ) + + regional_load_hourly = reaggregate_to_model_regions( + state_load_hourly, + inputs_case, + sw.GSw_LoadAllocationMethod + ) + + #%%%######################################### + # -- Performing Load Modifications -- # + ############################################# + + regional_load_hourly = apply_distribution_loss_factor( + regional_load_hourly, + scalars['distloss'] + ) + regional_load_hourly = regional_load_hourly.astype(np.float32) + + #%%%######################################### + # -- Peak Load Calculation -- # + ############################################# + + peakload = calculate_peak_load(regional_load_hourly, hierarchy) + + #%%%######################################### + # -- DR Shed Load Modifications -- # + ############################################# + + if int(sw.GSw_DRShed): + state_dr_shed_hourly = reeds.io.read_file(os.path.join(inputs_case, 'dr_shed_hourly.h5')) + dr_types = list({x.split('|')[0] for x in state_dr_shed_hourly.columns[1:]}) + + # Reformat to match state load profiles + state_dr_shed_hourly = state_dr_shed_hourly.reset_index().set_index(['year','datetime']) + regional_dr_shed_hourly = {} + for dr_type in dr_types: + type_cols = [col for col in state_dr_shed_hourly.columns if col.startswith(dr_type)] + reg_shed = state_dr_shed_hourly[type_cols].copy() + reg_shed.columns = [col.split('|')[1] for col in reg_shed.columns] + reg_shed = reaggregate_to_model_regions( + reg_shed, + inputs_case, + 'state_lpf', + dr_data=True + ) + # Add back dr type to column header + reg_shed.columns = [f"{dr_type}|{col}" for col in reg_shed.columns] + reg_shed = reg_shed.reset_index() + if isinstance(reg_shed['datetime'].iloc[0], bytes): + reg_shed['datetime'] = reg_shed['datetime'].str.decode('utf-8') + reg_shed['datetime'] = pd.to_datetime(reg_shed['datetime']) + reg_shed = reg_shed.set_index(['year','datetime']) + regional_dr_shed_hourly[dr_type] = reg_shed + + # Combined dr shed types + regional_dr_shed_hourly = pd.concat(regional_dr_shed_hourly.values(), axis=1) + regional_dr_shed_hourly = regional_dr_shed_hourly.astype(np.float32) + regional_dr_shed_hourly = regional_dr_shed_hourly.reset_index().set_index(['datetime']) + + #%%########################### + # -- Data Write-Out -- # + ############################## + + reeds.io.write_profile_to_h5(regional_load_hourly, 'load.h5', inputs_case) + peakload.to_csv(os.path.join(inputs_case,'peakload.csv')) + ### Write peak demand by NERC region to use in firm net import constraint + ( + peakload.loc['nercr'] + .stack('year') + .rename_axis(['*nercr','t']) + .rename('MW') + .to_csv(os.path.join(inputs_case,'peakload_nercr.csv')) + ) + if int(sw.GSw_DRShed): + reeds.io.write_profile_to_h5(regional_dr_shed_hourly, 'dr_shed_hourly.h5', inputs_case) + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if __name__ == '__main__': + # Time the operation of this script + tic = datetime.datetime.now() + + ### Parse arguments + parser = argparse.ArgumentParser( + description='Create run-specific hourly profiles', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('reeds_path', help='ReEDS-2.0 directory') + parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + #%% Run it + main(reeds_path=reeds_path, inputs_case=inputs_case) + + reeds.log.toc(tic=tic, year=0, process='inputs/hourly_load.py', + path=os.path.join(inputs_case,'..')) + + print('Finished hourly_load.py') diff --git a/reeds/input_processing/hourly_plots.py b/reeds/input_processing/hourly_plots.py new file mode 100644 index 00000000..2e7d8bb6 --- /dev/null +++ b/reeds/input_processing/hourly_plots.py @@ -0,0 +1,719 @@ +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== +import os +import sys +import logging +import pandas as pd +import numpy as np +import h5py +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib import patheffects as pe +import cmocean +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +from reeds.input_processing import hourly_repperiods +from reeds import plots +plots.plotparams() + +## Turn off logging for imported packages +for i in ['matplotlib']: + logging.getLogger(i).setLevel(logging.CRITICAL) + +#%%################# +### FIXED INPUTS ### +interactive = False + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +def plot_unclustered_periods(profiles, sw, reeds_path, figpath): + """ + """ + ### Overlapping days + for label, dfin in [ + # ('unscaled',profiles), + ('scaled',profiles), + ]: + properties = dfin.columns.get_level_values('property').unique() + nhours = len(dfin.columns.get_level_values('region').unique())*(24 if sw['GSw_HourlyType']=='day' else 120) + plt.close() + f,ax = plt.subplots(1,len(properties),sharex=True,figsize=(nhours/12, 3.75)) + for col, prop in enumerate(properties): + dfin[prop].T.reset_index(drop=True).plot( + ax=ax[col], lw=0.2, legend=False) + dfin[prop].mean(axis=0).reset_index(drop=True).plot( + ax=ax[col], lw=1.5, color='k', legend=False) + ax[col].set_title(prop) + for x in np.arange(0,nhours+1,(24 if sw['GSw_HourlyType']=='day' else 120)): + ax[col].axvline(x,c='k',ls=':',lw=0.3) + ax[col].tick_params(labelsize=9) + ax[col].set_ylim(0) + ### Formatting + title = ' | '.join( + profiles.columns.get_level_values('region').drop_duplicates().tolist()) + ax[0].annotate(title,(0,1.12), xycoords='axes fraction', fontsize='large',) + ax[0].xaxis.set_major_locator(mpl.ticker.MultipleLocator(12)) + ax[0].xaxis.set_minor_locator(mpl.ticker.MultipleLocator(6)) + ax[0].set_xlim(0, nhours) + plots.despine(ax) + plt.savefig(os.path.join(figpath,'inputs_profiles-day_hourly-{}.png'.format(label))) + if interactive: + plt.show() + plt.close() + + ### Sequential days + properties = profiles.columns.get_level_values('property').unique() + regions = profiles.columns.get_level_values('region').unique() + rows = [(p,r) for p in properties for r in regions] + colors = {'wind-ons':'C0', 'upv':'C1', 'load':'C2', 'wind-ofs':'C4'} + + for label in ['hourly', 'daily']: + plt.close() + f,ax = plt.subplots(len(rows),1,figsize=(12,len(rows)*0.5),sharex=True,sharey=True) + for row, (p,r) in enumerate(rows): + if label == 'hourly': + df = profiles[p][r].stack('h_of_period') + else: + df = profiles[p][r].mean(axis=1) + ax[row].fill_between(range(len(df)), df.values, lw=0, color=colors.get(p,'k')) + ax[row].set_ylabel(f'{p}\n{r}', ha='right', va='center', rotation=0,color=colors.get(p,'k')) + ax[0].set_ylim(0,1) + plots.despine(ax) + plt.savefig(os.path.join(figpath,f'inputs_profiles-year_{label}.png')) + if interactive: + plt.show() + plt.close() + + +def plot_feature_scatter(profiles_fitperiods, reeds_path, figpath): + """ + """ + ### Settings + colors = plots.rainbowmapper(profiles_fitperiods.columns.get_level_values('region').unique()) + props = ['load','upv','wind-ons'] + ### Plot it + plt.close() + f,ax = plt.subplots(3,3,figsize=(7,7),sharex='col',sharey='row') + for row, yax in enumerate(props): + for col, xax in enumerate(props): + for region, c in colors.items(): + ax[row,col].plot( + profiles_fitperiods[xax][region].values, + profiles_fitperiods[yax][region].values, + c=c, lw=0, markeredgewidth=0, ms=5, alpha=0.5, marker='o', + label=(region if (row,col)==(1,2) else '_nolabel'), + ) + ### Formatting + ax[1,-1].legend( + loc='center left', bbox_to_anchor=(1,0.5), frameon=False, fontsize='large', + handletextpad=0.3,handlelength=0.7, + ) + for i, prop in enumerate(props): + ax[-1,i].set_xlabel(prop) + ax[i,0].set_ylabel(prop) + + plots.despine(ax) + plt.savefig(os.path.join(figpath,'inputs_feature_scatter.png')) + if interactive: + plt.show() + plt.close() + + +def plot_ldc( + period_szn, profiles, rep_periods, + forceperiods_write, sw, reeds_path, figpath): + """ + """ + if isinstance(sw.GSw_HourlyWeatherYears, str): + GSw_HourlyWeatherYears = [int(y) for y in sw.GSw_HourlyWeatherYears.split('_')] + else: + GSw_HourlyWeatherYears = sw.GSw_HourlyWeatherYears + ### Get clustered load, repeating representative periods based on how many + ### periods they represent + numperiods = period_szn.value_counts().rename('numperiods').to_frame() + numperiods['yearperiod'] = numperiods.index.map(hourly_repperiods.szn2yearperiod).values + numperiods['year'] = numperiods.index.map(hourly_repperiods.szn2yearperiod).map(lambda x: x[0]) + numperiods['yperiod'] = numperiods.index.map(hourly_repperiods.szn2period) + periods = [[row.yearperiod] * row.numperiods for (i,row) in numperiods.iterrows()] + periods = [item for sublist in periods for item in sublist] + + #### Hourly + hourly_in = ( + profiles + .stack('h_of_period') + .loc[GSw_HourlyWeatherYears] + ).copy() + hourly_out = hourly_in.unstack('h_of_period').loc[periods].stack('h_of_period') + + #### Daily + periodly_in = hourly_in.groupby('yperiod').mean() + ## Index doesn't matter; replace it so we can take daily mean + periodly_out = hourly_out.copy() + hourly_out.index = hourly_in.index.copy() + periodly_out = hourly_out.groupby('yperiod').mean() + + ### Get axis coordinates: properties = rows, regions = columns + properties = periodly_out.columns.get_level_values('property').unique().values + regions = periodly_out.columns.get_level_values('region').unique().values + nrows, ncols = len(properties), len(regions) + coords = {} + if ncols == 1: + coords = dict(zip( + [(prop, regions[0]) for prop in properties], + range(nrows))) + elif nrows == 1: + coords = dict(zip( + [(properties[0], region) for region in regions], + range(ncols))) + else: + coords = dict(zip( + [(prop, reg) for prop in properties for reg in regions], + [(row, col) for row in range(nrows) for col in range(ncols)], + )) + + ###### Plot it + for plotlabel, xlabel, dfin, dfout in [ + ('hourly','Hour',hourly_in,hourly_out), + ('periodly','Period',periodly_in,periodly_out), + ]: + plt.close() + f,ax = plt.subplots( + nrows,ncols,figsize=(len(regions)*1.2,9),sharex=True,sharey='row', + gridspec_kw={'hspace':0.1,'wspace':0.1}, + ) + for region in regions: + for prop in properties: + if region not in dfout[prop]: + continue + df = dfout[prop][region].sort_values(ascending=False) + ax[coords[prop,region]].plot( + range(len(dfout)), df.values, + label='Clustered', c='C1') + # label='Clustered', c='C7', lw=0.25) + # ax[coords[prop,region]].scatter( + # range(len(dfout)), df.values, + # c=df.index.map(yperiod2color), s=10, lw=0, + # ) + ax[coords[prop,region]].plot( + range(len(dfin)), + dfin[prop][region].sort_values(ascending=False).values, + ls=':', label='Original', c='k') + ### Formatting + for region in regions: + ax[coords[properties[0], region]].set_title(region) + for prop in properties: + ax[coords[prop, regions[0]]].set_ylabel(prop) + ax[coords[prop, regions[0]]].set_ylim(0) + ax[coords[properties[0], regions[0]]].annotate( + '{} periods: {} forced, {} clustered'.format( + sw['GSw_HourlyNumClusters'], len(forceperiods_write), + int(sw['GSw_HourlyNumClusters']) - len(forceperiods_write)), + (0,1.15), xycoords='axes fraction', fontsize='x-large', + ) + ax[coords[properties[0], regions[-1]]].legend( + loc='lower right', bbox_to_anchor=(1,1.1), ncol=2) + ax[coords[properties[-1], regions[0]]].set_xlabel( + '{} of year'.format(xlabel), x=0, ha='left') + plots.despine(ax) + plt.savefig(os.path.join(figpath,f'inputs_ldc-{plotlabel}.png')) + if interactive: + plt.show() + plt.close() + + +def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG:5070'): + """ + """ + ### Settings + cmaps = { + 'cf_actual':plt.cm.turbo, 'cf_rep':plt.cm.turbo, 'cf_diff':plt.cm.RdBu_r, + 'GW_actual':cmocean.cm.rain, 'GW_rep':cmocean.cm.rain, + 'GW_diff':plt.cm.RdBu_r, 'GW_frac':plt.cm.RdBu_r, 'pct_diff':plt.cm.RdBu_r, + } + vm = { + 'upv':{'cf_actual':(0,0.3),'cf_rep':(0,0.3),'cf_diff':(-0.05,0.05)}, + 'wind-ons':{'cf_actual':(0,0.6),'cf_rep':(0,0.6),'cf_diff':(-0.05,0.05)}, + 'wind-ofs':{'cf_actual':(0,0.6),'cf_rep':(0,0.6),'cf_diff':(-0.05,0.05)}, + } + vlimload = {'GW_diff':1, 'pct_diff':5} + title = ( + '{}\n' + 'Algorithm={}, NumClusters={}, RegionLevel={}' + ).format( + os.path.abspath(os.path.join(inputs_case,'..')), + sw['GSw_HourlyClusterAlgorithm'], + sw['GSw_HourlyNumClusters'], sw['GSw_HourlyClusterRegionLevel'], + ) + techs = ['upv', 'wind-ons', 'wind-ofs'] + colors = {'cf_actual':'k', 'cf_rep':'C1'} + lss = {'cf_actual':':', 'cf_rep':'-'} + zorders = {'cf_actual':10, 'cf_rep':9} + if isinstance(sw.GSw_HourlyWeatherYears, str): + GSw_HourlyWeatherYears = [int(y) for y in sw.GSw_HourlyWeatherYears.split('_')] + else: + GSw_HourlyWeatherYears = sw.GSw_HourlyWeatherYears + + hierarchy = reeds.io.get_hierarchy(os.path.abspath(os.path.join(inputs_case,'..'))) + dfmap = reeds.io.get_dfmap(os.path.abspath(os.path.join(inputs_case,'..'))) + for key, df in dfmap.items(): + dfmap[key] = df.to_crs(crs) + dfmap[key]['centroid_x'] = dfmap[key].centroid.x + dfmap[key]['centroid_y'] = dfmap[key].centroid.y + + ### Get the CF data over all years, take the mean over weather years + recf = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) + recf = recf.loc[recf.index.year.isin(GSw_HourlyWeatherYears)].mean() + + ### Get the hourly data + hours = pd.read_csv( + os.path.join(inputs_case, periodtype, 'numhours.csv') + ).rename(columns={'*h':'h'}).set_index('h').numhours + dfcf = pd.read_csv(os.path.join(inputs_case, periodtype, 'cf_vre.csv')).rename(columns={'*i':'i'}) + + for tech in techs: + ### Get supply curve + dfsc = pd.read_csv( + os.path.join(inputs_case, f'supplycurve_{tech}.csv') + ).rename(columns={'region':'r'}) + dfsc['i'] = tech + '_' + dfsc['class'].astype(str) + ### Add geographic and CF information + sitemap = reeds.io.get_sitemap(offshore=(True if tech == 'wind-ofs' else False)) + + dfsc['latitude'] = dfsc.sc_point_gid.map(sitemap.latitude) + dfsc['longitude'] = dfsc.sc_point_gid.map(sitemap.longitude) + dfsc = plots.df2gdf(dfsc, crs=crs) + dfsc['resource'] = dfsc.i + '|' + dfsc.r + dfsc['cf_actual'] = dfsc.resource.map(recf) + + ### Get the annual average CF of the hourly-processed data + cf_hourly = dfcf.loc[dfcf.i.str.startswith(tech)].pivot( + index=['i','r'],columns='h',values='cf') + cf_hourly = ( + (cf_hourly * cf_hourly.columns.map(hours)).sum(axis=1) / hours.sum() + ).rename('cf_rep').reset_index() + cf_hourly['resource'] = cf_hourly.i + '|' + cf_hourly.r + + ### Merge with supply curve, take the difference + cfmap = dfsc.assign( + cf_rep=dfsc.resource.map(cf_hourly.set_index('resource').cf_rep)).copy() + cfmap['cf_diff'] = cfmap.cf_rep - cfmap.cf_actual + + ### Calculate the difference at different resolutions + levels = ['r', 'st', 'transgrp', 'transreg', 'interconnect', 'country'] + dfdiffs = {} + for col in levels: + if col != 'r': + cfmap[col] = cfmap.r.map(hierarchy[col]) + dfdiffs[col] = dfmap[col].copy() + df = cfmap.copy() + for i in ['cf_actual','cf_rep']: + df['weighted'] = cfmap[i] * cfmap.capacity + dfdiffs[col][i] = ( + df.groupby(col).weighted.sum() / df.groupby(col).capacity.sum() + ) + dfdiffs[col]['cf_diff'] = dfdiffs[col].cf_rep - dfdiffs[col].cf_actual + + ## Convert from point to polygons (raster is 11.52 km but include a little extra) + cfmap.geometry = cfmap.buffer(11530/2, cap_style='square') + + ### Plot the difference map + nrows, ncols, coords = plots.get_coordinates([ + 'cf_actual', 'cf_rep', 'cf_diff', + 'r', 'st', 'transgrp', + 'transreg', 'interconnect', 'country', + ], aspect=1) + + plt.close() + f,ax = plt.subplots( + nrows, ncols, figsize=(14,9), gridspec_kw={'wspace':-0.05, 'hspace':0}, + ) + ## Absolute and site difference + for col in ['cf_actual','cf_rep','cf_diff']: + cfmap.plot( + ax=ax[coords[col]], column=col, cmap=cmaps[col], + lw=0, + legend=False, + vmin=vm[tech][col][0], vmax=vm[tech][col][1], + ) + dfmap['st'].plot(ax=ax[coords[col]], facecolor='none', edgecolor='k', lw=0.1, zorder=1e6) + ## Colorbar + plots.addcolorbarhist( + f=f, ax0=ax[coords[col]], data=cfmap[col]*100, nbins=51, + cmap=cmaps[col], histratio=1.5, + vmin=vm[tech][col][0]*100, vmax=vm[tech][col][1]*100, + cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, + ) + ## Regional differences + for level in levels: + dfdiffs[level].plot( + ax=ax[coords[level]], column='cf_diff', cmap=cmaps['cf_diff'], + vmin=vm[tech]['cf_diff'][0], vmax=vm[tech]['cf_diff'][1], + lw=0, legend=False, + ) + dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) + ## Text differences + for r, row in (dfdiffs[level].assign(val=dfdiffs[level].cf_diff.abs()).sort_values('val')).iterrows(): + decimals = 0 if abs(row.cf_diff) >= 1 else 1 + ax[coords[level]].annotate( + f"{row.cf_diff*100:+.{decimals}f}", + [row.centroid_x, row.centroid_y], + ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), + path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], + ) + ## Colorbar + plots.addcolorbarhist( + f=f, ax0=ax[coords[level]], data=dfdiffs[level].cf_diff*100, nbins=51, + cmap=cmaps['cf_diff'], histratio=1.5, + vmin=vm[tech]['cf_diff'][0]*100, vmax=vm[tech]['cf_diff'][1]*100, + cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, + ) + ## Formatting + ax[0,0].annotate(title+f', tech={tech}', (0.05,1.05), xycoords='axes fraction', fontsize=10) + for level in coords: + ax[coords[level]].set_title({'cf_diff':'site'}.get(level,level), y=0.9, weight='bold') + ax[coords[level]].axis('off') + savename = f"inputs_cfmap-{tech.replace('-','')}.png" + print(savename) + plt.savefig(os.path.join(figpath,savename)) + if interactive: + plt.show() + plt.close() + + #%% Plot the distribution of capacity factors + plt.close() + f,ax = plt.subplots() + for col in ['cf_actual','cf_rep']: + ax.plot( + np.linspace(0,100,len(cfmap)), + cfmap.sort_values('cf_actual', ascending=False)[col].values, + label=col.split('_')[1], + color=colors[col], ls=lss[col], zorder=zorders[col], + ) + ax.set_ylim(0) + ax.set_xlim(-1,101) + ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(10)) + ax.legend(fontsize='large', frameon=False) + ax.set_ylabel('{} capacity factor [.]'.format(tech)) + ax.set_xlabel('Percent of sites [%]') + ax.set_title( + '\n'.join(title.split('\n')[1:]).replace(' ','\n').replace(',',''), + x=0, ha='left', fontsize=10) + plots.despine(ax) + plt.savefig(os.path.join(figpath, f"inputs_cfmapdist-{tech.replace('-','')}.png")) + if interactive: + plt.show() + plt.close() + + #%%### Do it again for load + ### Get the full hourly data, take the mean for the cluster year and weather year(s) + with h5py.File(os.path.join(inputs_case, 'load.h5'), 'r') as f: + index_year = pd.Series(f['index_0']) + index_datetime = pd.to_datetime(pd.Series(f['index_1']).str.decode('utf-8')) + index = pd.MultiIndex.from_arrays( + [index_year, index_datetime], names=['year','timeindex']) + load_raw = pd.DataFrame( + columns=pd.Series(f['columns']).str.decode('utf-8'), + data=f['data'], index=index, + ) + loadyears = load_raw.index.get_level_values('year').unique() + keepyear = ( + int(sw['GSw_HourlyClusterYear']) if int(sw['GSw_HourlyClusterYear']) in loadyears + else max(loadyears)) + load_raw = load_raw.loc[keepyear].copy() + load_mean = load_raw.loc[ + load_raw.index.map(lambda x: x.year in GSw_HourlyWeatherYears) + ].mean() / 1000 + ## load.h5 is busbar load, but b_inputs.gms ingests end-use load, so scale down by distloss + scalars = reeds.io.get_scalars(inputs_case) + load_mean *= (1 - scalars['distloss']) + ### Get the representative data, take the mean for the cluster year + load_allyear = ( + pd.read_csv(os.path.join(inputs_case, periodtype, 'load_allyear.csv')).rename(columns={'*r':'r'}) + .set_index(['t','r','h']).MW.loc[keepyear] + .multiply(hours).groupby('r').sum() + / hours.sum() + ) / 1000 + ### Map it + dfplot = dfmap['r'].copy() + for level in [i for i in levels if i != 'r']: + dfplot[level] = dfplot.index.map(hierarchy[level]) + dfplot['GW_actual'] = load_mean + dfplot['GW_rep'] = load_allyear + + #%% Calculate the difference at different resolutions + dfdiffs = {} + for level in levels: + dfdiffs[level] = dfplot.groupby(level)[['GW_actual','GW_rep']].sum() + dfdiffs[level] = dfmap[level].merge(dfdiffs[level], left_index=True, right_index=True) + dfdiffs[level]['GW_diff'] = dfdiffs[level].GW_rep - dfdiffs[level].GW_actual + dfdiffs[level]['pct_diff'] = (dfdiffs[level].GW_rep / dfdiffs[level].GW_actual - 1) * 100 + + ### Plot the difference map + nrows, ncols, coords = plots.get_coordinates([ + 'GW_actual', 'GW_rep', 'None', + 'r', 'st', 'transgrp', + 'transreg', 'interconnect', 'country', + ], aspect=1) + labels = {'GW_diff':'[GW]', 'pct_diff':'[%]'} + + for val, label in labels.items(): + plt.close() + f,ax = plt.subplots( + nrows, ncols, figsize=(14,9), gridspec_kw={'wspace':-0.05, 'hspace':0}, + ) + ## Absolute and site difference + for col in ['GW_actual','GW_rep']: + dfplot.plot( + ax=ax[coords[col]], column=col, cmap=cmaps[col], + lw=0, + legend=False, + vmin=0, vmax=dfplot[col].max(), + ) + dfmap['st'].plot(ax=ax[coords[col]], facecolor='none', edgecolor='k', lw=0.1, zorder=1e6) + ## Colorbar + plots.addcolorbarhist( + f=f, ax0=ax[coords[col]], data=dfplot[col], nbins=51, + cmap=cmaps[col], histratio=1.5, + vmin=0., vmax=dfplot[col].max(), + cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, + ) + ## Regional differences + for level in levels: + dfdiffs[level].plot( + ax=ax[coords[level]], column=val, cmap=cmaps[val], + vmin=-vlimload[val], vmax=vlimload[val], + lw=0, legend=False, + ) + dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) + ## Text differences + for r, row in (dfdiffs[level].assign(val=dfdiffs[level][val].abs()).sort_values('val')).iterrows(): + decimals = 0 if abs(row[val]) >= 1 else 1 + ax[coords[level]].annotate( + f"{row[val]:+.{decimals}f}", + [row.centroid_x, row.centroid_y], + ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), + path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], + ) + ## Colorbar + plots.addcolorbarhist( + f=f, ax0=ax[coords[level]], data=dfdiffs[level][val], nbins=51, + cmap=cmaps[val], histratio=1.5, + vmin=-vlimload[val], vmax=vlimload[val], + cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, + ) + ax[coords[level]].annotate( + label, (0.96,0.08), xycoords='axes fraction', ha='center', va='top', + weight='bold', fontsize=8, + ) + ## Formatting + ax[0,0].annotate(title+f', {val}', (0.05,1.05), xycoords='axes fraction', fontsize=10) + for level in coords: + ax[coords[level]].axis('off') + if level != 'None': + ax[coords[level]].set_title(level, y=0.9, weight='bold') + savename = f"inputs_loadmap-{val}.png" + print(savename) + plt.savefig(os.path.join(figpath,savename)) + if interactive: + plt.show() + plt.close() + + #%% Plot the distribution of load by region + colors = {'GW_actual':'k', 'GW_rep':'C1'} + lss = {'GW_actual':':', 'GW_rep':'-'} + zorders = {'GW_actual':10, 'GW_rep':9} + plt.close() + f,ax = plt.subplots() + for col in ['GW_actual','GW_rep']: + ax.plot( + range(1,len(dfplot)+1), + dfplot.sort_values('GW_actual', ascending=False)[col].values, + label='{} ({:.1f} GW mean)'.format(col.split('_')[1], dfplot[col].sum()), + color=colors[col], ls=lss[col], zorder=zorders[col], + ) + ax.set_ylim(0) + ax.set_xlim(0,len(dfplot)+1) + ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(10)) + ax.legend(fontsize='large', frameon=False) + ax.set_ylabel('Average load [GW]') + ax.set_xlabel('Number of BAs') + ax.set_title(title.replace(' ','\n').replace(',',''), x=0, ha='left', fontsize=10) + plots.despine(ax) + plt.savefig(os.path.join(figpath,'inputs_loadmapdist.png')) + if interactive: + plt.show() + plt.close() + + +def plot_8760(profiles, period_szn, sw, reeds_path, figpath): + def get_profiles(regions, year): + """Assemble 8760 profiles from original and representative days""" + timeindex = pd.date_range(f'{year}-01-01',f'{year+1}-01-01',freq='H',inclusive='left')[:8760] + props = profiles.columns.get_level_values('property').unique() + ### Original profiles + dforig = {} + for prop in props: + df = profiles[prop].loc[year].stack('h_of_period')[regions].sum(axis=1) + dforig[prop] = df / df.max() + dforig[prop].index = timeindex + dforig = pd.concat(dforig, axis=1) + + ### Representative profiles + periodmap = period_szn.map(hourly_repperiods.szn2yearperiod).to_frame() + periodmap['year'] = periodmap.szn.map(lambda x: x[0]) + periodmap['yperiod'] = periodmap.szn.map(lambda x: x[1]) + periodmap = periodmap.loc[periodmap.year==year].yperiod + + dfrep = {} + for prop in props: + df = ( + profiles[prop].loc[year].loc[periodmap.values] + .stack('h_of_period')[regions].sum(axis=1)) + dfrep[prop] = df / df.max() + dfrep[prop].index = timeindex + dfrep = pd.concat(dfrep, axis=1) + + return dforig, dfrep + + ###### All regions + if isinstance(sw.GSw_HourlyWeatherYears, str): + GSw_HourlyWeatherYears = [int(y) for y in sw.GSw_HourlyWeatherYears.split('_')] + else: + GSw_HourlyWeatherYears = sw.GSw_HourlyWeatherYears + for year in GSw_HourlyWeatherYears: + props = profiles.columns.get_level_values('property').unique() + regions = profiles.columns.get_level_values('region').unique() + dforig, dfrep = get_profiles(regions, year) + + ### Original vs representative + plt.close() + f,ax = plt.subplots(38,1,figsize=(12,16),sharex=True,sharey=True) + for i, prop in enumerate(props): + plots.plotyearbymonth( + dfrep[prop].rename('Representative').to_frame(), + style='line', colors=['C1'], ls='-', f=f, ax=ax[i*12+i:(i+1)*12+i]) + plots.plotyearbymonth( + dforig[prop].rename('Original').to_frame(), + style='line', colors=['k'], ls=':', f=f, ax=ax[i*12+i:(i+1)*12+i]) + for i in [12,25]: + ax[i].axis('off') + for i, prop in list(zip(range(len(props)), props)): + ax[i*12+i].set_title( + '{}: {}'.format(prop,' | '.join(regions)),x=0,ha='left',fontsize=12) + ax[0].legend(loc='lower left', bbox_to_anchor=(0,1.5), ncol=2, frameon=False) + plt.savefig(os.path.join(figpath,f'inputs_8760-allregions-{year}.png')) + if interactive: + plt.show() + plt.close() + + ### Load, wind, solar together; original + plt.close() + f,ax = plots.plotyearbymonth( + dforig[['wind-ons','upv']], colors=['#0064ff','#ff0000'], alpha=0.5) + plots.plotyearbymonth(dforig['load'], f=f, ax=ax, style='line', colors='k') + plt.savefig(os.path.join(figpath,f'inputs_8760-allregions-original-{year}.png')) + if interactive: + plt.show() + plt.close() + + ### Load, wind, solar together; representative + plt.close() + f,ax = plots.plotyearbymonth( + dfrep[['wind-ons','upv']], colors=['#0064ff','#ff0000'], alpha=0.5) + plots.plotyearbymonth(dfrep['load'], f=f, ax=ax, style='line', colors='k') + plt.savefig(os.path.join(figpath,f'inputs_8760-allregions-representative-{year}.png')) + if interactive: + plt.show() + plt.close() + + ###### Individual regions, original vs representative + for region in profiles.columns.get_level_values('region').unique(): + dforig, dfrep = get_profiles([region], year) + + plt.close() + f,ax = plt.subplots(38,1,figsize=(12,16),sharex=True,sharey=True) + for i, prop in enumerate(props): + plots.plotyearbymonth( + dfrep[prop].rename('Representative').to_frame(), + style='line', colors=['C1'], ls='-', f=f, ax=ax[i*12+i:(i+1)*12+i]) + plots.plotyearbymonth( + dforig[prop].rename('Original').to_frame(), + style='line', colors=['k'], ls=':', f=f, ax=ax[i*12+i:(i+1)*12+i]) + for i in [12,25]: + ax[i].axis('off') + for i, prop in list(zip(range(len(props)), props)): + ax[i*12+i].set_title('{}: {}'.format(prop,region),x=0,ha='left',fontsize=12) + ax[0].legend(loc='lower left', bbox_to_anchor=(0,1.5), ncol=2, frameon=False) + plt.savefig(os.path.join(figpath,f'inputs_8760-{region}-{year}.png')) + if interactive: + plt.show() + + +def plot_load_days(profiles, rep_periods, period_szn, sw, reeds_path, figpath): + """ + """ + ### Input processing + idx_reedsyr = period_szn.map(hourly_repperiods.szn2yearperiod) + medoid_profiles = profiles.loc[rep_periods] + centroids = profiles.loc[rep_periods] + centroid_profiles = centroids * profiles.stack('h_of_period').max() + + colors = plots.rainbowmapper(list(set(idx_reedsyr)), plt.cm.turbo) + + ### Plot all days on same x axis + ## Map days to axes + ncols = len(colors) + nrows = 1 + coords = dict(zip( + colors.keys(), + [col for col in range(ncols)] + )) + plt.close() + f,ax = plt.subplots( + nrows, ncols, figsize=(1.5*ncols,2.5*nrows), sharex=True, sharey=True) + for day in range(len(idx_reedsyr)): + szn = idx_reedsyr[day] + this_profile = profiles.load.iloc[day].groupby('h_of_period').sum() + ax[coords[szn]].plot( + range(len(this_profile)), this_profile.values/1e3, color=colors[szn], alpha=0.5) + ## add centroids and medoids to the plot: + for szn in colors: + ## centroids - only for clustered days, not force-included days + try: + ax[coords[szn]].plot( + range(len(this_profile)), + centroid_profiles['load'].loc[szn].groupby('h_of_period').sum().values/1e3, + color='k', alpha=1, linewidth=2, label='centroid', + ) + except IndexError: + pass + ## medoids + ax[coords[szn]].plot( + range(len(this_profile)), + medoid_profiles['load'].loc[szn].groupby('h_of_period').sum().values/1e3, + ls='--', color='0.7', alpha=1, linewidth=2, label='medoid', + ) + ## title + ax[coords[szn]].set_title( + '{}, {} days'.format(szn, idx_reedsyr.value_counts()[szn]), size=9) + + ax[0].legend(loc='upper left', frameon=False, fontsize='small') + ax[0].set_xlabel('Hour') + ax[0].set_ylabel('Conterminous\nUS Load [GW]', y=0, ha='left') + ax[0].xaxis.set_major_locator( + mpl.ticker.MultipleLocator(6 if sw['GSw_HourlyType']=='day' else 24)) + ax[0].xaxis.set_minor_locator( + mpl.ticker.MultipleLocator(3 if sw['GSw_HourlyType']=='day' else 12)) + ax[0].annotate( + 'Cluster Comparison (All Days of All Weather Years Shown)', + xy=(0,1.2), xycoords='axes fraction', ha='left', + ) + plots.despine(ax) + plt.savefig(os.path.join(figpath,'inputs_day_comparison_all.png')) + if interactive: + plt.show() + plt.close() diff --git a/reeds/input_processing/hourly_repperiods.py b/reeds/input_processing/hourly_repperiods.py new file mode 100644 index 00000000..1fb3bac3 --- /dev/null +++ b/reeds/input_processing/hourly_repperiods.py @@ -0,0 +1,977 @@ +""" +The purpose of this script is to collect 8760 data as it is output by +hourlize and perform a temporal aggregation to produce load and capacity +factor parameters for the representative days that will be read by ReEDS. +The other outputs are the hours/seasons to be modeled in ReEDS and linking +sets used in the model. + +General notes: +* h: a timeslice with an h prefix, starting at h1 +* hour: an hour of the full period, starting at 1 ([1-8760] for 1 year or [1-61320] for 7 years) +* dayhour: a clock hour starting at 1 [1-24] +* period: a day (if GSw_HourlyType=='day') or a wek (if GSw_HourlyType=='wek') +* wek: A consecutive 5-day period (365 is only divisible by 1, 5, 73, and 365) + +This script is currently not compatible with: +* Climate impacts (climateprep.py) +* Beyond-2050 modeling (forecast.py) +* Flexible demand +""" + +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== +import argparse +import json +import numpy as np +import os +import sys +import datetime +import pandas as pd +import scipy +import sklearn.cluster +import sklearn.neighbors +import traceback +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +from reeds.input_processing import hourly_writetimeseries +from reeds.input_processing import hourly_plots +## Time the operation of this script +tic = datetime.datetime.now() + + +#%%################# +### FIXED INPUTS ### + +decimals = 3 +### Whether to show plots interactively [default False] +interactive = False +### VRE techs considered for GSw_PRM_StressSeedMinRElevel and GSw_HourlyMinRElevel +techs_min_vre = ['upv', 'wind-ons'] + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +def szn2yearperiod(szn): + """ + szn's are formatted as 'y{20xx}{d or w}{day of year or wek of year}' + where a 'wek' is a 5-day period (5*73 = 365) + """ + year, period = szn.split('d') if 'd' in szn else szn.split('w') + return int(year.strip('y')), int(period) + + +def szn2period(szn): + """ + szn's are formatted as 'y{20xx}{d or w}{day of year or wek of year}' + where a 'wek' is a 5-day period (5*73 = 365) + """ + year, period = szn.split('d') if 'd' in szn else szn.split('w') + return int(period) + + +############################### +# -- Load Processing -- # +############################### + +def get_load(inputs_case, keep_modelyear=None, keep_weatheryears=[2012]): + """ + """ + ### Subset to modeled regions + load = reeds.io.read_file(os.path.join(inputs_case,'load.h5'), parse_timestamps=True) + ### Subset to keep_modelyear if provided + if keep_modelyear: + load = load.loc[keep_modelyear].copy() + ### load.h5 is busbar load, but b_inputs.gms ingests end-use load, so scale down by distloss + scalars = reeds.io.get_scalars(inputs_case) + load *= (1 - scalars['distloss']) + + ### Downselect to weather years if provided + if isinstance(keep_weatheryears, list): + load = load.loc[load.index.year.isin(keep_weatheryears)] + + return load + + +def optimize_period_weights(profiles_fitperiods, numclusters=100): + """ + The optimization approach (minimizing sum of absolute errors) is described at + https://optimization.mccormick.northwestern.edu/index.php/Optimization_with_absolute_values + The general idea of optimizing period weights to reproduce regional variability is similar + to the method used in the EPRI US-REGEN model, described at + https://www.epri.com/research/products/000000003002016601 + """ + ### Imports + import pulp + + ### Input processing + profiles_day = ( + profiles_fitperiods.groupby(['property','region'], axis=1).mean()) + profiles_mean = profiles_day.mean() + numdays = len(profiles_day) + days = profiles_day.index.values + + ### Optimization: minimize sum of absolute errors + m = pulp.LpProblem('LinearDaySelection', pulp.LpMinimize) + ###### Variables + ### day weights + WEIGHT = pulp.LpVariable.dicts('WEIGHT', (d for d in days), lowBound=0, cat='Continuous') + ### errors + ERROR_POS = pulp.LpVariable.dicts( + 'ERROR_POS', (c for c in profiles_day.columns), lowBound=0, cat='Continuous') + ERROR_NEG = pulp.LpVariable.dicts( + 'ERROR_NEG', (c for c in profiles_day.columns), lowBound=0, cat='Continuous') + ###### Constraints + ### weights must sum to 1 + m += pulp.lpSum([WEIGHT[d] for d in days]) == 1 + ### definition of errors + for c in profiles_day.columns: + m += ( + ### Full error for column (given by positive component minus negative component)... + ERROR_POS[c] - ERROR_NEG[c] + ### ...plus sum of values for weighted representative days... + + pulp.lpSum([WEIGHT[d] * profiles_day[c][d] for d in days]) + ### ...equals the mean for that column + == profiles_mean[c]) + ###### Objective: minimize the sum of absolute values of errors across all columns + m += pulp.lpSum([ + ERROR_POS[c] + ERROR_NEG[c] + for c in profiles_day.columns + ]) + + ### Solve it + m.solve(solver=pulp.PULP_CBC_CMD(msg=True)) + + ### Collect weights, scaled by total number of days + weights = pd.Series({d:WEIGHT[d].varValue for d in days}) * numdays + + ### Truncate based on numclusters, scale appropriately, and convert to integers + ### Keep the the 'numclusters' highest-weighted days + rweights = (weights.sort_values(ascending=False)[:numclusters]) + ### Scale so that the weights sum to numdays (have to do if numclusters is small) + rweights *= numdays / rweights.sum() + ### Convert to integers + iweights = rweights.round(0).astype(int) + ### Scale all weights little by little until they sum to number of actual days + sumweights = iweights.sum() + diffweights = sumweights - numdays + increment = 0.00001 * (1 if diffweights < 0 else -1) + for i in range(1000000): + iweights = (rweights * (1 + increment*i)).round(0).astype(int) + if iweights.sum() == numdays: + break + + iweights = iweights.replace(0,np.nan).dropna().astype(int) + ### Make sure it worked + if iweights.sum() != numdays: + raise ValueError(f'Sum of rounded weights = {iweights.sum()} != {numdays}') + + return profiles_day, iweights, weights + + +def assign_representative_days(profiles_day, rweights): + """ + """ + ### Imports + import pulp + + ### Input processing + actualdays = profiles_day.index.values + repdays = list(rweights.index) + + ### Optimization: minimize sum of absolute errors + m = pulp.LpProblem('RepDayAssignment', pulp.LpMinimize) + ###### Variables + ### Weighting of rep days (r) for each actual day (a). + ### Can only use whole days, so it's a binary variable. + WEIGHT = pulp.LpVariable.dicts( + 'WEIGHT', ((a,r) for a in actualdays for r in repdays), + lowBound=0, upBound=1, cat=pulp.LpInteger) + ### Errors. These are defined for features (c) and for actual days (a). + ERROR_POS = pulp.LpVariable.dicts( + 'ERROR_POS', ((a,c) for a in actualdays for c in profiles_day.columns), + lowBound=0, cat='Continuous') + ERROR_NEG = pulp.LpVariable.dicts( + 'ERROR_NEG', ((a,c) for a in actualdays for c in profiles_day.columns), + lowBound=0, cat='Continuous') + ###### Constraints + ### Each actual day can only be assigned to one representative day + for a in actualdays: + m += pulp.lpSum([WEIGHT[a,r] for r in repdays]) == 1 + ### Each representative day must be used a number of times equal to its weight + for r in repdays: + m += pulp.lpSum([WEIGHT[a,r] for a in actualdays]) == rweights[r] + ### Define the error variables + for a in actualdays: + for c in profiles_day.columns: + m += ( + ### Full error for column on actual day (given by positive + ### component minus negative component)... + ERROR_POS[a,c] - ERROR_NEG[a,c] + ### ...plus value for its representative day (since WEIGHT is binary)... + + pulp.lpSum([WEIGHT[a,r] * profiles_day[c][r] for r in repdays]) + ### ...equals the actual value for that column and day + == profiles_day[c][a]) + ###### Objective: minimize the sum of absolute values of errors + m += pulp.lpSum([ + ERROR_POS[a,c] + ERROR_NEG[a,c] + for a in actualdays for c in profiles_day.columns + ]) + + ### Solve it + m.solve(solver=pulp.PULP_CBC_CMD(msg=True)) + + ### Collect assignments + assignments = pd.Series( + {(a,r):WEIGHT[a,r].varValue for a in actualdays for r in repdays}).astype(int) + assignments.index = assignments.index.rename(['act','rep']) + a2r = assignments.replace(0,np.nan).dropna().reset_index(level='rep').rep + + return a2r + + +def identify_peak_containing_periods(df, hierarchy, level): + """ + Identify the period containing the peak value. + Set of (region,reason,year,yperiod), with yperiod starting from 1. + """ + ### Map columns to level, then sum + if level == 'r': + rmap = pd.Series(hierarchy.index, index=hierarchy.index) + else: + rmap = hierarchy[level] + dfmod = df.copy() + dfmod.columns = dfmod.columns.map(lambda x: x.split('|')[-1]).map(rmap) + dfmod = dfmod.groupby(axis=1, level=0).sum() + ### Get the max value by (year,yperiod) + dfmax = dfmod.groupby(['year','yperiod']).max() + ### Get the max (year,yperiod) for each column + forceperiods = set([(c, 'peak-containing', *dfmax[c].nlargest(1).index[0]) for c in dfmax]) + + return forceperiods + + +def identify_min_periods(df, hierarchy, level, prefix=''): + """ + Identify the period with the minimum average value. + Set of (region,reason,year,yperiod), with yperiod starting from 1. + """ + ### Map columns to level, then sum + if level == 'r': + rmap = pd.Series(hierarchy.index, index=hierarchy.index) + else: + rmap = hierarchy[level] + dfmod = df[[c for c in df if c.startswith(prefix)]].copy() + dfmod.columns = dfmod.columns.map(lambda x: x.split('|')[-1]).map(rmap) + dfmod = dfmod.groupby(axis=1, level=0).sum() + ### Get the mean value by (year,yperiod) + dfmean = dfmod.groupby(['year','yperiod']).mean() + ### Get the min (year,yperiod) for each column + forceperiods = set([(c, 'min average', *dfmean[c].nsmallest(1).index[0]) for c in dfmean]) + + return forceperiods + + + +########################### +# -- Clustering -- # +########################### + +def cluster_profiles(profiles_fitperiods, sw, forceperiods_yearperiod): + """ + Cluster the load and (optionally) RE profiles to find representative days for dispatch in ReEDS. + + Args: + GSw_HourlyClusterRegionLevel: Level of inputs/hierarchy.csv at which to aggregate + profiles for clustering. VRE profiles are converted to available-capacity-weighted + averages. That's not the best - it would be better to weight sites that are more likely + to be developed more strongly - but it's better than not weighting at all. + + Returns: + cf_representative - hourly profile of centroid or medoid capacity factor values + for all regions and technologies + load_representative - hourly profile of centroid or medoid load values for all regions + period_szn - day indices of each cluster center + """ + ###### Run the clustering + print(f"Performing {sw.GSw_HourlyClusterAlgorithm} clustering") + if ( + sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical') + or sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kme') + ): + if sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical'): + args = sw['GSw_HourlyClusterAlgorithm'].split('_') + if len(args) > 1: + metric = args[1] + linkage = args[2] + else: + metric = 'euclidean' + linkage = 'ward' + clusters = sklearn.cluster.AgglomerativeClustering( + n_clusters=int(sw['GSw_HourlyNumClusters']), + metric=metric, linkage=linkage, + ) + elif sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmeans'): + clusters = sklearn.cluster.KMeans( + n_clusters=int(sw['GSw_HourlyNumClusters']), + random_state=0, n_init='auto', max_iter=1000, + ) + elif sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): + import sklearn_extra.cluster + args = sw['GSw_HourlyClusterAlgorithm'].split('_') + if len(args) > 1: + metric = args[1] + init = args[2] + else: + metric = 'euclidean' + init = 'heuristic' + clusters = sklearn_extra.cluster.KMedoids( + n_clusters=int(sw['GSw_HourlyNumClusters']), + metric=metric, init=init, method='pam', + max_iter=1000, random_state=0, + ) + ### Generate the fits + idx = clusters.fit_predict(profiles_fitperiods) + ### Get nearest period to each centroid + centroids = pd.DataFrame( + sklearn.neighbors.NearestCentroid().fit(profiles_fitperiods, idx).centroids_, + columns=profiles_fitperiods.columns, + ) + nearest_period = { + i: + profiles_fitperiods.loc[:,idx==i,:].apply( + lambda row: scipy.spatial.distance.euclidean(row, centroids.loc[i]), + axis=1 + ).nsmallest(1).index[0] + for i in range(int(sw['GSw_HourlyNumClusters'])) + } + + period_szn = pd.DataFrame({ + 'period': profiles_fitperiods.index.values, + 'szn': [f"y{i[0]}{sw['GSw_HourlyType'][0]}{i[1]:>03}" + for i in pd.Series(idx).map(nearest_period)] + ### Add the force-include periods to the end of the list of seasons + }) + period_szn = pd.concat([ + period_szn, + pd.DataFrame({ + 'period': list(forceperiods_yearperiod), + 'szn': [f"y{i[0]}{sw['GSw_HourlyType'][0]}{i[1]:>03}" + for i in forceperiods_yearperiod] + }) + ]).sort_values('period').set_index('period').szn + + elif sw['GSw_HourlyClusterAlgorithm'] in ['opt','optimized','optimize']: + ### Optimize the weights of representative days + profiles_day, rweights, weights = optimize_period_weights( + profiles_fitperiods=profiles_fitperiods, numclusters=int(sw['GSw_HourlyNumClusters'])) + ### Optimize the assignment of actual days to representative days + a2r = assign_representative_days(profiles_day=profiles_day.round(4), rweights=rweights) + + if len(rweights) < int(sw['GSw_HourlyNumClusters']): + print( + 'Asked for {} representative periods but only needed {}'.format( + sw['GSw_HourlyNumClusters'], len(rweights))) + + period_szn = pd.concat([ + a2r.reset_index().rename(columns={'act':'period','rep':'szn'}), + pd.DataFrame({'period':list(forceperiods_yearperiod), + 'szn':list(forceperiods_yearperiod)}) + if len(forceperiods_yearperiod) else None + ]).sort_values('period').set_index('period').szn + period_szn = period_szn.map(lambda x: f'y{x[0]}{sw.GSw_HourlyType[0]}{x[1]:>03}') + + elif 'user' in sw['GSw_HourlyClusterAlgorithm'].lower(): + print('Using user-defined representative period weights') + period_szn = pd.read_csv( + os.path.join(inputs_case,'period_szn_user.csv') + ).set_index('actual_period').rep_period.rename('szn') + period_szn.index = period_szn.index.map(szn2yearperiod).values + period_szn.index = period_szn.index.rename('period') + + + ### Get the list of representative periods for convenience + rep_periods = sorted(period_szn.map(szn2yearperiod).unique()) + + return rep_periods, period_szn + + +def make_timestamps(sw): + ### Get some useful constants + hoursperperiod = {'day':24, 'wek':120, 'year':24} + periodsperyear = {'day':365, 'wek':73, 'year':365} + weather_years = sw.resource_adequacy_years_list + + ### Get map from yperiod, hour, and h_of_period to timestamp + timestamps = pd.DataFrame({ + 'year': np.ravel([[y]*8760 for y in weather_years]), + 'h_of_year': np.ravel([list(range(1,8761)) * len(weather_years)]), + 'h_of_period': np.ravel( + [f'{h+1:>03}' for h in range(hoursperperiod[sw['GSw_HourlyType']])] + * periodsperyear[sw['GSw_HourlyType']] * len(weather_years)), + 'yperiod': np.ravel( + [p+1 for p in range(periodsperyear[sw['GSw_HourlyType']]) + for h in range(hoursperperiod[sw['GSw_HourlyType']])] + * len(weather_years)), + 'h_of_day': np.ravel( + [f'{h+1:>03}' for h in range(hoursperperiod['day'])] + * periodsperyear['day'] * len(weather_years)), + 'yday': np.ravel( + [p+1 for p in range(periodsperyear['day']) + for h in range(hoursperperiod['day'])] + * len(weather_years)), + 'h_of_wek': np.ravel( + [f'{h+1:>03}' for h in range(hoursperperiod['wek'])] + * periodsperyear['wek'] * len(weather_years)), + 'ywek': np.ravel( + [p+1 for p in range(periodsperyear['wek']) + for h in range(hoursperperiod['wek'])] + * len(weather_years)), + }) + timestamps['timestamp'] = ( + 'y' + timestamps.year.astype(str) + ## d for day and w for wek + + ('w' if sw.GSw_HourlyType == 'wek' else 'd') + + timestamps.yperiod.astype(str).map('{:>03}'.format) + + 'h' + timestamps.h_of_period + ) + timestamps['period'] = timestamps['timestamp'].map(lambda x: x.split('h')[0]) + timestamps['day'] = ( + 'y' + timestamps.year.astype(str) + + 'd' + timestamps.yday.astype(str).map('{:>03}'.format) + ) + timestamps['wek'] = ( + 'y' + timestamps.year.astype(str) + + 'w' + timestamps.ywek.astype(str).map('{:>03}'.format) + ) + timestamps.index = np.ravel([ + pd.date_range( + f'{y}-01-01', f'{y+1}-01-01', + freq='H', inclusive='left', tz='Etc/GMT+6', + )[:8760] + for y in weather_years + ]) + + return timestamps + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== + +def main( + sw, + reeds_path, + inputs_case, + periodtype='rep', + minimal=0, +): + """ + """ + #%% Parse inputs if necessary + if not isinstance(sw['GSw_HourlyClusterWeights'], pd.Series): + sw['GSw_HourlyClusterWeights'] = pd.Series(json.loads( + '{"' + + (':'.join(','.join(sw['GSw_HourlyClusterWeights'].split('/')).split('_')) + .replace(':','":').replace(',',',"')) + +'}' + )) + sw['GSw_HourlyClusterWeights'].index = sw['GSw_HourlyClusterWeights'].index.rename('property') + sw['GSw_HourlyClusterWeights'] = ( + sw['GSw_HourlyClusterWeights'].loc[sw['GSw_HourlyClusterWeights'] != 0] + ).copy() + if not isinstance(sw['GSw_HourlyWeatherYears'], list): + sw['GSw_HourlyWeatherYears'] = [int(y) for y in sw['GSw_HourlyWeatherYears'].split('_')] + if not isinstance(sw['GSw_CSP_Types'], list): + sw['GSw_CSP_Types'] = [int(i) for i in sw['GSw_CSP_Types'].split('_')] + ## VRE techs that can be used for profiles + techs_vre = ['upv', 'wind-ons', 'wind-ofs'] if int(sw.GSw_OfsWind) else ['upv', 'wind-ons'] + + #%% Direct plots to outputs folder + figpath = os.path.join(inputs_case,'..', 'outputs', 'figures') + os.makedirs(figpath, exist_ok=True) + os.makedirs(os.path.join(inputs_case, periodtype), exist_ok=True) + + val_r_all = pd.read_csv( + os.path.join(inputs_case, 'val_r_all.csv'), header=None).squeeze(1).tolist() + modelyears = pd.read_csv( + os.path.join(inputs_case, 'modeledyears.csv')).columns.astype(int) + # Use agglevel_variables function to obtain spatial resolution variables + agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) + + #%% Get map from yperiod, hour, and h_of_period to timestamp + timestamps = make_timestamps(sw) + timestamps_myr = timestamps.loc[timestamps.year.isin(sw['GSw_HourlyWeatherYears'])].copy() + + ### Get region hierarchy for use with GSw_HourlyClusterRegionLevel + hierarchy = pd.read_csv( + os.path.join(inputs_case,'hierarchy.csv')).rename(columns={'*r':'r'}).set_index('r') + hierarchy_orig = pd.read_csv( + os.path.join(inputs_case,'hierarchy_original.csv')) + + if sw.GSw_HourlyClusterRegionLevel == 'r': + rmap = pd.Series(hierarchy_orig.index, index=hierarchy_orig.index) + elif agglevel_variables['agglevel'] == 'county' or 'county' in agglevel_variables['agglevel']: + rmap = hierarchy[sw['GSw_HourlyClusterRegionLevel']] + elif agglevel_variables['agglevel'] in ['ba','aggreg']: + rmap = (hierarchy_orig.loc[hierarchy_orig['ba'].isin(val_r_all)] + [['aggreg',sw['GSw_HourlyClusterRegionLevel']]] + .drop_duplicates().set_index('aggreg')).squeeze(1) + + #%% Load supply curves to use for available capacity weighting + sc = { + tech: pd.read_csv( + os.path.join(inputs_case, f'supplycurve_{tech}.csv') + ).groupby(['region','class'], as_index=False).capacity.sum() + for tech in techs_vre + } + sc = ( + pd.concat(sc, names=['tech','drop'], axis=0) + .reset_index(level='drop', drop=True).reset_index()) + ### Downselect to modeled regions + sc = sc.loc[sc.region.isin(val_r_all)].copy() + sc['i'] = sc.tech+'_'+sc['class'].astype(str) + sc['resource'] = sc.i + '|' + sc.region + sc['aggreg'] = sc.region.map(rmap) + + #%%### Load RE CF data, then take available-capacity-weighted average by (tech,region) + print("Collecting 8760 capacity factor data") + recf_ra = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) + ### Downselect to techs used for rep-period selection + recf_ra = recf_ra[[c for c in recf_ra if any([c.startswith(p) for p in techs_vre])]].copy() + ### Multiply by available capacity for weighted average + recf_ra *= sc.set_index('resource')['capacity'] + ### Downselect to modeled years, add descriptive time index + recf = recf_ra.loc[recf_ra.index.year.isin(sw['GSw_HourlyWeatherYears'])] + recf.index = timestamps_myr.set_index(['year','yperiod','h_of_period']).index + recf_ra.index = timestamps.set_index(['year','yperiod','h_of_period']).index + + ### Identify outlying periods if using capacity credit instead of stress periods + if (int(sw.GSw_PRM_CapCredit) + and (sw['GSw_HourlyMinRElevel'].lower() not in ['false','none'])): + forceperiods_minre = { + tech: identify_min_periods( + df=recf, hierarchy=hierarchy, + level=sw['GSw_HourlyMinRElevel'], prefix=tech) + for tech in techs_min_vre + } + else: + forceperiods_minre = {tech: set() for tech in techs_min_vre} + + ### Aggregate to (tech,GSw_HourlyClusterRegionLevel) + recf_agg = recf.copy() + tmp = ( + pd.DataFrame({'resource':recf.columns}).set_index('resource') + .merge(sc.set_index('resource')[['tech','region']], left_index=True, right_index=True) + ) + columns = tmp.loc[tmp.index.isin(recf.columns)] + recf_agg = recf_agg[tmp.index] + columns['region'] = columns.region.map(rmap) + recf_agg.columns = pd.MultiIndex.from_frame(columns[['tech','region']]) + recf_agg = recf_agg.groupby(axis=1, level=['tech','region']).sum() + + ### Divide by aggregated capacity to get back to CF + recf_agg /= sc.groupby(['tech','aggreg']).capacity.sum().rename_axis(['tech','region']) + + ### Load load data (Eastern time) + print("Collecting 8760 load data") + load = get_load( + inputs_case=inputs_case, + keep_modelyear=(int(sw['GSw_HourlyClusterYear']) + if int(sw['GSw_HourlyClusterYear']) in modelyears + else max(modelyears)), + keep_weatheryears=sw.GSw_HourlyWeatherYears, + ) + ## Add descriptive index + load.index = timestamps_myr.set_index(['year','yperiod','h_of_period']).index + + ### Identify outlying periods if using capacity credit instead of stress periods + if (int(sw.GSw_PRM_CapCredit) + and (sw['GSw_HourlyPeakLevel'].lower() not in ['false','none']) + ): + forceperiods_load = identify_peak_containing_periods( + df=load, hierarchy=hierarchy, level=sw['GSw_HourlyPeakLevel']) + else: + forceperiods_load = set() + + ### Aggregate to GSw_HourlyClusterRegionLevel + load_agg = load.copy() + load_agg.columns = load_agg.columns.map(rmap) + load_agg = load_agg.groupby(axis=1, level=0).sum() + match sw.GSw_HourlyClusterLoadNorm: + case 'none': + ## Don't normalize load + pass + case 'regionmax': + ## Normalize each region to [0,1] + load_agg /= load_agg.max() + case 'maxmax': + ## Divide each region by largest regional max across all regions + load_agg /= load_agg.max().max() + case 'maxmin': + ## Divide each region by smallest regional max across all regions + load_agg /= load_agg.max().min() + case _: + ## Like 'maxmin' but scaled by the provided numeric value + load_agg /= load_agg.max().min() * float(sw.GSw_HourlyClusterLoadNorm) + + ### Get the full list of forced periods + forceperiods = forceperiods_load.copy() + for tech in forceperiods_minre: + forceperiods.update(forceperiods_minre[tech]) + ## Make a simpler list without the metadata to use for indexing below + ## (use list(set()) to drop duplicate force-periods) + forceperiods_yearperiod = list(set([(i[2], i[3]) for i in forceperiods])) + ### Add number of force-include periods to GSw_HourlyNumClusters for total number of periods + num_rep_periods = int(sw['GSw_HourlyNumClusters']) + len(forceperiods) + ### Record the force-included periods + print('representative periods: {}'.format(sw['GSw_HourlyNumClusters'])) + print('force-include periods: {}'.format(len(forceperiods))) + print(' peak-load periods: {}'.format(len(forceperiods_load))) + for tech in forceperiods_minre: + print(' min-{} periods: {}'.format(tech, len(forceperiods_minre[tech]))) + print('total periods: {}'.format(num_rep_periods)) + + + forceperiods_write = pd.DataFrame( + [['load'] + list(i) for i in forceperiods_load] + + [[k]+list(i) for k,v in forceperiods_minre.items() for i in v], + columns=['property','region','reason','year','yperiod'], + ) + forceperiods_write['szn'] = ( + 'y' + forceperiods_write.year.astype(str) + + ('d' if sw.GSw_HourlyType=='year' else sw.GSw_HourlyType[0]) + + forceperiods_write.yperiod.map('{:>03}'.format) + ) + forceperiods_write.drop_duplicates('szn', inplace=True) + + ### Package profiles into one dataframe + profiles = pd.concat({ + **{'load': load_agg}, + **{tech: recf_agg[tech] for tech in techs_vre if tech in recf_agg} + }, + axis=1, + names=('property', 'region'), + ).unstack('h_of_period') + + ### Drop forceperiods for clustering + profiles_fitperiods_hourly = profiles.loc[~profiles.index.isin(forceperiods_yearperiod)].copy() + ## Normalize the profiles if desired + if int(sw.GSw_HourlyNormProfiles): + profiles_fitperiods_hourly /= profiles_fitperiods_hourly.stack('h_of_period').max() + + ### Aggregate from hours to periods if necessary + if sw.GSw_HourlyClusterTimestep in ['period','day','wek','week']: + profiles_fitperiods = ( + profiles_fitperiods_hourly.groupby(axis=1, level=['property','region']).mean()) + else: + profiles_fitperiods = profiles_fitperiods_hourly.copy() + + #%% Plots + if int(sw.debug): + try: + hourly_plots.plot_unclustered_periods(profiles, sw, reeds_path, figpath) + except Exception as err: + print('plot_unclustered_periods failed with the following error:\n{}'.format(err)) + + try: + hourly_plots.plot_feature_scatter(profiles_fitperiods, reeds_path, figpath) + except Exception as err: + print('plot_feature_scatter failed with the following error:\n{}'.format(err)) + + + #%%### Determine representative periods + print("Identify and weight representative periods") + ## First weight the profiles + profiles_fitperiods_weighted = ( + profiles_fitperiods + .multiply(sw.GSw_HourlyClusterWeights, axis=1, level='property') + .dropna(axis=1, how='all') + ) + + ## Representative days or weeks + if sw['GSw_HourlyType'] in ['day','wek']: + rep_periods, period_szn = cluster_profiles( + profiles_fitperiods=profiles_fitperiods_weighted, + sw=sw, + forceperiods_yearperiod=forceperiods_yearperiod, + ) + print("Clustering complete") + + ## 8760 + elif sw['GSw_HourlyType']=='year': + ### For 8760 we use the original seasons + month2quarter = pd.read_csv( + os.path.join(inputs_case, 'month2quarter.csv'), + index_col='month', + ).squeeze(1).map(lambda x: x[:4]) + + period_szn = pd.Series( + index=timestamps_myr.drop_duplicates('yperiod').yperiod.values, + data=timestamps_myr.drop_duplicates('yperiod').index.month.map(month2quarter), + name='szn', + ).rename_axis('period') + + rep_periods = period_szn.index.tolist() + forceperiods_write = pd.DataFrame(columns=['property','region','reason','year','yperiod']) + + + #%%### Identify a (potentially different) collection of periods to use as initial stress periods + if ((not int(sw.GSw_PRM_CapCredit)) + and (sw['GSw_PRM_StressSeedMinRElevel'].lower() not in ['false','none']) + ): + stressperiods_minre = { + tech: identify_min_periods( + df=recf_ra, + hierarchy=hierarchy, + level=sw['GSw_PRM_StressSeedMinRElevel'], + prefix=tech, + ) + for tech in techs_min_vre} + else: + stressperiods_minre = {tech: set() for tech in techs_min_vre} + + if ((not int(sw.GSw_PRM_CapCredit)) + and (sw['GSw_PRM_StressSeedLoadLevel'].lower() not in ['false','none']) + ): + ## Get load for all model and weather years + load_allyears = get_load(inputs_case, keep_weatheryears='all').loc[modelyears] + ## Add descriptive index + load_allyears = load_allyears.merge( + timestamps[['year', 'yperiod', 'h_of_period']], left_on='datetime', right_index=True) + load_allyears = load_allyears.droplevel('datetime') + load_allyears.index.names = ['modelyear'] + load_allyears = load_allyears.set_index(['year', 'yperiod', 'h_of_period'], append=True) + stressperiods_load = { + y: identify_peak_containing_periods( + df=load_allyears.loc[y], hierarchy=hierarchy, + level=sw['GSw_PRM_StressSeedLoadLevel']) + for y in modelyears + } + else: + stressperiods_load = {y: set() for y in modelyears} + + ## Combine dicts of load and min-wind/solar stress periods into a dataframe with + ## (modelyear, property, region, reason) index and (weatheryear, period of year, szn) + ## values. + stressperiods_write = pd.concat( + {y: pd.DataFrame( + [['load'] + list(i) for i in stressperiods_load[y]] + + [[k]+list(i) for k,v in stressperiods_minre.items() for i in v], + columns=['property','region','reason','year','yperiod'] + ).drop_duplicates(subset=['year','yperiod']) + for y in modelyears}, + axis=0, names=['modelyear','index'], + ).reset_index(level='index', drop=True) + stressperiods_write['szn'] = ( + 'y' + stressperiods_write.year.astype(str) + + ('d' if sw.GSw_HourlyType=='year' else sw.GSw_HourlyType[0]) + + stressperiods_write.yperiod.map('{:>03}'.format) + ) + + + #%%### Get the representative and force periods + period_szn_write = period_szn.rename('season').reset_index() + if sw['GSw_HourlyType'] == 'year': + period_szn_write['year'] = sorted(sw['GSw_HourlyWeatherYears']*365) + period_szn_write['yperiod'] = period_szn_write.period + else: + period_szn_write['rep_period'] = period_szn_write['season'].copy() + period_szn_write['year'] = period_szn_write.period.map(lambda x: x[0]) + period_szn_write['yperiod'] = period_szn_write.period.map(lambda x: x[1]) + period_szn_write['actual_period'] = ( + 'y' + period_szn_write.year.astype(str) + + ('w' if sw.GSw_HourlyType == 'wek' else 'd') + + period_szn_write.yperiod.astype(str).map('{:>03}'.format) + ) + if sw['GSw_HourlyType'] == 'year': + period_szn_write['rep_period'] = period_szn_write['actual_period'].copy() + + + #%% Get some other convenience sets + timestamps_day = make_timestamps(sw=pd.Series({**sw, **{'GSw_HourlyType':'day'}})) + timestamps_wek = make_timestamps(sw=pd.Series({**sw, **{'GSw_HourlyType':'wek'}})) + ## Include all possible seasons so dispatch mode can be rerun with any of them + quarters = pd.read_csv( + os.path.join(inputs_case, 'sets', 'quarter.csv'), + header=None, + ).squeeze(1).tolist() + set_allszn = pd.Series( + list(timestamps_day.period.unique()) + + list(timestamps_wek.period.unique()) + + quarters + ) + ## Include stress periods + set_allszn = pd.concat([set_allszn, 's'+set_allszn]) + + set_allh = pd.concat([ + timestamps_day['timestamp'], + timestamps_wek['timestamp'], + 's'+timestamps_day['timestamp'], + 's'+timestamps_wek['timestamp'], + ]) + + set_actualszn = ( + period_szn_write['season'].drop_duplicates() if sw['GSw_HourlyType'] == 'year' + else period_szn_write['actual_period']) + + stress_period_szn = ( + stressperiods_write.assign(rep_period=stressperiods_write.szn) + [['rep_period','year','yperiod','szn']].rename(columns={'szn':'actual_period'}) + ) + + stressperiods_seed = ( + stressperiods_write + .assign(szn='s'+stressperiods_write.szn) + .reset_index().rename(columns={'modelyear':'t'}) + [['t','szn']] + ) + + + #%%### Plot some stuff + try: + hourly_plots.plot_ldc( + period_szn, profiles, rep_periods, + forceperiods_write, sw, reeds_path, figpath) + except Exception: + print('plot_ldc failed:') + print(traceback.format_exc()) + + if int(sw.debug): + try: + hourly_plots.plot_load_days(profiles, rep_periods, period_szn, sw, reeds_path, figpath) + except Exception: + print('plot_load_days failed:') + print(traceback.format_exc()) + + try: + hourly_plots.plot_8760(profiles, period_szn, sw, reeds_path, figpath) + except Exception: + print('plot_8760 failed:') + print(traceback.format_exc()) + + + #%%### Write the outputs + period_szn_write.drop('period', axis=1).to_csv( + os.path.join(inputs_case, periodtype, 'period_szn.csv'), index=False) + + if 'user' not in sw['GSw_HourlyClusterAlgorithm']: + forceperiods_write.to_csv( + os.path.join(inputs_case, periodtype, 'forceperiods.csv'), index=False) + + timestamps.to_csv( + os.path.join(inputs_case, periodtype, 'timestamps.csv'), index=False) + + set_actualszn.to_csv( + os.path.join(inputs_case, periodtype, 'set_actualszn.csv'), header=False, index=False) + + if minimal: + return period_szn_write + + #%% Write the sets over all possible periods (representative and stress) + set_allszn.to_csv( + os.path.join(inputs_case, 'set_allszn.csv'), header=False, index=False) + + set_allh.to_csv( + os.path.join(inputs_case, 'set_allh.csv'), header=False, index=False) + + #%% Write the seed stress periods to use for the PRM constraint + if 'user' in sw.GSw_PRM_StressModel: + stressperiods_seed = pd.read_csv(os.path.join(inputs_case, 'stressperiods_user.csv')) + stressperiods_seed.to_csv(os.path.join(inputs_case, 'stressperiods_seed.csv'), index=False) + _missing = [t for t in modelyears if t not in stressperiods_seed.t.unique()] + if len(_missing): + raise Exception(f"Missing user-defined stress periods for {','.join(map(str, _missing))}") + for t in modelyears: + ## Write the period_szn file + szns = stressperiods_seed.loc[stressperiods_seed.t==t, 'szn'].values + dfwrite = pd.DataFrame({ + 'rep_period': [i.strip('s') for i in szns], + 'year': [int(i.strip('sy')[:4]) for i in szns], + 'yperiod': [int(i[-3:]) for i in szns], + 'actual_period': [i.strip('s') for i in szns], + }) + os.makedirs(os.path.join(inputs_case, f'stress{t}i0'), exist_ok=True) + dfwrite.to_csv(os.path.join(inputs_case, f'stress{t}i0', 'period_szn.csv'), index=False) + else: + stressperiods_seed.to_csv(os.path.join(inputs_case, 'stressperiods_seed.csv'), index=False) + for t in modelyears: + os.makedirs(os.path.join(inputs_case, f'stress{t}i0'), exist_ok=True) + if stressperiods_write.empty: + pd.DataFrame(columns=['property','region','reason','year','yperiod','szn']).to_csv( + os.path.join(inputs_case, f'stress{t}i0', 'forceperiods.csv'), index=False) + else: + stressperiods_write.loc[[t]].to_csv( + os.path.join(inputs_case, f'stress{t}i0', 'forceperiods.csv'), index=False) + if stress_period_szn.empty: + pd.DataFrame(columns=['rep_period','year','yperiod','actual_period']).to_csv( + os.path.join(inputs_case, f'stress{t}i0', 'period_szn.csv'), index=False) + else: + stress_period_szn.loc[[t]].to_csv( + os.path.join(inputs_case, f'stress{t}i0', 'period_szn.csv'), index=False) + + return period_szn_write + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if __name__ == '__main__': + + #%% Parse arguments + parser = argparse.ArgumentParser( + description='Create the necessary 8760 and capacity factor data for hourly resolution') + parser.add_argument('reeds_path', help='ReEDS-2.0 directory') + parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + # #%% Settings for testing + # reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + # inputs_case = os.path.join( + # reeds_path,'runs', + # 'v20260411_itlM0_USA_faster','inputs_case','') + # interactive = True + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + print('Starting hourly_repperiods.py') + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + + ####################################### + #%% Identify the representative periods + main(sw=sw, reeds_path=reeds_path, inputs_case=inputs_case) + + #################################################### + #%% Write timeseries data for representative periods + hourly_writetimeseries.main( + sw=sw, reeds_path=reeds_path, inputs_case=inputs_case, + periodtype='rep', + make_plots=1, + ) + + ############################################ + #%% Write timeseries data for stress periods + modelyears = pd.read_csv( + os.path.join(inputs_case, 'modeledyears.csv')).columns.astype(int) + for t in modelyears: + print(f'Writing seed stress periods for {t}') + hourly_writetimeseries.main( + sw=sw, reeds_path=reeds_path, inputs_case=inputs_case, + periodtype=f'stress{t}i0', + make_plots=0, + ) + + #%% All done + reeds.log.toc(tic=tic, year=0, process='inputs/hourly_repperiods.py', + path=os.path.join(inputs_case,'..')) + print('Finished hourly_repperiods.py') diff --git a/reeds/input_processing/hourly_writetimeseries.py b/reeds/input_processing/hourly_writetimeseries.py new file mode 100644 index 00000000..1882269a --- /dev/null +++ b/reeds/input_processing/hourly_writetimeseries.py @@ -0,0 +1,1584 @@ +# %% =========================================================================== +### --- IMPORTS --- +### =========================================================================== +import os +import sys +import logging +import shutil +import datetime +import pandas as pd +import numpy as np +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +##% Time the operation of this script +tic = datetime.datetime.now() +## Turn off logging for imported packages +for i in ["matplotlib"]: + logging.getLogger(i).setLevel(logging.CRITICAL) + + +# %%################# +### FIXED INPUTS ### +decimals = 3 +### Indicate whether to show plots interactively [default False] +interactive = False +### Indicate whether to save the old h17 inputs for comparison +debug = True + + +# %% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== +def make_8760_map(period_szn, sw): + """ + """ + hoursperperiod = {'day':24, 'wek':120, 'year':24}[sw['GSw_HourlyType']] + periodsperyear = {'day':365, 'wek':73, 'year':365}[sw['GSw_HourlyType']] + fulltimeindex = reeds.timeseries.get_timeindex(sw.resource_adequacy_years_list) + ### Start with all weather years + hmap_allyrs = pd.DataFrame({ + 'timestamp': fulltimeindex, + 'year': np.ravel([[y]*8760 for y in sw.resource_adequacy_years_list]), + 'yearperiod': np.ravel([ + [h+1 for d in range(365) for h in (d,)*24] if sw['GSw_HourlyType'] == 'year' + else [h+1 for d in range(periodsperyear) + for h in (d,)*hoursperperiod] + for y in sw.resource_adequacy_years_list]), + 'hour': range(1, 8760*len(sw.resource_adequacy_years_list) + 1), + 'hour0': range(8760*len(sw.resource_adequacy_years_list)), + 'yearhour': np.ravel(list(range(1,8761))*len(sw.resource_adequacy_years_list)), + 'periodhour': ( + list(range(1,25))*365*len(sw.resource_adequacy_years_list) + if sw['GSw_HourlyType'] == 'year' + else ( + list(range(1, hoursperperiod+1)) + * periodsperyear + * len(sw.resource_adequacy_years_list)) + ), + }) + hmap_allyrs['actual_period'] = ( + 'y' + hmap_allyrs.year.astype(str) + + ('w' if sw.GSw_HourlyType == 'wek' else 'd') + + hmap_allyrs.yearperiod.astype(str).map('{:>03}'.format) + ) + hmap_allyrs['actual_h'] = ( + hmap_allyrs.actual_period + + 'h' + hmap_allyrs.periodhour.astype(str).map('{:>03}'.format) + ) + hmap_allyrs['season'] = hmap_allyrs.actual_period.map( + period_szn.set_index('actual_period').season) + hmap_allyrs['month'] = hmap_allyrs.timestamp.dt.strftime('%b').str.upper() + ### create the timestamp index: y{20xx}d{xxx}h{xx} (left-padded with 0) + if sw["GSw_HourlyType"] == "year": + ### If using a chronological year (i.e. 8760) the day index uses actual days + hmap_allyrs['h'] = ( + 'y' + hmap_allyrs.year.astype(str) + + 'd' + hmap_allyrs.yearperiod.astype(str).map('{:>03}'.format) + + 'h' + hmap_allyrs.periodhour.astype(str).map('{:>03}'.format) + ) + else: + ### If using representative periods (days/weks) the period index uses + ### representative periods, which are in the 'season' column + hmap_allyrs['h'] = ( + hmap_allyrs.season + + 'h' + hmap_allyrs.periodhour.astype(str).map('{:>03}'.format) + ) + ### hmap_myr (for "model years") only contains the actually-modeled periods + hmap_myr = hmap_allyrs.dropna(subset=['season']).copy() + + return hmap_allyrs, hmap_myr + + +def get_ccseason_peaks_hourly(load, sw, inputs_case, hierarchy, h2ccseason, val_r_all): + ### Aggregate demand by GSw_PRM_hierarchy_level + if sw["GSw_PRM_hierarchy_level"] == "r": + rmap = pd.Series(hierarchy.index, index=hierarchy.index) + else: + rmap = hierarchy[sw['GSw_PRM_hierarchy_level']] + load_agg = ( + load.assign(region=load.r.map(rmap)) + .groupby(["h", "region"]) + .MW.sum() + .unstack("region") + .reset_index() + ) + ### Get the peak hour by ccseason for aggregated load + load_agg["ccseason"] = load_agg.h.map(h2ccseason) + # Get the peak hours for the aggregated region associated with GSw_PRM_hierarchy_level + peakhour_agg_byccseason = load_agg.set_index("h").groupby("ccseason").idxmax() + ### Get the BA/region resolution demand during the peak hour of the associated GSw_PRM_hierarchy_level + + peak_out = {} + + # Determination of season peaks: We merge the peak_agg_byccseason dataframe to rmap and load. + # By changing the peak_agg_byccseason to long format we are able to merge based on the aggregated GSw_PRM_hierarchy_level region + # Then by merging the resultant dataframe to load based on 'r' and peak hour 'h', + # we get the peak hours for each region of interest at the desired region resolution by ccseason. + peak_out = ( + peakhour_agg_byccseason.unstack() + .rename("h") + .reset_index() + .merge(rmap.rename("region").reset_index(), on="region") + .merge(load, on=["r", "h"], how="left")[["r", "ccseason", "MW"]] + ) + + return peak_out + + +def append_csp_profiles(cf_rep, sw): + ### Parse switch data (hourly_repperiods.py does this already but stress_periods.py does not) + if isinstance(sw["GSw_CSP_Types"], str): + sw["GSw_CSP_Types"] = [int(i) for i in sw["GSw_CSP_Types"].split("_")] + ### Get the CSP profiles + cfcsp = cf_rep[[c for c in cf_rep if c.startswith("csp")]].copy() + ### As in cfgather.py, we duplicate the csp1 profiles for each CSP tech + cfcsp_out = pd.concat( + ( + [ + cfcsp.rename( + columns={c: c.replace("csp", f"csp{i}") for c in cfcsp.columns} + ) + for i in sw["GSw_CSP_Types"] + ] + ), + axis=1, + ) + ### Drop the original 'csp' profiles and append the duplicated CSP profiles to rest of cf's + cf_combined = pd.concat( + [cf_rep[[c for c in cf_rep if not c.startswith("csp")]], cfcsp_out], axis=1 + ) + + return cf_combined + + +def get_minloading_windows(sw, h_szn, chunkmap): + """ + Create combinations of h's within GSw_HourlyWindow of each other, + beginning every GSw_HourlyWindowOverlap + """ + ### Inputs for testing + # sw['GSw_HourlyWindow'] = 2 + # sw['GSw_HourlyWindowOverlap'] = 1 + h_szn_chunked = h_szn.assign(h=h_szn.h.map(chunkmap)).drop_duplicates() + seasons = h_szn_chunked.season.unique() + hour_szn_group = set() + for season in seasons: + ## 2 copies so we can loop around the end + timeslices = h_szn_chunked.loc[h_szn_chunked.season == season, "h"].tolist() * 2 + numslices = len(set(timeslices)) + all_combos = [ + (t1, t2) + for (i, t1) in enumerate(timeslices) + for (j, t2) in enumerate(timeslices) + if ( + ## Drop duplicates + (i != j) + ## Must be within GSw_HourlyWindow steps of each other + and (abs(i - j) < int(sw["GSw_HourlyWindow"])) + ## First index must be in the first pass + and (i <= numslices) + ## Only keep the windows that start from overlaps that are kept + and not (i % (int(sw["GSw_HourlyWindowOverlap"]) + 1)) + ) + ] + ### Add both polarities + hour_szn_group.update(all_combos) + hour_szn_group.update([(j, i) for (i, j) in all_combos]) + + ### Format as dataframe and return + hour_szn_group = pd.DataFrame(hour_szn_group, columns=["h", "hh"]).sort_values( + ["h", "hh"] + ) + + return hour_szn_group + + +def get_yearly_demand(sw, hmap_myr, hmap_allyrs, inputs_case, periodtype='rep'): + """ + After clustering based on GSw_HourlyClusterYear and identifying the modeled days, + reload the raw demand and extract the demand on the modeled days for each year. + """ + ### Get original demand data, subset to cluster year + load_in = reeds.io.read_file( + os.path.join(inputs_case,'load.h5'), parse_timestamps=True).unstack(level=0) + load_in.columns = load_in.columns.rename(['r','t']) + ### load.h5 is busbar load, but b_inputs.gms ingests end-use load, so scale down by distloss + scalars = reeds.io.get_scalars(inputs_case) + load_in *= (1 - scalars['distloss']) + + ### Add time index + load_in.index = load_in.index.map(hmap_allyrs.set_index('timestamp')['actual_h']).rename('h') + + load_out = load_in.copy() + ### For full year, keep all periods in the modeled years + if (sw.GSw_HourlyType == 'year') and (periodtype == 'rep'): + load_out = load_out.loc[ + load_out.index.map(hmap_allyrs.set_index('actual_h').year) + .isin(sw['GSw_HourlyWeatherYears']) + ].copy() + ### Otherwise, pull out the specified periods + else: + load_out = load_out.loc[hmap_myr.h.unique()].copy() + + ### Reshape for ReEDS + load_out = load_out.stack("r").reorder_levels(["r", "h"], axis=0).sort_index() + + return load_in, load_out + +def format_climate_inputs(filename, inputs_case, szn_month_weights): + """ + This function converts climate data from monthly to repperiod resolution using the + szn_month_weights + """ + climate_index = { + 'temp_hydadjsea': ['r','season','t'], + 'temp_UnappWaterMult': ['wst','r','season','t'], + 'temp_UnappWaterSeaAnnDistr': ['wst','r','season','t'] + } + + df = pd.read_csv(os.path.join(inputs_case,filename+'.csv')) + df_out = szn_month_weights.merge(df, on='month', how='outer') + df_out['value'] = df_out['weight'] * df_out['Value'] + df_out = ( + df_out + .groupby(climate_index[filename]).agg({'value':'sum'}) + .value + ## For rep periods, sum of season weights is 1, so the next line has no effect. + ## For full chronological year (GSw_HourlyType=year), we use four seasons, + ## so the sum of season weights is the number of months in that season and + ## we need to divide sum{cf*weight} by sum{weight}. + / szn_month_weights.groupby('season').weight.sum() + ).rename('value').reset_index().rename(columns={'season':'szn'}) + # Convert to GAMS-readable wide format + climate_index = [x if x != 'season' else 'szn' for x in climate_index[filename]] + climate_index = [x for x in climate_index if x != 't'] + df_out = df_out.pivot_table(index=climate_index, columns='t', values='value') + + return df_out + +def get_yearly_flexibility( + sw, + period_szn, + rep_periods, + hmap_1yr, + set_szn, + inputs_case, + drcat, +): + """ + After clustering based on GSw_HourlyClusterYear and identifying the modeled days, + reload the raw flexible DR or EV profiles and extract for the modeled days of each year + """ + hoursperperiod = {"day": 24, "wek": 120, "year": np.nan}[sw["GSw_HourlyType"]] + ### Get the set of szn's and h's + szn_h = ( + hmap_1yr.drop_duplicates(["h", "season"]) + .sort_values(["season", "hour"]) + .reset_index(drop=True)[["season", "h"]] + .assign( + periodhour=np.ravel( + ( + [range(1, 25)] * 365 + if sw["GSw_HourlyType"] == "year" + else [range(1, hoursperperiod + 1)] * len(set_szn) + ) + ) + ) + .set_index(["season", "periodhour"]) + .h + ).copy() + + idx_vals = [i + 1 for i in period_szn.index.values] + period_szn_dict = period_szn.set_index("yperiod").to_dict()["season"] + + ### Original flexibility data + shape = {} + shape_out = {} + + for stype in ["increase", "decrease", "energy"]: + if stype == "energy": + if drcat.lower() == "evmc_storage": + shape[stype] = pd.read_csv( + os.path.join(inputs_case, f"evmc_storage_{stype}.csv") + ) + else: + continue + elif drcat.lower() == "evmc_shape": + shape[stype] = pd.read_csv( + os.path.join(inputs_case, f"evmc_shape_profile_{stype}.csv") + ) + elif drcat.lower() == "evmc_storage": + shape[stype] = pd.read_csv( + os.path.join(inputs_case, f"evmc_storage_profile_{stype}.csv") + ) + else: + raise ValueError( + f"drcat must be in ['dr','evmc_shape','evmc_storage'] but is '{drcat}'" + ) + + unique_techs = len(shape[stype].i.unique()) + unique_years = len(shape[stype].year.unique()) + ### Add time indices ("season" is the identifier for modeled periods) + shape[stype]["yperiod"] = ( + np.ravel([[d] * 24 for d in range(1, 366)] * unique_techs * unique_years) + if sw["GSw_HourlyType"] == "year" + else np.ravel( + [[d] * hoursperperiod for d in idx_vals] * unique_techs * unique_years + ) + ) + shape[stype]["periodhour"] = ( + np.ravel([range(1, 25) for d in range(365)] * unique_techs * unique_years) + if sw["GSw_HourlyType"] == "year" + else np.ravel( + [range(1, hoursperperiod + 1) for d in idx_vals] + * unique_techs + * unique_years + ) + ) + shape[stype]["season"] = shape[stype].yperiod.map(period_szn_dict) + + ### If modeling a full year, keep everything + if sw["GSw_HourlyType"] == "year": + shape_out[stype] = shape[stype].drop( + ["yperiod", "periodhour", "season"], axis=1 + ) + shape_out[stype].index = hmap_1yr.h + ### If using representative periods, pull out the representative periods + elif unique_years == 1: + shape_out[stype] = ( + shape[stype] + .loc[shape[stype].season.isin(rep_periods)] + .drop(["yperiod", "hour", "year"], axis=1) + .set_index(["season", "periodhour"]) + .sort_index() + ) + shape_out[stype].index = shape_out[stype].index.map(szn_h).rename("h") + shape_out[stype] = ( + shape_out[stype] + .reset_index() + .set_index(["h", "i"]) + .stack() + .reset_index() + .rename(columns={"i": "*i", "level_2": "r", 0: "Values"})[ + ["*i", "r", "h", "Values"] + ] + ) + else: + shape_out[stype] = ( + shape[stype] + .loc[shape[stype].season.isin(rep_periods)] + .drop(["yperiod", "hour"], axis=1) + .set_index(["season", "periodhour"]) + .sort_index() + ) + + shape_out[stype].index = shape_out[stype].index.map(szn_h).rename("h") + shape_out[stype] = ( + shape_out[stype] + .reset_index() + .set_index(["h", "i", "year"]) + .stack() + .reset_index() + .rename(columns={"i": "*i", "level_3": "r", "year": "t", 0: "Values"})[ + ["*i", "r", "h", "t", "Values"] + ] + ) + + if "energy" in shape.keys(): + return shape_out["decrease"], shape_out["increase"], shape_out["energy"] + else: + return shape_out["decrease"], shape_out["increase"] + + +# %% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== +def main(sw, reeds_path, inputs_case, periodtype='rep', make_plots=1, logging=True): + """ """ + # #%% Settings for testing + # reeds_path = os.path.realpath(os.path.join(os.path.dirname(__file__),'..')) + # inputs_case = os.path.join(reeds_path, 'runs', 'v20250313_chunkM0_Pacific_r4mean_s4max', 'inputs_case') + # sw = reeds.io.get_switches(inputs_case) + # periodtype = 'stress2010i0' + # periodtype = 'rep' + # make_plots = 0 + + #%% Set up logger + if logging: + _log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + # %% Parse some switches + if not isinstance(sw["GSw_HourlyWeatherYears"], list): + sw["GSw_HourlyWeatherYears"] = [ + int(y) for y in sw["GSw_HourlyWeatherYears"].split("_") + ] + + # Ensure the GSw_CSP_Types is a list, as hourly_writetimeseries is called in F_stress_periods.py as well + if not isinstance(sw['GSw_CSP_Types'],list): + sw['GSw_CSP_Types'] = [int(i) for i in sw['GSw_CSP_Types'].split('_')] + ## Make outputs path + outpath = os.path.join(inputs_case, periodtype) + os.makedirs(outpath, exist_ok=True) + ## Designate prefix for timestamps + tprefix = 's' if periodtype.startswith('stress') else '' + + # %%### Load shared files + val_r_all = ( + pd.read_csv(os.path.join(inputs_case, "val_r_all.csv"), header=None) + .squeeze(1) + .tolist() + ) + hierarchy = ( + pd.read_csv(os.path.join(inputs_case, "hierarchy.csv")) + .rename(columns={"*r": "r"}) + .set_index("r") + ) + + #%%### Load period-szn map, get representative and stress periods + period_szn = pd.read_csv(os.path.join(outpath, 'period_szn.csv')) + try: + forceperiods = pd.read_csv(os.path.join(outpath, 'forceperiods.csv')) + except FileNotFoundError: + forceperiods = pd.DataFrame( + columns=["property", "region", "reason", "year", "yperiod", "szn"] + ) + ### Strip off prefix to start with a fresh slate + for col in ["rep_period", "actual_period"]: + period_szn[col] = period_szn[col].str.strip(tprefix) + if len(forceperiods): + for col in ["szn"]: + forceperiods[col] = forceperiods[col].str.strip(tprefix) + if "season" not in period_szn: + period_szn["season"] = period_szn["rep_period"].copy() + + # %%### If there are no periods, write empty dataframes and stop here + if not len(period_szn): + write = { + 'set_h': ['*h'], + 'set_szn': ['*szn'], + 'h_preh': ['*h','preh'], + 'szn_actualszn': ['*season', 'actual_period'], + 'numpartitions': ['*actual_period', 'next_actual_period'], + 'nextpartition': ['*actual_period', 'next_actual_period'], + 'h_szn': ['*h','season'], + 'h_dt_szn': ['h','season','ccseason','year','hour'], + 'numhours': ['*h','numhours'], + 'nexth': ['*h','h'], + 'frac_h_ccseason_weights': ['*h','ccseason','weight'], + 'frac_h_quarter_weights': ['*h','quarter','weight'], + 'h_szn_start': ['*season','h'], + 'h_szn_end': ['*season','h'], + 'hour_szn_group': ['*h','hh'], + 'opres_periods': ['*szn'], + 'h_ccseason_prm': ['*h','ccseason'], + 'load_allyear': ['*r','h','t','MW'], + 'peak_ccseason': ['*r','ccseason','t','MW'], + 'cf_vre': ['*i','r','h','cf'], + 'cf_hyd': ['*i','szn','r','t','cf'], + 'cap_hyd_szn_adj': ['*i','szn','r','value'], + 'can_exports_h_frac': ['*h','frac_weighted'], + 'can_imports_szn_frac': ['*szn','frac_weighted'], + 'period_weights': ['*szn','rep_period'], + 'hmap_myr': ['*timestamp', 'year', 'yearperiod', 'hour', 'hour0', 'yearhour', + 'periodhour', 'actual_period', 'actual_h', 'season', 'month', 'h'], + 'periodmap_1yr': ['*actual_period','season'], + 'canmexload': ['*r','h'], + 'outage_forced_h': ['*i','r','h'], + 'outage_scheduled_h': ['*i','h'], + 'dr_shed_out': ['*i','r','h'], + 'evmc_baseline_load': ['r','h','t'], + 'evmc_shape_generation': ['*i','r','h'], + 'evmc_shape_load': ['*i','r','h'], + 'evmc_storage_discharge': ['*i','r','h','t'], + 'evmc_storage_charge': ['*i','r','h','t'], + 'evmc_storage_energy': ['*i','r','h','t'], + 'flex_frac_all': ['*flex_type','r','h','t'], + 'peak_h': ['*r','h','t','MW'], + } + for f, columns in write.items(): + pd.DataFrame(columns=columns).to_csv( + os.path.join(outpath, f+'.csv'), index=False) + + return write + + + #%%### Process the representative period weights + #%% Generate map from actual to representative periods + hmap_allyrs, hmap_myr = make_8760_map(period_szn=period_szn, sw=sw) + ### Add prefix if necessary + if tprefix: + for col in ["actual_period", "actual_h", "season", "h"]: + hmap_myr[col] = tprefix + hmap_myr[col] + hmap_allyrs[col] = tprefix + hmap_allyrs[col] + for col in ['rep_period','actual_period']: + period_szn[col] = tprefix + period_szn[col] + if not ( + (sw['GSw_HourlyType'] == 'year') + and ((periodtype == 'rep') or periodtype.startswith('pcm')) + ): + period_szn['season'] = tprefix + period_szn['season'] + if len(forceperiods): + for col in ["szn"]: + forceperiods[col] = tprefix + forceperiods[col] + + ### Add ccseasons + ccseason_dates = pd.read_csv( + os.path.join(inputs_case, 'ccseason_dates.csv'), + index_col=['month','day'], + ).squeeze(1) + hmap_allyrs['ccseason'] = hmap_allyrs.timestamp.map(lambda x: ccseason_dates[x.month, x.day]) + + #%%### Load full hourly RE CF, for downselection below + #%% VRE + recf = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) + ### Overwrite CSP CF (which in recf.h5 is post-storage) with solar field CF + cspcf = reeds.io.read_file(os.path.join(inputs_case, 'csp.h5'), parse_timestamps=True) + recf = ( + recf.drop([c for c in recf if c.startswith('csp')], axis=1) + .merge(cspcf, left_index=True, right_index=True) + ).loc[hmap_allyrs.timestamp] + recf.index = hmap_allyrs.actual_h + + # %% Get data for representative periods + rep_periods = sorted(period_szn.rep_period.unique()) + + ### Broadcast CSP values for all techs + cf_rep = recf.loc[ + recf.index.map(lambda x: any([x.startswith(i) for i in rep_periods])) + ] + + if int(sw["GSw_CSP"]) != 0: + cf_rep = append_csp_profiles(cf_rep=cf_rep, sw=sw) + + cf_out = cf_rep.rename_axis("h").copy() + i = cf_rep.columns.map(lambda x: x.split("|")[0]) + r = cf_rep.columns.map(lambda x: x.split("|")[1]) + cf_out.columns = pd.MultiIndex.from_arrays([i, r], names=["i", "r"]) + cf_out = ( + cf_out.stack(["i", "r"]) + .reorder_levels(["i", "r", "h"]) + .rename("cf") + .reset_index() + ) + + # %%### Create the temporal sets used by ReEDS + ### Calculate number of hours represented by each timeslice + hours = ( + hmap_myr.groupby('h').season.count().rename('numhours') + / (len(sw['GSw_HourlyWeatherYears']) if not periodtype.startswith('stress') else 1)) + ## Stress period hours are scaled to sum to 6 hours, making 8766 hours (365.25 days) per year + if periodtype.startswith('stress'): + hours = hours / hours.sum() * 6 + ### Make sure it lines up + if not periodtype.startswith('stress'): + assert int(np.around(hours.sum(), 0)) % 8760 == 0 + else: + assert np.around(hours.sum(), 0) == 6 + + # create the timeslice-to-season and timeslice-to-ccseason mappings + h_szn = hmap_myr[['h','season']].drop_duplicates().reset_index(drop=True) + h_ccseason = hmap_allyrs[['h','ccseason']].drop_duplicates().reset_index(drop=True) + + ### create the set of szn's modeled in ReEDS + set_szn = pd.DataFrame({"szn": period_szn.season.sort_values().unique()}) + + ### create the set of timeslicess modeled in ReEDS + hset = h_szn.h.sort_values().reset_index(drop=True) + + ### List of periods in which to apply operating reserve constraints + if (not periodtype.startswith('stress')) and ('user' in sw['GSw_HourlyClusterAlgorithm']): + period_szn_user = pd.read_csv(os.path.join(inputs_case, 'period_szn_user.csv')) + opres_periods = period_szn_user.loc[ + ~period_szn_user.opres.isnull() + ].rep_period.drop_duplicates().rename('szn').to_frame() + elif (sw["GSw_OpResPeriods"] == "all") or (sw["GSw_HourlyType"] == "year"): + opres_periods = set_szn + elif sw["GSw_OpResPeriods"] == "representative": + opres_periods = set_szn.loc[~set_szn.szn.isin(forceperiods.szn)] + elif sw["GSw_OpResPeriods"] == "stress": + opres_periods = set_szn.loc[set_szn.szn.isin(forceperiods.szn)] + elif sw["GSw_OpResPeriods"] in ["peakload", "peak_load", "peak", "load", "demand"]: + opres_periods = set_szn.loc[ + set_szn.szn.isin(forceperiods.loc[forceperiods.property == "load"].szn) + ] + elif sw["GSw_OpResPeriods"] in ["minre", "re", "vre", "min_re"]: + opres_periods = set_szn.loc[ + set_szn.szn.isin(forceperiods.loc[forceperiods.property != "load"].szn) + ] + + ### Calculate the fraction of each h associated with each ReEDS quarter, for compatibility + ### with model inputs that are defined by quarter + ## Get a map from hour-of-year to ReEDS quarter + month2quarter = pd.read_csv( + os.path.join(inputs_case, 'month2quarter.csv'), + index_col='month', + ).squeeze(1) + + quarters = ( + hmap_allyrs.iloc[:8760] + .set_index('hour') + .timestamp.dt.month.map(month2quarter) + .rename('quarter') + .map(lambda x: x[:4]) + ) + + ccseasons = ( + hmap_allyrs.iloc[:8760] + .set_index('hour').ccseason + ) + + # %% Calculate the fraction of hours of each timeslice associated with each quarter + frac_h_weights = {} + if not periodtype.startswith('stress'): + for season in ['quarter','ccseason']: + frac_h_weights[season] = hmap_myr.copy() + frac_h_weights[season][season] = hmap_myr.yearhour.map( + {"quarter": quarters, "ccseason": ccseasons}[season] + ) + frac_h_weights[season] = ( + ## Count the number of days in each szn that are part of each quarter + frac_h_weights[season].groupby(["h", season])["season"].count() + ## Normalize by the total number of hours per timeslice + / hours + / len(sw["GSw_HourlyWeatherYears"]) + ).rename("weight") + frac_h_weights[season] = frac_h_weights[season].reset_index() + else: + for season in ['quarter','ccseason']: + frac_h_weights[season] = hmap_allyrs.copy() + frac_h_weights[season][season] = hmap_allyrs.yearhour.map( + {'quarter':quarters, 'ccseason':ccseasons}[season]) + frac_h_weights[season] = ( + ( + frac_h_weights[season] + .groupby(["actual_h", season]) + .actual_period.count() + ) + .rename_axis(["h", season]) + .rename("weight") + .reset_index() + ) + + ### Make sure it lines up + for season in ["quarter", "ccseason"]: + assert (frac_h_weights[season].groupby("h").weight.sum().round(5) == 1).all() + + ### Calculate the fraction of hours of each h associated with each calendar month, + ### for compatibility with model inputs that are defined by quarter + # Get a map from hour-of-year to ReEDS month + months = hmap_allyrs.iloc[:8760].set_index('hour').month + + if not periodtype.startswith('stress'): + frac_h_month_weights = hmap_myr.copy() + frac_h_month_weights["month"] = hmap_myr.yearhour.map(months) + frac_h_month_weights = ( + ## Count the number of days in each szn that are part of each month + frac_h_month_weights.groupby(["h", "month"]).season.count() + ## Normalize by the total number of hours per timeslice + / hours + / len(sw["GSw_HourlyWeatherYears"]) + ).rename("weight") + frac_h_month_weights = frac_h_month_weights.reset_index() + else: + frac_h_month_weights = hmap_allyrs.copy() + frac_h_month_weights['month'] = hmap_allyrs.yearhour.map(months) + frac_h_month_weights = ( + (frac_h_month_weights.groupby(["actual_h", "month"]).actual_period.count()) + .rename_axis(["h", "month"]) + .rename("weight") + .reset_index() + ) + + ### Make sure it lines up + assert (frac_h_month_weights.groupby("h").weight.sum().round(5) == 1).all() + + # %%### Seasonal Canadian imports/exports for GSw_Canada=1 + # %% Exports: Spread equally over hours by quarter. + can_exports_szn_frac = pd.read_csv( + os.path.join(inputs_case, "can_exports_szn_frac.csv"), + header=0, + names=["season", "frac"], + index_col="season", + ).squeeze(1) + df = ( + frac_h_weights["quarter"] + .astype({"h": "str", "quarter": "str"}) + .replace({"weight": {0: np.nan}}) + .dropna(subset=["weight"]) + .copy() + ) + df = ( + df.assign(frac_exports=df.quarter.map(can_exports_szn_frac)) + .assign(season=df.h.map(h_szn.set_index("h").season)) + .assign(hours=df.h.map(hours)) + ) + df["quarter_hours"] = df.hours * df.weight + df["hours_per_quarter"] = df.quarter.map(quarters.value_counts()) + df["frac_weighted"] = df.frac_exports * df.quarter_hours / df.hours_per_quarter + can_exports_h_frac = df.groupby("h", as_index=False).frac_weighted.sum() + ### Make sure it sums to 1 + if not periodtype.startswith('stress'): + assert can_exports_h_frac.frac_weighted.sum().round(5) == 1 + + # %% Imports: Spread over seasons by quarter. + can_imports_quarter_frac = pd.read_csv( + os.path.join(inputs_case, "can_imports_quarter_frac.csv"), + header=0, + names=["season", "frac"], + index_col="season", + ).squeeze(1) + df = hmap_myr.assign(quarter=hmap_myr.yearhour.map(quarters)) + hours_per_quarter = df["quarter"].value_counts() + ## Fraction of quarter made up by each season (typically rep period) + quarter_season_weights = ( + df.groupby(["quarter", "season"]) + .year.count() + .divide(hours_per_quarter, axis=0, level="quarter") + ) + can_imports_szn_frac = ( + quarter_season_weights.multiply(can_imports_quarter_frac, level="quarter") + .groupby("season") + .sum() + .rename("frac_weighted") + .reset_index() + .rename(columns={"season": "szn"}) + ) + ### Make sure it sums to 1 + if not periodtype.startswith('stress'): + assert can_imports_szn_frac.frac_weighted.sum().round(5) == 1 + + ################################################## + # -- Hour, Region, and Timezone Mapping -- # + ################################################## + + period_weights = ( + (period_szn.rep_period.value_counts() / len(sw["GSw_HourlyWeatherYears"])) + .reset_index() + .rename(columns={"index": "szn", "szn": "weight"}) + ) + + ###### Mapping from hourly resolution to GSw_HourlyChunkLength resolution + ### Aggregation is performed as an average over the hours to be aggregated + ### For simplicity, midnight is always a boundary between chunks + + ### First make sure the number of hours is divisible by the chunk length + GSw_HourlyChunkLength = int( + sw[f"GSw_HourlyChunkLength{'Stress' if periodtype.startswith('stress') else 'Rep'}"]) + assert not len(hset) % GSw_HourlyChunkLength, ( + "Hours are not divisible by chunk length:" + "\nlen(hset) = {}\nGSw_HourlyChunkLength = {}" + ).format(len(hset), GSw_HourlyChunkLength) + + ### Map hours to chunks. Chunks are formatted as hour-ending. + ## If GSw_HourlyChunkLength == 2, + ## h1-h2-h3-h4-h5-h6 is mapped to h2-h2-h4-h4-h6-h6. + outchunks = hmap_myr.actual_h[GSw_HourlyChunkLength - 1 :: GSw_HourlyChunkLength] + chunkmap = dict( + zip( + hmap_myr.actual_h.values, + np.ravel([[c] * GSw_HourlyChunkLength for c in outchunks]), + ) + ) + + outchunks_allyrs = hmap_allyrs.actual_h[GSw_HourlyChunkLength-1::GSw_HourlyChunkLength] + chunkmap_allyrs = dict(zip( + hmap_allyrs.actual_h.values, + np.ravel([[c]*GSw_HourlyChunkLength for c in outchunks_allyrs]) + )) + + # %%### h_dt_szn for Augur + if not len(hmap_myr) % 8760: + ## Important: When modeling a single weather year, rep periods in the + ## h_dt_szn table are just the single-year periods concatenated n times. + ## In that case electrolyzer demand (optimized for GSw_HourlyWeatherYears, usually + ## 2012) won't line up with optimal operation times (low demand / high wind/solar) + ## outside of the single reprsentative year. + h_dt_szn = pd.concat( + {y: hmap_myr.drop_duplicates(['yearhour']).drop('year', axis=1) + for y in sw.resource_adequacy_years_list}, names=('year',), + axis=0, + ).reset_index(level='year').reset_index(drop=True) + h_dt_szn['ccseason'] = h_dt_szn.timestamp.map(lambda x: ccseason_dates[x.month, x.day]) + h_dt_szn['hour0'] = h_dt_szn.index + h_dt_szn['hour'] = h_dt_szn['hour0'] + 1 + for col in ['actual_period', 'actual_h']: + h_dt_szn[col] = 'y' + h_dt_szn.year.astype(str) + h_dt_szn[col].str[5:] + ## If hmap_myr contains less than a full year (e.g. for stress periods), + ## just use hmap_myr as-is. + else: + h_dt_szn = hmap_myr.copy() + h_dt_szn['ccseason'] = h_dt_szn.actual_h.map(hmap_allyrs.set_index('actual_h').ccseason) + + ################################################ + # -- Season starting and ending hours -- # + ################################################ + + ### Start hour is the lowest-numbered h in each season + ## End up with a series mapping season to start hour: {'szn1':'h1', ...} + szn2starth = hmap_myr.drop_duplicates("season", keep="first").set_index("season").h + + ### End hour is the highest-numbered h in each season + szn2endh = hmap_myr.drop_duplicates("season", keep="last").set_index("season").h + + ### next timeslice + nexth_actualszn = ( + hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) + [[ + ( + 'season' + if ((sw.GSw_HourlyType == 'year') and (not periodtype.startswith('stress'))) + else 'actual_period' + ), + 'h', + ]] + .drop_duplicates() + .rename(columns={"actual_period": "allszn", "season": "allszn"}) + ).copy() + ## Roll to make a lookup table for GAMS + nexth_actualszn["allsznn"] = np.roll(nexth_actualszn["allszn"], -1) + nexth_actualszn["hh"] = np.roll(nexth_actualszn["h"], -1) + + ### h-to-actual-period mapping for inter-period storage + h_actualszn = ( + hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) + [[ + 'h', + ( + 'season' + if ((sw.GSw_HourlyType == 'year') and (not periodtype.startswith('stress'))) + else 'actual_period' + ) + ]] + .drop_duplicates()) + + ### The following four sets are used for the inter-day linkage constraints for energy storage + ### Inter-day linkage only applicable to rep day and wek scenarios, so we only need to calculate these sets for + ### rep day and wek scenarios, otherwise they are empty + if sw.GSw_HourlyType in ['day', 'wek']: + ### Write rep period to actual period mapping set for inter-day storage linkage + szn_actualszn = ( + hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) + [['season', 'actual_period']] + .drop_duplicates()) + + ### Group actual period to partitions and count number of partitions of for each actual period + numpartitions = ( + hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) + [['season', 'actual_period']] + .drop_duplicates()).copy() + + if 'actual_period' not in numpartitions.columns: + numpartitions['actual_period'] = numpartitions['season'] + + numpartitions['partition'] = ( + numpartitions['season'] != numpartitions['season'].shift() + ).cumsum() + + count_partition = numpartitions.groupby('partition').size() + numpartitions['partition_count'] = numpartitions['partition'].map(count_partition) + + numpartitions = ( + numpartitions.drop_duplicates('partition') + [['actual_period', 'partition_count']] + .reset_index(drop=True) + ) + + ### Write next partition mapping set + nextpartition = numpartitions[['actual_period']].copy() + nextpartition['next_actual_period'] = nextpartition['actual_period'].shift(-1) + nextpartition.iloc[-1, nextpartition.columns.get_loc('next_actual_period') + ] = nextpartition['actual_period'].iloc[0] + + ### Write mapping set between current hour and previous hours before current hour + ### of the period to assist the inter-day linkage constraints. For example: + ### y2012d001h004 -> [y2012d001h004] + ### y2012d001h008 -> [y2012d001h004, y2012d001h008] + ### y2012d001h012 -> [y2012d001h004, y2012d001h008, y2012d001h012] + unique_timeslices = ( + hmap_myr + .assign(h=hmap_myr.h.map(chunkmap)) + .drop_duplicates('h') + .sort_values('h') + ) + h_preh = pd.Series( + index=unique_timeslices.h, + data=( + unique_timeslices + .groupby('season') + .h.apply(lambda x: (x+' ').cumsum().str.strip().str.split()) + .values + ), + name='preh', + ).explode().reset_index() + + else: + szn_actualszn = pd.DataFrame(columns=['season', 'actual_period']) + numpartitions = pd.DataFrame(columns=['actual_period', 'partition_count']) + nextpartition = pd.DataFrame(columns=['actual_period', 'next_actual_period']) + h_preh = pd.DataFrame(columns=['h', 'preh']) + + ### Number of times one h follows another h (for startup/ramping costs) + numhours_nexth = ( + hmap_myr.assign(h=hmap_myr.h.map(chunkmap))[ + ["actual_period", "h"] + ].drop_duplicates() + ).copy() + ## Roll to make a lookup table for GAMS + numhours_nexth = ( + numhours_nexth.assign(nexth=np.roll(numhours_nexth["h"], -1)) + .groupby(["h", "nexth"]) + .count() + .reset_index() + .rename(columns={"nexth": "hh", "actual_period": "hours"}) + ) + + #%%### Adjacent hour linkages (including links across adjacent stress periods) + if not periodtype.startswith('stress'): + if (sw.GSw_HourlyType == 'year') and (sw.GSw_HourlyWrapLevel == 'year'): + nexth_unchunked = dict(zip( + hmap_myr.actual_h.values, + np.roll(hmap_myr.actual_h.values, -1) + )) + else: + nexth_unchunked = {} + for period in hmap_myr.season.unique(): + hs = hmap_myr.loc[hmap_myr.season == period, "h"].values + nexth_unchunked = {**nexth_unchunked, **dict(zip(hs, np.roll(hs, -1)))} + else: + ### Get runs of periods + ## Two copies in case it loops from end of timeseries to beginning + unique_periods = list(hmap_myr.actual_period.unique())*2 + ## Map from each actual period to the next actual period + next_actual_period = dict(zip( + hmap_allyrs.actual_period.drop_duplicates().values, + np.roll(hmap_allyrs.actual_period.drop_duplicates().values, -1), + )) + _runs = [] + for period in unique_periods: + ## Start a run for each period + this_run = [period] + for nextperiod in unique_periods: + ## If the next period is a stress period, add it to the run, then + ## do the same for the period after that (and so forth) + if next_actual_period[period] == nextperiod: + this_run += [nextperiod] + period = nextperiod + _runs.append(this_run) + + runs = pd.Series(_runs).drop_duplicates() + + ### For each period, get the longest run containing it + _longest_run = {} + for i, period in enumerate(unique_periods): + _longest_run[period] = [] + for j, row in runs.items(): + if (period in row) and (len(row) > len(_longest_run[period])): + _longest_run[period] = row + + longest_run = pd.Series(_longest_run.values()).drop_duplicates() + + ### Cyclic boundary conditions within each run of periods + nexth_unchunked = {} + for i, row in longest_run.items(): + hs = hmap_allyrs.set_index('actual_period').loc[row,'h'] + nexth_unchunked = { + **nexth_unchunked, + **dict(zip(hs, np.roll(hs, -1))) + } + + nexth = pd.Series({ + chunkmap_allyrs[k]: chunkmap_allyrs[v] for k,v in nexth_unchunked.items() + }).rename_axis('*h').rename('h') + + # %%########################################## + # -- Hour groups for eq_minloading -- # + ############################################# + + hour_szn_group = get_minloading_windows(sw=sw, h_szn=h_szn, chunkmap=chunkmap) + + ############################# + # -- Yearly demand -- # + ############################# + + load_in, load_h = get_yearly_demand( + sw=sw, hmap_myr=hmap_myr, hmap_allyrs=hmap_allyrs, inputs_case=inputs_case, + periodtype=periodtype, + ) + + ###### Get the peak demand in each (r,szn,modelyear) for GSw_HourlyWeatherYears + load_full_yearly = load_in.loc[ + load_in.index.map(hmap_allyrs.set_index('actual_h').year.isin(sw['GSw_HourlyWeatherYears'])) + ].stack('r').reset_index() + + h2ccseason = hmap_allyrs.set_index('actual_h').ccseason + years = pd.read_csv(os.path.join(inputs_case,'modeledyears.csv')).columns.astype(int).values + + peak_all = {} + for year in years: + peak_all[year] = get_ccseason_peaks_hourly( + load=load_full_yearly[["r", "h", year]].rename(columns={year: "MW"}), + sw=sw, + inputs_case=inputs_case, + hierarchy=hierarchy, + h2ccseason=h2ccseason, + val_r_all=val_r_all, + ) + peak_all = ( + pd.concat(peak_all, names=["t", "drop"]) + .reset_index() + .drop("drop", axis=1)[["r", "ccseason", "t", "MW"]] + ).copy() + + ############################################## + # %% -- Hydro Month-to-Szn Adjustments -- # + ############################################## + + ### Import and format hydro capacity factors + h_szn_chunked = h_szn.assign(h=h_szn.h.map(chunkmap)).drop_duplicates() + + ## Calculate fraction of each month associated with each season. + szn_month_weights = ( + frac_h_month_weights.merge(h_szn_chunked, on="h", how="inner") + .drop("h", axis=1) + .drop_duplicates()[["season", "month", "weight"]] + ) + + cf_hyd = pd.read_csv( + os.path.join(inputs_case, "hydcf.csv"), + header=0, + ).rename(columns={"value": "cf_month"}) + ## Filter for modeled years + ## Get last data year (year used to forward-fill data) by removing all duplicated data + lastdatayr = ( + cf_hyd.pivot_table(index='t', columns=['*i','month','r'], values='cf_month') + ## remove all duplicated data, leaving the last data year + .drop_duplicates() + .index.max() + ) + buildyears = np.arange(2010, lastdatayr+1).tolist() + [y for y in years if y > lastdatayr] + cf_hyd = cf_hyd.loc[cf_hyd["t"].isin(buildyears)] + ## Calculate the month-weighted-average capacity factor by season + cf_hyd_out = szn_month_weights.merge(cf_hyd, on="month", how="outer") + cf_hyd_out["cf"] = cf_hyd_out["weight"] * cf_hyd_out["cf_month"] + cf_hyd_out = ( + ( + cf_hyd_out.groupby(["*i", "season", "r", "t"]) + .sum() + .drop(["month", "weight", "cf_month"], axis=1) + .cf + ## For rep periods, sum of season weights is 1, so the next line has no effect. + ## For full chronological year (GSw_HourlyType=year), we use four seasons, + ## so the sum of season weights is the number of months in that season and + ## we need to divide sum{cf*weight} by sum{weight}. + / szn_month_weights.groupby("season").weight.sum() + ) + .rename("cf") + .reset_index() + .rename(columns={"season": "szn"}) + ) + + ### Import and format monthly hydro capacity adjustment factors + hydcapadj = pd.read_csv( + os.path.join(inputs_case, "hydcapadj.csv"), header=0 + ).rename(columns={"value": "cap_month"}) + ## Calculate the month-weighted-average capacity factor by season + hydcapadj_out = szn_month_weights.merge(hydcapadj, on="month", how="outer") + hydcapadj_out["cap"] = hydcapadj_out["weight"] * hydcapadj_out["cap_month"] + hydcapadj_out = ( + ( + hydcapadj_out.groupby(["*i", "season", "r"]) + .sum() + .drop(["month", "weight", "cap_month"], axis=1) + .cap + ## For rep periods, sum of season weights is 1, so the next line has no effect. + ## For full chronological year (GSw_HourlyType=year), we use four seasons, + ## so the sum of season weights is the number of months in that season and + ## we need to divide sum{cf*weight} by sum{weight}. + / szn_month_weights.groupby("season").weight.sum() + ) + .rename("value") + .reset_index() + .rename(columns={"season": "szn"}) + ) + + ### Import and format monthly climate_{hydadjsea/UnappWaterMult/UnappWaterSeaAnnDistr}.csv + climate_files = {} + if int(sw.GSw_ClimateHydro): + climate_files['temp_hydadjsea'] = format_climate_inputs('temp_hydadjsea', inputs_case, szn_month_weights) + if int(sw.GSw_ClimateWater): + for file in ['temp_UnappWaterMult', 'temp_UnappWaterSeaAnnDistr']: + climate_files[file] = format_climate_inputs(file, inputs_case, szn_month_weights) + + ### Calculate the peak demand timeslice of each ccseason. + ## Used for hydro_nd PRM constraint. + h_ccseason_prm = ( + pd.merge(load_h[max(years)].groupby("h").sum().rename("MW"), h_ccseason, on="h") + .sort_values("MW") + .drop_duplicates("ccseason", keep="last") + .drop("MW", axis=1) + .sort_values("ccseason") + ) + + + #%%### Outage rates ###### + aggmethod = 'mean' if (not periodtype.startswith('stress')) else 'max' + + outage_h = {} + for outage_type in ['forced', 'scheduled']: + outage_hourly = reeds.io.get_outage_hourly(inputs_case, outage_type) + column_levels = list(outage_hourly.columns.names) + ## Aggregate to model resolution + outage_h[outage_type] = outage_hourly.loc[hmap_myr.timestamp].copy() + outage_h[outage_type].index = hmap_myr.h.map(chunkmap) + outage_h[outage_type] = ( + outage_h[outage_type] + .groupby(outage_h[outage_type].index) + .agg(aggmethod) + .stack(column_levels) + .reorder_levels(column_levels+['h']) + .rename('outage_rate') + .reset_index() + ) + + + #%% + ############################# + # -- DR shed -- # + ############################# + if int(sw.GSw_DRShed) and periodtype.startswith('stress'): + # Only available in stress periods + + # identify year + t = int(periodtype[6:10]) + + # each year (2030-2050) has a different dr shed profile + # prior years assume 2030 data + t_set = max(t, 2030) + + dr_shed_avail_allyears = reeds.io.read_file(os.path.join(inputs_case, 'dr_shed_hourly.h5'), parse_timestamps=True) + dr_shed_avail_allyears['year'] = round(dr_shed_avail_allyears['year'],0).astype(int) + dr_shed_avail = dr_shed_avail_allyears.loc[dr_shed_avail_allyears['year']==t_set].copy().drop('year', axis=1) + + # dr_shed only has 2018 weather year data, need to populate for other RA years + dr_shed_avail_all_weatheryears = pd.DataFrame() + # copy 2018 data to other weather years + for y in sw.resource_adequacy_years_list: + #set datetime column to match hmap_allyrs.timestamp for y + dr_shed_avail_new_index = dr_shed_avail.copy() + dr_shed_avail_new_index.index = pd.to_datetime(hmap_allyrs[hmap_allyrs['year']==y].timestamp) + + dr_shed_avail_all_weatheryears = pd.concat([dr_shed_avail_all_weatheryears, dr_shed_avail_new_index]) + + # downselect dr_shed_avail to timestamps in all weather years + dr_shed_avail_all_weatheryears.loc[hmap_allyrs.timestamp] + # map dr_shed_avail index to actual period + dr_shed_avail_all_weatheryears.index = hmap_allyrs.loc[hmap_allyrs.timestamp.isin(dr_shed_avail_all_weatheryears.index)].actual_h + # Map actual periods to rep periods + dr_shed_avail_all_weatheryears = dr_shed_avail_all_weatheryears.loc[ + dr_shed_avail_all_weatheryears.index.map(lambda x: any([x.startswith(i) for i in rep_periods]))] + #Need to convert avail to a fraction - use max in each column as base + # Normalize dr_shed_avail by values specified in inputs/demand_response/dr_shed_avail_scalar.csv + dr_shed_avail_scalar = pd.read_csv(os.path.join(inputs_case,'dr_shed_avail_scalar.csv')) + dr_shed_avail_scalar = dr_shed_avail_scalar[dr_shed_avail_scalar['t']==t_set]['Value'].item() + dr_shed_avail_all_weatheryears = (dr_shed_avail_all_weatheryears + .div(dr_shed_avail_all_weatheryears .max()))*dr_shed_avail_scalar + + + # Reformat to be indexed by i,r,h + dr_shed_avail_out = dr_shed_avail_all_weatheryears.rename_axis('h').copy() + i = dr_shed_avail_all_weatheryears.columns.map(lambda x: x.split('|')[0]) + r = dr_shed_avail_all_weatheryears.columns.map(lambda x: x.split('|')[1]) + dr_shed_avail_out.columns = pd.MultiIndex.from_arrays([i,r], names=['i','r']) + dr_shed_avail_out = dr_shed_avail_out.stack(['i','r']).reorder_levels(['i','r','h']).rename('cap').reset_index() + + else: + # populate empty dataframe + dr_shed_avail_out = pd.DataFrame(columns=['i','r','h','cap']) + + ############################# + # -- EV Managed Charging -- # + ############################# + + if int(sw.GSw_EVMC): + evmc_baseline_load = ( + pd.read_hdf(os.path.join(inputs_case, "ev_baseline_load.h5")) + .rename(columns={"h": "hour"}) + .astype({"r": str}) + ) + ## Drop the h + evmc_baseline_load.hour = evmc_baseline_load.hour.str.strip("h").astype("int") + ## Concat for each weather year + evmc_baseline_load_weatheryears = evmc_baseline_load.pivot( + index="hour", columns=["t", "r"], values="net" + ) + evmc_baseline_load_weatheryears = pd.concat( + {y: evmc_baseline_load_weatheryears for y in sw.resource_adequacy_years_list}, + axis=0, ignore_index=True).loc[hmap_myr.hour0] + ## Map 8760 hours to modeled hours + evmc_baseline_load_weatheryears.index = hmap_myr.h + ### Sum by (r,h,t) to get net trade in MWh during modeled hours + evmc_baseline_load_out = ( + evmc_baseline_load_weatheryears.stack(["r", "t"]) + .groupby(["r", "h", "t"]) + .sum() + .rename("MWh") + ## Divide by number of weather years since we concatted that number of weather years + / (len(sw['GSw_HourlyWeatherYears']) if (not periodtype.startswith('stress')) else 1) + ).reset_index() + ## Only keep modeled regions + evmc_baseline_load_out = evmc_baseline_load_out.loc[ + evmc_baseline_load_out.r.isin(val_r_all) + ].copy() + + evmc_shape_dec, evmc_shape_inc = get_yearly_flexibility( + sw=sw, + period_szn=period_szn, + rep_periods=rep_periods, + hmap_1yr=hmap_myr, + set_szn=set_szn, + inputs_case=inputs_case, + drcat="evmc_shape", + ) + + evmc_storage_dec, evmc_storage_inc, evmc_storage_energy = ( + get_yearly_flexibility( + sw=sw, + period_szn=period_szn, + rep_periods=rep_periods, + hmap_1yr=hmap_myr, + set_szn=set_szn, + inputs_case=inputs_case, + drcat="evmc_storage", + ) + ) + else: + evmc_shape_dec = pd.DataFrame(columns=["*i", "r", "h"]) + evmc_shape_inc = pd.DataFrame(columns=["*i", "r", "h"]) + evmc_storage_dec = pd.DataFrame(columns=["*i", "r", "h", "t"]) + evmc_storage_inc = pd.DataFrame(columns=["*i", "r", "h", "t"]) + evmc_storage_energy = pd.DataFrame(columns=["*i", "r", "h", "t"]) + evmc_baseline_load_out = pd.DataFrame(columns=["r", "h", "t", "MWh"]) + + + #%% Chunk the profiles + if sw.GSw_HourlyChunkAggMethod == 'mean': + aggmethod = 'mean' + args = [] + else: + if sw.GSw_HourlyChunkAggMethod == 'mid': + ## Round up: + ## For 2 hours, keep hour 2; + ## for 3 hours, keep hour 2; + ## for 4 hours, keep hour 3; etc. + keephour = int(np.ceil((GSw_HourlyChunkLength + 0.1) / 2)) + else: + keephour = int(sw.GSw_HourlyChunkAggMethod) + assert 0 < keephour <= GSw_HourlyChunkLength + aggmethod = 'nth' + ## Change from start-at-1 index to Python's start-at-0 index + args = [keephour - 1] + + if ('stress' in periodtype) and (aggmethod == 'mean'): + aggmethod_load = sw.GSw_PRM_StressLoadAggMethod + else: + aggmethod_load = aggmethod + print(f'{periodtype} load aggregation method: {aggmethod_load} {args}') + + cf_vre = ( + cf_out + .sort_values(['i','r','h']) + .assign(h=cf_out.h.map(chunkmap)) + .groupby(['i','r','h'], as_index=False) + .agg(aggmethod, *args) + ) + + load_long = ( + load_h + .stack('t') + .rename('MW') + .reorder_levels(['t','r','h']) + .sort_index() + .reset_index() + ) + load_allyear = ( + load_long + .assign(h=load_long.h.map(chunkmap)) + .groupby(['t','r','h'], as_index=False) + .agg(aggmethod_load, *args) + .set_index(['r','h','t']) + .reset_index() + ) + + + # %%################################################################################### + # -- Write outputs, aggregating hours to GSw_HourlyChunkLength if necessary -- # + ###################################################################################### + write = { + ### Contents are [dataframe, header, index] + ## h set for representative timeslices + "set_h": [hset.map(chunkmap).drop_duplicates().to_frame(), False, False], + ## szn set for representative periods + 'set_szn': [set_szn, False, False], + ## Previous hour for each h of the period + 'h_preh': [h_preh, False, False], + ## Hours to season mapping (h,szn) + "h_szn": [ + h_szn.assign(h=h_szn.h.map(chunkmap)).drop_duplicates(), + False, + False, + ], + ## 8760 hour linkage set for Augur (h,szn,year,hour) + "h_dt_szn": [ + h_dt_szn[["h", "season", "ccseason", "year", "hour"]].assign( + h=h_dt_szn.h.map(chunkmap) + ), + True, + False, + ], + ## Number of hours represented by each timeslice (h) + "numhours": [ + ( + hours.reset_index() + .assign(h=hours.index.map(chunkmap)) + .groupby("h") + .numhours.sum() + .reset_index() + .round(decimals + 3) + ), + False, + False, + ], + ## Number of times in actual year that one timeslice follows another (h,hh) + "numhours_nexth": [numhours_nexth, False, False], + ## Quarterly season weights for assigning quarter-dependent parameters (h,quarter) + "frac_h_quarter_weights": [ + ( + frac_h_weights["quarter"] + .assign(h=frac_h_weights["quarter"].h.map(chunkmap)) + .groupby(["h", "quarter"], as_index=False) + .weight.mean() + .round(decimals + 3) + ), + False, + False, + ], + ## ccseason weights for assigning ccseason-dependent parameters (h,ccseason) + "frac_h_ccseason_weights": [ + ( + frac_h_weights["ccseason"] + .assign(h=frac_h_weights["ccseason"].h.map(chunkmap)) + .groupby(["h", "ccseason"], as_index=False) + .weight.mean() + .round(decimals + 3) + ), + False, + False, + ], + ## Hydro capacity factors by szn + "cf_hyd": [cf_hyd_out.round(decimals), True, False], + ## Hydro capacity adjustment factors by szn + "cap_hyd_szn_adj": [hydcapadj_out.round(decimals + 2), True, False], + ## mapping from one timeslice to the next + "nexth": [nexth, True, True], + ## Hours to actual season mapping (h,allszn) + 'h_actualszn': [h_actualszn, False, False], + ## season to actual season mapping (szn,allszn) + 'szn_actualszn': [szn_actualszn, False, False], + ## actual season partition + 'numpartitions': [numpartitions, False, False], + ## next partition + 'nextpartition': [nextpartition, False, False], + ## mapping from one timeslice to the next for actual periods + "nexth_actualszn": [nexth_actualszn, False, False], + ## first timeslice in season (szn,h) + "h_szn_start": [szn2starth.map(chunkmap).reset_index(), False, False], + ## last timeslice in season (szn,h) + "h_szn_end": [szn2endh.map(chunkmap).reset_index(), False, False], + ## minload hour windows with overlap (h,h) + "hour_szn_group": [hour_szn_group, False, False], + ## periods in which to apply operating reserve constraints (szn) + "opres_periods": [opres_periods, False, False], + ## Season-peak demand hour for each szn's representative day (h,szn) + "h_ccseason_prm": [ + h_ccseason_prm.assign(h=h_ccseason_prm.h.map(chunkmap)), + False, + False, + ], + ## Annual timeslice demand + 'load_allyear': [load_allyear.round(decimals), False, False], + ## Seasonal peak demand + "peak_ccseason": [peak_all.round(decimals), False, False], + ## Capacity factors (i,r,h) + 'cf_vre': [cf_vre.round(5), False, False], + ## Exports to Canada [fraction] (h) + "can_exports_h_frac": [ + ( + can_exports_h_frac.assign(h=can_exports_h_frac.h.map(chunkmap)) + .groupby("h", as_index=False) + .sum() + .round(6) + ), + False, + False, + ], + ## Imports from Canada [fraction] (szn) + 'can_imports_szn_frac': [can_imports_szn_frac.round(6), False, False], + ## Outage rates + 'outage_forced_h': [outage_h['forced'].round(3), False, False], + 'outage_scheduled_h': [outage_h['scheduled'].round(3), False, False], + # DR + "dr_shed_out": [ + (dr_shed_avail_out.assign(h=dr_shed_avail_out.h.map(chunkmap)) + .groupby(['i','r','h'], as_index=False).cap.mean().round(5)), + False, False], + ## EVMC + "evmc_baseline_load": [ + ( + evmc_baseline_load_out.assign(h=evmc_baseline_load_out.h.map(chunkmap)) + .groupby(["r", "h", "t"], as_index=False) + .MWh.sum() + .round(decimals) + ), + False, + False, + ], + "evmc_shape_generation": [ + ( + evmc_shape_dec.assign(h=evmc_shape_dec.h.map(chunkmap)) + .groupby(["*i", "r", "h"]) + .mean() + .round(decimals) + .reset_index() + ), + False, + False, + ], + "evmc_shape_load": [ + ( + evmc_shape_inc.assign(h=evmc_shape_inc.h.map(chunkmap)) + .groupby(["*i", "r", "h"]) + .mean() + .round(decimals) + .reset_index() + ), + False, + False, + ], + "evmc_storage_discharge": [ + ( + evmc_storage_dec.assign(h=evmc_storage_dec.h.map(chunkmap)) + .groupby(["*i", "r", "h", "t"]) + .mean() + .round(decimals) + .reset_index() + ), + False, + False, + ], + "evmc_storage_charge": [ + ( + evmc_storage_inc.assign(h=evmc_storage_inc.h.map(chunkmap)) + .groupby(["*i", "r", "h", "t"]) + .mean() + .round(decimals) + .reset_index() + ), + False, + False, + ], + "evmc_storage_energy": [ + ( + evmc_storage_energy.assign(h=evmc_storage_energy.h.map(chunkmap)) + .groupby(["*i", "r", "h", "t"]) + .mean() + .round(decimals) + .reset_index() + ), + False, + False, + ], + ################################################################################## + ###### The next parameters are just diagnostics and are not actually used in ReEDS + ## Representative period weights for postprocessing (szn) + "period_weights": [period_weights, False, False], + ## Mapping from representative h to actual h + 'hmap_myr': [ + hmap_myr.assign(h=hmap_myr.h.map(chunkmap)), + False, False], + ## Mapping from representative h to actual h for full set of years + 'hmap_allyrs': [ + hmap_allyrs.assign(h=hmap_allyrs.h.map(chunkmap)), + False, False], + ## Mapping from representative period to actual period + "periodmap_1yr": [ + hmap_myr[["actual_period", "season"]].drop_duplicates(), + False, + False, + ], + ################################################################### + ###### The folowing parameters don't yet work for hourly resolution + ## Canada/Mexico + "canmexload": [pd.DataFrame(columns=["*r", "h"]), True, False], + ## GSw_EFS_Flex + "flex_frac_all": [ + pd.DataFrame(columns=["*flex_type", "r", "h", "t"]), + True, + False, + ], + "peak_h": [pd.DataFrame(columns=["*r", "h", "t", "MW"]), True, False], + } + + # Add climate inputs based on GSw_Climate* switch selection + if int(sw.GSw_ClimateHydro): + ## Climate-adjusted annual/seasonal nondispatchable hydropower availability + write['climate_hydadjsea'] = [climate_files['temp_hydadjsea'], True, True] + if int(sw.GSw_ClimateWater): + ## Climate-adjusted time-varying annual/seasonal water supply + write['climate_UnappWaterMult'] = [climate_files['temp_UnappWaterMult'], True, True] + ## Climate-adjusted time-varying fractional seasonal allocation of water + write['climate_UnappWaterSeaAnnDistr'] = [climate_files['temp_UnappWaterSeaAnnDistr'], True, True] + + #%% Write output csv files + for f in write: + ### Rename first column so GAMS reads it as a comment + if not write[f][1]: + write[f][0] = write[f][0].rename( + columns={write[f][0].columns[0]: "*" + str(write[f][0].columns[0])} + ) + ### If the file already exists and we're creating representative period data, + ### add a '_h17' to the filename and save it as a backup + if (debug + and os.path.isfile(os.path.join(inputs_case, f+'.csv')) + and (not periodtype.startswith('stress'))): + shutil.copy( + os.path.join(inputs_case, f + ".csv"), + os.path.join(inputs_case, f + "_h17.csv"), + ) + ### Write the new hourly parameters + write[f][0].to_csv( + os.path.join(outpath, f+'.csv'), + index=write[f][2], + ) + + #%% Map weighted average profile values and difference from full-resolution mean + if make_plots: + figpath = os.path.abspath( + os.path.join(os.path.dirname(inputs_case.rstrip(os.sep)), 'outputs', 'figures') + ) + try: + import matplotlib.pyplot as plt + import hourly_plots + ## Capacity factor and load + hourly_plots.plot_maps(sw, inputs_case, reeds_path, figpath) + ## Representative days + f, ax, _ = reeds.reedsplots.plot_repdays(os.path.dirname(os.path.abspath(inputs_case))) + plt.savefig(os.path.join(figpath, 'inputs_repdays.png')) + except Exception: + import traceback + print(traceback.format_exc()) + + return write diff --git a/reeds/input_processing/hydcf.py b/reeds/input_processing/hydcf.py new file mode 100644 index 00000000..c9419c8c --- /dev/null +++ b/reeds/input_processing/hydcf.py @@ -0,0 +1,470 @@ +''' +This script calculates monthly hydro capacity factors (CFs) for each model +region and year. Historical CFs are calculated by taking the ratio of net and +max hydro generation for each region's existing hydro fleet. +Future CFs come from two sources: +1) In some cases, CFs are calculated by taking each plant's average net/max +generation across select years and calculating the ratio of total average net +and average max generation for each region's hydro fleet. +2) In other cases, capacity factors come from "hydcf_fixed.csv", +which contains pre-calculated CFs for the legacy 134 zones. +These are transformed into model region-level CFs by uniformly assigning the +zonal CFs to each legacy zone's counties and taking the average CF +across each model region's counties. +''' + +import argparse +import numpy as np +import pandas as pd +import os +import sys +import datetime +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds + +def get_monthly_plant_generation(inputs_case: str) -> ( + tuple[pd.DataFrame, pd.DataFrame] +): + """ + Get monthly net generation and maximum generation in MWh for + each hydro plant. Net generation values are read from + inputs_case/net_gen_existing_hydro.csv, while maximum generation values + are derived from annual capcities (inputs_case/cap_existing_hydro.csv) + by calculating monthly generation assuming 100% capacity factor. + + Args: + inputs_case: Path to the inputs case directory. + + Returns: + tuple[pd.DataFrame, pd.DataFrame] + """ + # Read inputs + annual_plant_capacities = pd.read_csv( + os.path.join(inputs_case, 'cap_existing_hydro.csv'), + index_col='t' + ) + monthly_plant_net_generation = pd.read_csv( + os.path.join(inputs_case, 'net_gen_existing_hydro.csv'), + index_col=['t', 'month'] + ) + # Expand annual capacity data to monthly + monthly_plant_capacities = annual_plant_capacities.reindex( + monthly_plant_net_generation.index, + level=0 + ) + # Assign number of hours to each month + monthly_plant_capacities['date'] = pd.to_datetime( + ( + monthly_plant_capacities.index.get_level_values('t').astype(str) + + '-' + + monthly_plant_capacities.index.get_level_values('month') + ), + format='%Y-%b' + ) + monthly_plant_capacities['num_hours'] = ( + monthly_plant_capacities['date'].dt.daysinmonth * 24 + ) + # Multiply monthly capacities by number of hours in each month + monthly_plant_max_generation = ( + monthly_plant_capacities.drop(columns=['date', 'num_hours']) + .mul(monthly_plant_capacities['num_hours'], axis=0) + ) + # Align null values across datasets + monthly_plant_max_generation[monthly_plant_net_generation.isna()] = np.nan + monthly_plant_net_generation[monthly_plant_max_generation.isna()] = np.nan + + return monthly_plant_net_generation, monthly_plant_max_generation + + +def calculate_regional_generation( + plant_generation: pd.DataFrame, + hydro_plants: pd.DataFrame +) -> pd.DataFrame: + """ + Calculate total generation for each model region in MWh. + + Args: + plant_generation: Plant-level generation in MWh. + hydro_plants: Tech and region information for each hydro plant. + + Returns: + pd.DataFrame + """ + # Reformat plant generation data and append tech and region information + index_cols = list(plant_generation.index.names) + plant_generation = pd.melt( + plant_generation.reset_index(), + id_vars=index_cols, + var_name='EIA_PlantID' + ) + plant_generation = ( + plant_generation.merge( + hydro_plants, + left_on=['EIA_PlantID'], + right_index=True + ) + .rename(columns={'tech': '*i'}) + ) + # Group by tech and region and calculate total generation + groupby_cols = index_cols + ['*i', 'r'] + regional_generation = plant_generation.groupby(groupby_cols).sum() + regional_generation = ( + pd.pivot_table( + regional_generation, + index=['*i'] + index_cols, + columns=['r'], + values=['value'] + ) + .droplevel(level=0, axis=1) + .rename_axis(columns=['']) + ) + + return regional_generation + + +def calculate_historical_monthly_regional_cf( + monthly_plant_net_generation: pd.DataFrame, + monthly_plant_max_generation: pd.DataFrame, + hydro_plants: pd.DataFrame, + inputs_case: str +) -> pd.DataFrame: + """ + Calculate monthly CFs for each model region in historical years. + In historical years, CFs are calculated by aggregating plant-level + generation to the region level and taking the ratio of each region's + total net generation and total max generation. + + Args: + monthly_plant_net_generation: Monthly plant net generation in MWh. + monthly_plant_max_generation: Monthly plant max generation in MWh. + hydro_plants: Tech and region information for each hydro plant. + inputs_case: Path to the inputs case directory. + + Returns: + pd.DataFrame + """ + # Calculate monthly net and max generation for each model region + monthly_regional_net_generation = calculate_regional_generation( + monthly_plant_net_generation, + hydro_plants + ) + monthly_regional_max_generation = calculate_regional_generation( + monthly_plant_max_generation, + hydro_plants + ) + # Calculate monthly CFs for each model region + monthly_regional_cf = ( + monthly_regional_net_generation.div( + monthly_regional_max_generation.replace(0, np.nan) + ) + .rename_axis(columns=['r']) + .reorder_levels(order=['t', '*i', 'month']) + ) + # Downselect to model years + sw = reeds.io.get_switches(inputs_case) + startyear = int(sw.startyear) + monthly_regional_cf = monthly_regional_cf.loc[( + monthly_regional_cf.index.get_level_values('t') >= startyear + )] + + return monthly_regional_cf + + +def calculate_regional_average_generation( + monthly_plant_generation: pd.DataFrame, + hydro_plants: pd.DataFrame, + future_hydcf_rep_years: list[int] +) -> pd.DataFrame: + """ + Calculate average generation across years for each plant + in each month and then aggregate to the model region level. + + Args: + monthly_plant_generation: Monthly plant-level generation in MWh. + hydro_plants: Tech and region information for each hydro plant. + future_hydcf_rep_years: Set of years from which to calculate + future hydro CFs. + + Returns: + pd.DataFrame + """ + # Subset generation data to years representing future hydro + monthly_plant_generation = monthly_plant_generation.loc[( + monthly_plant_generation.index + .get_level_values('t') + .isin(future_hydcf_rep_years) + )] + # Calculate average generation across years for each plant in each month + plant_average_generation = ( + monthly_plant_generation.groupby(level='month') + .mean() + ) + # Aggregate average plant-level generation to the model region level + regional_average_generation = calculate_regional_generation( + plant_average_generation, + hydro_plants + ) + + return regional_average_generation + + +def calculate_future_monthly_regional_cf( + monthly_plant_net_generation: pd.DataFrame, + monthly_plant_max_generation: pd.DataFrame, + hydro_plants: pd.DataFrame, + inputs_case: str, +): + """ + Calculate monthly CFs for each model region in future years. + Future CFs come from two sources: + 1) In some cases, CFs are calculated by taking each plant's average net/max + generation across select years (based on the GSw_FutureHydCF_RepYears + switch) and calculating the ratio of total average + net and average max generation for each region's hydro fleet. + 2) In other cases, capacity factors come from inputs_case/hydcf_fixed.csv, + which contains pre-calculated CFs for the legacy 134 zones. + These are transformed into model region-level CFs by uniformly + assigning the zonal CFs to each legacy zone's counties and taking + the average CF across each model region's counties. + + In cases where data for a given time and region exist in both sources, + the first source (i.e., plant-level data) takes precedence. + + Args: + monthly_plant_net_generation: Monthly plant net generation in MWh. + monthly_plant_max_generation: Monthly plant max generation in MWh. + hydro_plants: Tech and region information for each hydro plant. + inputs_case: Path to the inputs case directory. + + Returns: + pd.DataFrame + """ + # Get the set of years that represents future hydro + sw = reeds.io.get_switches(inputs_case) + future_hydcf_rep_years = sw['future_hydcf_rep_years_list'] + # Calculate average net and max generation for each plant + # and aggregate to the model region level + regional_average_net_generation = calculate_regional_average_generation( + monthly_plant_net_generation, + hydro_plants, + future_hydcf_rep_years + ) + regional_average_max_generation = calculate_regional_average_generation( + monthly_plant_max_generation, + hydro_plants, + future_hydcf_rep_years + ) + # Calculate monthly CFs for each model region + future_cf_existing_techs = regional_average_net_generation.div( + regional_average_max_generation.replace(0, np.nan) + ) + # Duplicate monthly CF data for existing techs to derive CFs for + # upgrade techs, re-assigning "ED/END" hydro categories to "UD/UND" + upgrade_dict = {"hydED": "hydUD", "hydEND": "hydUND"} + future_cf_upgrade_techs = ( + future_cf_existing_techs.loc[( + future_cf_existing_techs.index + .get_level_values('*i') + .isin(upgrade_dict.keys()) + )] + .reset_index() + .replace(upgrade_dict) + .set_index(['*i', 'month']) + ) + # Read pre-calculated fixed CFs and reformat + future_cf_fixed = pd.read_csv( + os.path.join(inputs_case, 'hydcf_fixed.csv') + ) + future_cf_fixed = future_cf_fixed.pivot_table( + index=['*i', 'month'], + columns='r', + values='value' + ) + ## Concatenate all future CFs + # Note that we don't simply call pd.concat because the component dataframes + # are not guaranteed to be mutually exclusive (i.e., we may have both fixed + # CFs and CFs derived from plant data for a given region and tech), so + # pd.concat could result in duplicate indices with different values. + # Instead, we use the concatenation operation below, which is structured so + # that the CFs calculated from plant data are prioritized over the fixed + # CFs in cases of duplicate indices. + future_cf_columns = ( + future_cf_fixed.columns + .union(future_cf_existing_techs.columns) + .union(future_cf_upgrade_techs.columns) + ) + future_cf_index = ( + future_cf_fixed.index + .union(future_cf_existing_techs.index) + .union(future_cf_upgrade_techs.index) + ) + future_cf = pd.DataFrame( + columns=future_cf_columns, + index=future_cf_index + ) + future_cf.update(future_cf_fixed) + future_cf.update(future_cf_existing_techs) + future_cf.update(future_cf_upgrade_techs) + + return future_cf + + +def get_hydro_plants(inputs_case: str) -> pd.DataFrame: + """ + Reads the EIA plant database from inputs_case/unitdata.csv and + filters down to hydro plants (plants whose tech starts with "hyd"). + + Args: + inputs_case: Path to the inputs case directory. + + Returns: + pd.DataFrame + """ + # Get county-to-region mapping + county2zone = reeds.io.get_county2zone(os.path.dirname(inputs_case)) + county2zone.index = 'p' + county2zone.index + # Get plant database and filter down to hydro plants + gendb = pd.read_csv( + os.path.join(inputs_case, 'unitdata.csv'), + usecols=['T_PID', 'tech', 'FIPS'] + ) + hydro_plants = ( + gendb.loc[gendb.tech.str.startswith('hyd')] + .drop_duplicates('T_PID') + .set_index('T_PID') + ) + # Assign each plant to a model region and reformat + hydro_plants['r'] = hydro_plants['FIPS'].map(county2zone) + hydro_plants = hydro_plants.drop(columns='FIPS') + hydro_plants.index = hydro_plants.index.astype(str) + + return hydro_plants + + +def assemble_hydcf( + historical_monthly_regional_cf: pd.DataFrame, + future_monthly_regional_cf: pd.DataFrame, + inputs_case: str +) -> pd.DataFrame: + """ + Combines monthly historical and future hydro CF data, + forward-filling the future data up to the ReEDS model end year. + + Args: + historical_monthly_regional_cf: Monthly regional CFs + in historical years. + future_monthly_regional_cf: Monthly regional CFs + in an unspecified future year. These CFs are duplicated + across years from the end of the historical period + to the model end year. + inputs_case: Path to the inputs case directory. + + Returns: + pd.DataFrame + """ + # Assign a year to the future CFs corresponding to the + # year after the final year of historical CFs + historical_endyear = ( + historical_monthly_regional_cf.index.get_level_values('t').max() + ) + future_monthly_regional_cf = ( + future_monthly_regional_cf.assign(t=historical_endyear+1) + .set_index('t', append=True) + .reorder_levels(historical_monthly_regional_cf.index.names) + ) + # Concatenate historical and future CFs + hydcf = pd.concat([ + historical_monthly_regional_cf, + future_monthly_regional_cf + ]) + # Reformat so that hydcf is indexed by year and + # has column levels for tech, month, and region + hydcf = ( + hydcf.stack() + .rename_axis(['t','*i','month','r']) + .rename('value') + .to_frame() + .reset_index() + .pivot_table(index='t', columns=['*i','month','r'], values='value') + ) + # Forward-fill years up to model end year + sw = reeds.io.get_switches(inputs_case) + model_endyear = int(sw.endyear) + data_endyear = hydcf.index.max() + reindex = ( + hydcf.index.tolist() + + np.arange(data_endyear+1, model_endyear+1).tolist() + ) + hydcf = hydcf.reindex(reindex) + hydcf.loc[data_endyear:] = hydcf.loc[data_endyear:].ffill() + # Convert from "wide" to "long" format + hydcf = hydcf.stack(['*i', 'month']).stack().rename('value').to_frame() + + return hydcf + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== +def main(reeds_path, inputs_case): + print('Starting hydcf.py') + + monthly_plant_net_generation, monthly_plant_max_generation = ( + get_monthly_plant_generation(inputs_case) + ) + hydro_plants = get_hydro_plants(inputs_case) + historical_monthly_regional_cf = calculate_historical_monthly_regional_cf( + monthly_plant_net_generation, + monthly_plant_max_generation, + hydro_plants, + inputs_case + ) + future_monthly_regional_cf = calculate_future_monthly_regional_cf( + monthly_plant_net_generation, + monthly_plant_max_generation, + hydro_plants, + inputs_case + ) + hydcf = assemble_hydcf( + historical_monthly_regional_cf, + future_monthly_regional_cf, + inputs_case + ) + hydcf.to_csv(os.path.join(inputs_case, 'hydcf.csv')) + + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if __name__ == '__main__': + # Time the operation of this script + tic = datetime.datetime.now() + + ### Parse arguments + parser = argparse.ArgumentParser( + description='Process hydro capacity factors', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('reeds_path', help='ReEDS-2.0 directory') + parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + #%% Run it + main(reeds_path=reeds_path, inputs_case=inputs_case) + + reeds.log.toc(tic=tic, year=0, process='inputs/hydcf.py', + path=os.path.join(inputs_case,'..')) + + print('Finished hydcf.py') diff --git a/reeds/input_processing/mcs_sampler.py b/reeds/input_processing/mcs_sampler.py new file mode 100644 index 00000000..80679f06 --- /dev/null +++ b/reeds/input_processing/mcs_sampler.py @@ -0,0 +1,1649 @@ +""" +This module performs the Monte Carlo sampling for ReEDS. +""" + + +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== +import os +import sys +import numpy as np +import pandas as pd +import copy +import argparse +import yaml +import datetime +from typing import Tuple, List +from collections import defaultdict + +# Local Imports +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +from reeds.input_processing import copy_files + + +#%% =========================================================================== +### --- CONSTANTS --- +### =========================================================================== +class MCSConstants: + """ + Configuration constants for the Monte Carlo Sampling (MCS) process in ReEDS. + Contains synonyms, file names for special treatment, and valid distribution identifiers. + """ + ### --- Synonyms + TECH_DESCRIPTOR = ['i', 'type', 'Tech', 'Geo class', 'Depth', 'Turbine', 'tech', '*tech', 'class'] + YEAR_SYNONYMS = ['t', 'Year', 'year'] + REGION_SYNONYMS = ['r', 'region', 'cendiv', 'sc_point_gid', 'FIPS'] + + ### --- Fixed columns that should not be modified in most cases + OTHER_INDICES = ['columns', 'p', '*p'] # 'p' is used in h2_exog_cap.csv + NONMODIFIABLE_FINANCIAL_COLUMNS = ['debt_fraction', 'tax_rate'] + FIXED_COLUMN_NAMES = YEAR_SYNONYMS + TECH_DESCRIPTOR + OTHER_INDICES + NONMODIFIABLE_FINANCIAL_COLUMNS + REGION_SYNONYMS + + ### --- Files that require special treatment + SUPPLY_CURVE_FILES = [ + "supplycurve_upv.csv", + "supplycurve_wind-ofs.csv", + "supplycurve_wind-ons.csv", + ] + EXOG_CAP_FILES = ["exog_cap_upv.csv", "exog_cap_wind-ons.csv"] + PRESCRIBED_BUILDS_FILES = ["prescribed_builds_wind-ofs.csv", "prescribed_builds_wind-ons.csv"] + RECF_FILES = ["recf_wind-ons.h5", "recf_wind-ofs.h5", "recf_upv.h5"] + + ### --- Switch-File(s) combinations hardcoded in copy_files.py + # These files are explicitly handled in copy_files.py, bypassing the standard + # runfiles.csv instructions. In these cases, the switch is often used as a filter + # to select specific rows or columns within the file. + HARD_CODED_SWITCH_TO_FILE_READ = { + 'GSw_H2_Demand_Case': ["h2_exogenous_demand.csv"], + } + + SITING_SWITCHES = ["GSw_SitingUPV", "GSw_SitingWindOfs", "GSw_SitingWindOns"] + + ### --- Valid distributions + VALID_DISTRIBUTIONS = ["dirichlet", "discrete", "uniform_multiplier", "triangular_multiplier"] + MULTIPLICATIVE_DISTRIBUTIONS = ["uniform_multiplier", "triangular_multiplier"] + + +#%% =========================================================================== +### --- Auxiliary functions --- +### =========================================================================== +def max_decimal_places(data, columns: list = None) -> dict: + """ + Calculate the maximum number of decimal places in a single number, specific columns, or all columns of a DataFrame. + + Args: + data (pd.DataFrame or numeric): The input DataFrame or a single numeric value (float or int). + columns (list or None): List of column names to analyze if data is a DataFrame. If None, all columns will be analyzed. + + Returns: + int or dict: + - If data is a single number, returns the number of decimal places in the number. + - If data is a DataFrame, returns a dictionary with column names as keys and their respective maximum number of decimal places as values. + """ + # Function to count the number of decimals in a single number + def count_decimals(data): + if isinstance(data, (float, int, str)) and '.' in str(data): + return len(str(data).split('.')[1]) + else: + return 0 + + # If we have a single number or a list of numbers + if not isinstance(data, pd.DataFrame): + if isinstance(data, (list, np.ndarray)): + return max([count_decimals(val) for val in data]) + else: + return count_decimals(data) + + else: + # If columns is None, analyze all columns + if columns is None: + columns = data.keys() + + # Compute the maximum number of decimal places for each specified column + return {col: data[col].apply(count_decimals).max() for col in columns} + + +def read_exception_file(sw_assignment: str, file_name: str, file_path: str) -> pd.DataFrame: + """ + Handles exceptions for files that are hardcoded in copy_files.py + and written directly without using runfiles.csv. + + This function allows you to manually support special cases where + a switch-file combination is not automatically handled by the MCS module. + If you encounter a new unsupported case, you can add it here. + + Args: + sw_assignment (str): The switch assignment. + file_name (str): Name of the file (output filename). + file_path (str): Path to the reference file (inputs folder). + + Returns: + pd.DataFrame: A DataFrame formatted as expected before being written + to the inputs_case folder (as in copy_files.py). + """ + if file_name == 'h2_exogenous_demand.csv': + # h2_exogenous_demand.csv has a path in runfiles.csv (considered a non-region file) + df = pd.read_csv(file_path, index_col=['p', 't']) + df = df[sw_assignment].round(3).rename_axis(['*p', 't']).reset_index() + + # Rename the value column to 'million_tons' to avoid issues in writecapdat.py + df.rename(columns={sw_assignment: 'million_tons'}, inplace=True) + return df + + return None + + +def read_csv_h5_file(sw_runfiles_csv, aux_files, reeds_path, inputs_case) -> pd.DataFrame: + """ + This function reads a csv or h5 file based on a row of runfiles.csv and returns a dataframe with the data + in the ReEDS format. + + Args: + sw_runfiles_csv (pd.Series): A row of runfiles.csv with sw preassigned to the filepath. + aux_files (dict): A dictionary with auxiliary information for the copy_files.py module. + reeds_path (str): The path to the ReEDS directory. + inputs_case (str): The path to the inputs case directory. + + Returns: + pd.DataFrame: A DataFrame with the data in the ReEDS format. + """ + # Obtain the data used by copy_files.py to filter regions and create tailored dataframes + nonregion_files = aux_files['nonregion_files'] + region_files = aux_files['region_files'] + file_name = sw_runfiles_csv['filename'] + file_path = os.path.join(reeds_path, sw_runfiles_csv['full_filepath']) + + # Try to read the file using the read_exception_file function first + df = read_exception_file(sw_runfiles_csv['sw_assignment'], file_name, file_path) + if df is not None: + return df + + if file_name in region_files['filename'].values: + # Regional file (works for both csv and h5) + df = copy_files.subset_to_valid_regions( + aux_files['sw'], + sw_runfiles_csv, + aux_files['agglevel_variables'], + aux_files['regions_and_agglevel'], + inputs_case, + agg=False, + ) + + elif file_name in nonregion_files['filename'].values: + files_not_supported = ['scalars.csv'] + if file_path.endswith('.csv') and file_name not in files_not_supported: + #Read the csv file + df = pd.read_csv(file_path) + else: + #File not implemented yet + error_message = 'The file %s has not been implemented yet' % sw_runfiles_csv['filename'] + raise ValueError(error_message + ' improve function read_csv_h5_file') + + elif file_name in ['switches.csv']: + df = pd.read_csv(os.path.join(inputs_case, file_name), + header = None, index_col=0, dtype=str) + + else: + error_message = ( + f"The file '{file_name}' is not classified under nonregion_files or region_files, " + "and it is not currently handled by the read_exception_file function. " + "If you want to use this switch-file combination in MCS, please update the read_exception_file function " + "and add an entry to MCSConstants.HARD_CODED_SWITCH_TO_FILE_READ." + ) + raise ValueError(error_message) + + return df + + +def get_hierarchy_file(inputs_case: str, ReEDS_resolution: str) -> pd.DataFrame: + """ + The hierarchy file in `{inputs_case}/hierarchy.csv` does not contain a + differentiation between "ba" and "aggreg" resolution. This function + reconstructs the hierarchy file with all possible combinations relevant + to the MCS. + + Args: + inputs_case (str): Path to the inputs case directory. + ReEDS_resolution (str): The spatial resolution used in ReEDS (e.g., 'ba', 'aggreg'). + + Returns: + pd.DataFrame: A DataFrame with the hierarchy information relevant to the regions + considered in the inputs_casse run. + """ + original_hierarchy_file = pd.read_csv( + os.path.join(inputs_case, "hierarchy_original.csv") + ) + + valid_regions = pd.read_csv( + os.path.join(inputs_case, "hierarchy.csv") + )['*r'].values + + filtered_hierarchy = original_hierarchy_file[ + original_hierarchy_file[ReEDS_resolution].isin(valid_regions) + ].reset_index(drop=True) + + return filtered_hierarchy + + +#%% =========================================================================== +### --- FILE PATHS & DISTRIBUTION INSTRUCTIONS --- +### =========================================================================== +def mcs_find_copy_paths( + sw_name: str, + sw_assignments: list, + runfiles: pd.DataFrame, + reeds_path: str, + inputs_case: str, + run_ReEDS: bool = True +) -> Tuple[list, pd.DataFrame]: + """ + Find the paths where the MCS samples should be copied to and the associated runfiles.csv rows. + + Args: + sw_name (str): The name of the switch being sampled. + sw_assignments (list): The assignments for the switch. + runfiles (pd.DataFrame): The runfiles.csv DataFrame. + reeds_path (str): The path to the ReEDS directory. + inputs_case (str): The path to the inputs case directory. + run_ReEDS (bool): Whether to run the ReEDS model or not. + + Returns: + save_path_list: A list of destination paths for the MCS samples. + runfile_instructions: The runfiles.csv rows associated with the switch. + """ + # Find if the switch name needs to be assigned to a specific file path in runfiles.csv + rf_contains_sw = runfiles['filepath'].fillna('').str.contains('{' + sw_name + '}') + if any(rf_contains_sw): + runfile_instructions = runfiles[rf_contains_sw].reset_index(drop=True) + elif sw_name in MCSConstants.HARD_CODED_SWITCH_TO_FILE_READ: + # If the switch name is found in the hardcoded exceptions, fid the rows + # in runfiles.csv that contain all the files associated with the switch. + runfile_instructions = runfiles[ + runfiles['filename'].isin(MCSConstants.HARD_CODED_SWITCH_TO_FILE_READ[sw_name]) + ].reset_index(drop=True) + else: + # If the switch name is not found in runfiles.csv, or in the hardcoded exceptions, + # assume it is only part of switches.csv + runfile_instructions = runfiles[runfiles['filename'] == 'switches.csv'].reset_index(drop=True) + + # Reorder rows: if any filename has "supply_curve", place those rows first. + # For siting data we need to sample the supply curve data first + # (CF,... is dependent on the supply curve data) + if runfile_instructions['filename'].str.contains('supply_curve', na=False).any(): + supply_curve_rows = runfile_instructions[runfile_instructions['filename'].str.contains('supply_curve', na=False)] + other_rows = runfile_instructions[~runfile_instructions['filename'].str.contains('supply_curve', na=False)] + runfile_instructions = pd.concat([supply_curve_rows, other_rows], ignore_index=True) + + # Iterate through each instruction to determine the destination paths. + # Since some switches point to multiple files, you can have multiple destination paths. + save_path_list = [] + for _, row in runfile_instructions.iterrows(): + file_name = row['filename'] + + if run_ReEDS: + dest_path = os.path.join(inputs_case, file_name) + else: + # Save samples at runs/Sample_ for later use. Useful for checking the samples. + dest_path = os.path.join( + reeds_path, 'runs', 'Sample_{sample_n}', file_name + ) + save_path_list.append(dest_path) + + # Supply curve files are used in other distributions, so they need to be first in the list. + # This should be cleaned up. + if any([os.path.basename(i).startswith('supplycurve') for i in save_path_list]): + supplycurve_index = [ + i for (i,f) in enumerate(save_path_list) + if os.path.basename(f).startswith('supplycurve') + ][0] + other_indices = [i for i in range(len(save_path_list)) if i != supplycurve_index] + index_order = [supplycurve_index] + other_indices + save_path_list = [save_path_list[i] for i in index_order] + runfile_instructions = runfile_instructions.loc[index_order].reset_index(drop=True) + + return save_path_list, runfile_instructions + + +def general_mcs_dist_validation(reeds_path: str, mcs_dist_path: str, sw: pd.Series) -> None: + """ + Validate the contents of mcs_distributions_{MCS_dist}.yaml used for Monte Carlo sampling. + + Args: + reeds_path (str): Path to the ReEDS directory. + mcs_dist_path (str): Path to the input .yaml file. + sw (pd.Series): Case switches + + Raises: + ValueError: If any structure or content in the .yaml file is invalid. + """ + print('Validating the input distribution information for Monte Carlo sampling...') + + with open(mcs_dist_path, 'r') as f: + data = yaml.safe_load(f) + df_input_dist = pd.DataFrame(data) + + mcs_dist_groups = sw['MCS_dist_groups'].split('.') + + # Read cases.csv to get the list of valid switches. + cases_default = pd.read_csv(os.path.join(reeds_path, 'cases.csv')) + valid_switches = cases_default.iloc[:, 0].values + + # Validate mandatory keys in df_input_dist + required_keys = {'name', 'assignments_list', 'dist', 'dist_params', 'weight_r'} + missing_keys = required_keys - set(df_input_dist.columns) + if missing_keys: + raise ValueError(f"Missing mandatory keys in mcs_distributions.yaml object: {missing_keys}") + + # Make sure that dist_params is a list + if not all(isinstance(df_input_dist.at[i, 'dist_params'], list) for i in range(len(df_input_dist))): + raise ValueError('The dist_params field must be a list') + + # Verify that all dist group names in mcs_distributions.yaml are unique. + if df_input_dist['name'].nunique() != len(df_input_dist): + raise ValueError('The distribution names in mcs_distributions.yaml are not unique. Please correct the file') + + # Ensure that we are not missing data for each row of the input distribution file. + missing_data = df_input_dist.isnull().sum(axis=1) + if missing_data.any(): + raise ValueError(f"The following dist names have missing data: {df_input_dist.loc[missing_data > 0, 'name'].values}. " + "Make sure you have all mandatory fields in the input distribution file") + + # Ignore all cases not in mcs_dist_groups + df_input_dist = df_input_dist[df_input_dist['name'].isin(mcs_dist_groups)].reset_index(drop=True) + + # Ensure all MCS_dist_groups options are present in the input distribution names. + missing = set(mcs_dist_groups) - set(df_input_dist['name'].unique()) + if missing: + raise ValueError(f"The following MCS_dist_groups switch options are missing in mcs_distributions.yaml {missing}") + + for i, sample_group in df_input_dist.iterrows(): + distribution = sample_group['dist'] + + switch_names = [next(iter(s)) for s in sample_group["assignments_list"]] + sw_assignments = [next(iter(s.values())) for s in sample_group["assignments_list"]] + + for d in sample_group["assignments_list"]: + if not (isinstance(d, dict) and len(d) == 1): + raise ValueError("Each item in assignments_list must be a single-key dictionary") + + val = next(iter(d.values())) + if not isinstance(val, list): + raise ValueError("The value in each dictionary must be a list") + + if distribution not in ["dirichlet", "discrete"] and any( + switch in MCSConstants.SITING_SWITCHES for switch in switch_names + ): + raise ValueError( + "The siting related switches can only be sampled " + "using a dirichlet or discrete distribution" + ) + + if distribution not in MCSConstants.VALID_DISTRIBUTIONS: + raise ValueError( + f"The distribution {distribution} is not supported." + f"Please choose one of the following: {MCSConstants.VALID_DISTRIBUTIONS}") + + if distribution in MCSConstants.MULTIPLICATIVE_DISTRIBUTIONS: + num_files = np.max([len(c) for c in sw_assignments]) + if num_files > 1: + raise ValueError( + f"The distribution {distribution} can only have a single reference file/value per switch." + ) + + # Iterate over each switch in the instruction. + for sw_name in switch_names: + # Check if the switch is valid. + if sw_name not in valid_switches: + raise ValueError(f'The switch {sw_name} is not a valid switch. Please check cases.csv') + + +def get_dist_instructions(reeds_path: str, inputs_case: str, run_ReEDS: bool = True) -> Tuple[pd.DataFrame, dict]: + """ + Obtain the instructions to sample the distributions for each switch + and organize information to facilitate the Monte Carlo sampling process. + + Args: + reeds_path (str): The path to the ReEDS directory. + inputs_case (str): The path to the inputs case directory. + run_ReEDS (bool): Whether to run the ReEDS model or not. + + Returns: + df_input_dist_ex: A DataFrame with the distribution instructions for each switch. + aux_files: A dictionary with auxiliary information (mostly used in the copy_files.py module). + """ + print('Reading the input distribution information for Monte Carlo sampling') + + # Read yaml file with the input distribution information. + mcs_dist_path = os.path.join(inputs_case, 'mcs_distributions.yaml') + with open(mcs_dist_path, 'r') as f: + data = yaml.safe_load(f) + df_input_dist = pd.DataFrame(data) + + sw = reeds.io.get_switches(inputs_case) + mcs_dist_groups = sw['MCS_dist_groups'].split('.') + + if not run_ReEDS: + # Since you did not run using run.py - check inputs here + general_mcs_dist_validation(reeds_path, mcs_dist_path, sw) + + # Ignore all cases not in mcs_dist_groups + df_input_dist = df_input_dist[df_input_dist['name'].isin(mcs_dist_groups)].reset_index(drop=True) + + # Expand df_input_dist with new information to facilitate the Monte Carlo sampling process. + # Sample ID here is used to uniquely identify each sample-process. + df_input_dist_ex = df_input_dist.copy(deep=True) + for col in ['Sample_ID', 'switch_names', 'sw_assignments', 'file_names', 'save_paths', 'runfiles_csv']: + df_input_dist_ex[col] = [[] for _ in range(len(df_input_dist))] + + # Save reeds_path and inputs_case for future use. + df_input_dist_ex['reeds_path'] = reeds_path + df_input_dist_ex['inputs_case'] = inputs_case + + agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) + # Read runfiles.csv to get instructions on how files must be copied. + runfiles, nonregion_files, region_files = copy_files.read_runfiles( + reeds_path, inputs_case, sw, agglevel_variables) + + ReEDS_resolution = sw['GSw_RegionResolution'] + # Process each distribution instruction. + for i, input_dist_row in df_input_dist.iterrows(): + + # If ReEDS_resolution is aggreg but weight_r is 'ba' change it to aggreg + if ReEDS_resolution == 'aggreg' and input_dist_row['weight_r'] == 'ba': + df_input_dist_ex.at[i, 'weight_r'] = 'aggreg' + print(f"[Warning]: The weight_r for {input_dist_row['name']} was changed to 'aggreg'") + + # Iterate over each switch in the instruction. + for sw_i, assignments_list in enumerate(input_dist_row['assignments_list']): + + sw_name, sw_assignments = next(iter(assignments_list.items())) + + filepaths, runfiles_csv = mcs_find_copy_paths( + sw_name, sw_assignments, runfiles, reeds_path, inputs_case, run_ReEDS=run_ReEDS + ) + + # handle cases where a single switch assignment is associated with multiple files and cases + # related to switches.csv, where multiple float switches may be associated with the same file. + for j in range(len(filepaths)): + file_name = runfiles_csv.iloc[j]['filename'] + df_input_dist_ex.at[i, 'switch_names'].append(sw_name) + df_input_dist_ex.at[i, 'sw_assignments'].append(sw_assignments) + df_input_dist_ex.at[i, 'save_paths'].append(filepaths[j]) + df_input_dist_ex.at[i, 'runfiles_csv'].append(runfiles_csv.iloc[j]) + df_input_dist_ex.at[i, 'file_names'].append(runfiles_csv.iloc[j]['filename']) + + if file_name != 'switches.csv': + df_input_dist_ex.at[i, 'Sample_ID'].append(f'{file_name}') + else: + df_input_dist_ex.at[i, 'Sample_ID'].append(f'{sw_name}') + + # Obtain the data used by copy_files.py to filter regions and create tailored dataframes. + regions_and_agglevel = copy_files.get_regions_and_agglevel( + reeds_path, inputs_case, save_regions_and_agglevel=False) + + source_deflator_map = copy_files.get_source_deflator_map(reeds_path) + + hierarchy_file = get_hierarchy_file(inputs_case, sw['GSw_RegionResolution']) + + # Save the auxiliary info in a dictionary. + aux_files = { + 'sw': sw, + 'nonregion_files': nonregion_files, + 'region_files': region_files, + 'source_deflator_map': source_deflator_map, + 'regions_and_agglevel': regions_and_agglevel, + 'agglevel_variables': agglevel_variables, + 'hierarchy_file': hierarchy_file, + } + + return df_input_dist_ex, aux_files + + +#%% =========================================================================== +### --- WEIGHT CALCULATION --- +### =========================================================================== +def get_region_weights(distribution: str, dist_params: list, n_samples: int = 1) -> np.ndarray: + """ + Generate weights for a single region based on the assigned distribution. + + Args: + distribution (str): The distribution to use for sampling. + dist_params (list): The parameters for the distribution. + n_samples (int): The number of samples to generate. + + Returns: + np.ndarray: The weights for the region-based sample ([n_samples, n_ref_files|values]). + """ + if distribution == "dirichlet": + r_weights = np.random.dirichlet(dist_params, n_samples) + + elif distribution == "discrete": + prob = np.array(dist_params) / np.sum(dist_params) + sampled_indices = np.random.choice(len(dist_params), n_samples, p=prob) + r_weights = np.zeros((n_samples, len(dist_params)), dtype=int) + r_weights[np.arange(n_samples), sampled_indices] = 1 + + elif distribution == "uniform_multiplier": + r_weights = np.random.uniform(dist_params[0], dist_params[1], n_samples) + + elif distribution == "triangular_multiplier": + r_weights = np.random.triangular(dist_params[0], dist_params[1], dist_params[2], n_samples) + + # Make sure r_weights is a 2D array + if r_weights.ndim == 1: + r_weights = r_weights[:, np.newaxis] + + return r_weights + + +def get_all_region_weights( + distribution: str, + dist_params: list, + hierarchy_file: pd.DataFrame, + sample_hierarchy_lvl: str = 'country', +) -> dict: + """ + Get the weights for all unique regions in sample_hierarchy_lvl and map them to the + relevant BAs and cendivs, levels. Those may be adjusted later for supply curve files + (in this case they may be combined with capacity data) + + Args: + distribution (str): The distribution to use for sampling. + dist_params (list): The parameters for the distribution. + hierarchy_file (pd.DataFrame): DataFrame with the hierarchy information from get_hierarchy_file (.) + sample_hierarchy_lvl (str): The hierarchy level which will be assigned unique weights. + + Returns: + dict: Dictionary with the weights for each region. + """ + + # Only needs to map weights to 'ba', and 'cendiv' + # levels since these are the only levels relevant to the files changed in the mcs sampling + all_r_weights = {} + unique_sample_levels = hierarchy_file[sample_hierarchy_lvl].unique() + + for region in unique_sample_levels: + # Generate region weights based on the specified distribution + r_weights = get_region_weights(distribution, dist_params) + + # Retrieve all BAs linked to the current region + bas = hierarchy_file.loc[hierarchy_file[sample_hierarchy_lvl] == region, "ba"].values + + # Assign weights to each BA, cendiv, and aggreg + for ba in bas: + all_r_weights[ba] = r_weights + + # Only save the cendiv weights if the sample_hierarchy_lvl is 'country' or 'cendiv' + if sample_hierarchy_lvl in ["country", "cendiv"]: + cendivs = hierarchy_file.loc[hierarchy_file[sample_hierarchy_lvl] == region, "cendiv"].unique() + for cendiv in cendivs: + all_r_weights[cendiv] = r_weights + + return all_r_weights + + +class WeightCalculator: + """ + Computes region-based weights for Monte Carlo Sampling in ReEDS. + + Args: + sample_group (pd.Series): a series with information about the sample group - from get_dist_instructions(.). + This contains the distribution, dist_params, switch_names, sw_assignments, file_names, save_paths, ... + It is a row of the df_input_dist_ex DataFrame. + aux_files (dict): Dictionary with auxiliary information - from get_dist_instructions (.) + n_samples (int): The number of samples + """ + def __init__( + self, + sample_group: pd.Series, + aux_files: dict, + n_samples: int = 1, + ): + self.sample_group = sample_group + self.aux_files = aux_files + self.distribution = sample_group['dist'] + self.dist_params = sample_group['dist_params'] + self.sample_hierarchy_lvl = sample_group['weight_r'].lower() + self.hierarchy_file = aux_files['hierarchy_file'] + self.n_samples = n_samples + + # Get all general region weights + self.r_weights = get_all_region_weights( + self.distribution, self.dist_params, self.hierarchy_file, self.sample_hierarchy_lvl) + ## Include aggregated region weights + if aux_files['sw']['GSw_RegionResolution'] == 'aggreg': + self.r_weights = { + **self.r_weights, + **{ + aux_files['hierarchy_file'].set_index('ba').aggreg.get(k,k): v + for k,v in self.r_weights.items() + }, + } + + # Store the weights for the recf files (CF files) + # Those are computed during the the supply curve file sampling + self.recf_weights_map = {} + + # Flag to validade that recf_weights_map was normalized + self.flag_recf_normalization = defaultdict(lambda: False) + + def _validate_inputs(self, dist_files: list, sw_name: str, file_name: str) -> None: + """ + Validate inputs + + Args: + dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) + sw_name (str): Name of the switch we are getting the weights for. + file_name (str): Name of the file we are getting the weights for. + """ + # Identify relevant columns that exist in the hierarchy + #Examples: p1, p2, New_England, ...(covers NG and LOAD) + columns_in_hierarchy = [col for col in dist_files[0].keys() if col in set(self.r_weights.keys())] + + # Columns that start with region, r, ... + generic_region_columns = [col for col in dist_files[0].keys() if col in MCSConstants.REGION_SYNONYMS] + + # We have as many unique weights as len(unique_sample_levels) + unique_sample_levels = self.hierarchy_file[self.sample_hierarchy_lvl].unique() + single_r_weight = len(unique_sample_levels) == 1 + + # Group files that require special treatment + except_files = MCSConstants.SUPPLY_CURVE_FILES + MCSConstants.EXOG_CAP_FILES + ( + MCSConstants.PRESCRIBED_BUILDS_FILES + MCSConstants.RECF_FILES) + + # Return an error if you have multiple weight assignments but the mcs_distributions.yaml object is + # pointing to a set of switches that have no region columns + # e.g. asking for a region-based sampling for swicthes.csv, or plantchar type files. + if not single_r_weight and not columns_in_hierarchy and not generic_region_columns and ( + file_name not in except_files): + raise ValueError( + f"Invalid sampling configuration for file: {file_name}\n" + f"Switch: {sw_name}\n" + f"weight_r group: {self.sample_hierarchy_lvl}\n" + "[Error] Either:\n" + " 1. The file does not contain any regional columns but was assigned" + " to a region-based sampling group different than country, or\n" + " 2. The selected weight_r resolution is not valid for this file" + " (e.g., BA for NG fuel prices, which are based on cendiv).\n\n" + "Please review the `mcs_distributions.yaml` configuration." + ) + + # Check if all elements in dist_files have the same index + if not all(df.index.equals(dist_files[0].index) for df in dist_files): + raise ValueError( + f"Invalid sampling configuration for file: {file_name}\n" + f"Switch: {sw_name}\n" + "All reference files must have the same indexes" + ) + + # Check if the distribution is multiplicative and the file has a year column + if self.distribution in MCSConstants.MULTIPLICATIVE_DISTRIBUTIONS: + if dist_files[0].columns.isin(MCSConstants.YEAR_SYNONYMS).any(): + raise ValueError( + "Files with year columns are not supported for multiplicative distributions. " + f"Change the distribution for switch {sw_name}" + ) + + def get_df_weights( + self, + dist_files: list, + modifiable_columns: list, + sw_name: str, + file_name: str, + ) -> dict: + """ + Dispatch to the appropriate method based on file type. + + Args: + dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) + modifiable_columns (list of str): List of columns that can be directly multiplied by the weights. + sw_name (str): Name of the switch we are getting the weights for. + file_name (str): Name of the file we are getting the weights for. + + Returns: + dict: Dictionary with the weights for each reference file and sample. + """ + + self._validate_inputs(dist_files, sw_name, file_name) + + if file_name in MCSConstants.SUPPLY_CURVE_FILES: + return self._get_weights_supply_curve(dist_files, modifiable_columns, sw_name) + elif file_name in MCSConstants.RECF_FILES: + return self._get_weights_recf(sw_name) + elif file_name in MCSConstants.EXOG_CAP_FILES + MCSConstants.PRESCRIBED_BUILDS_FILES: + return self._get_weights_exog_prescribed(dist_files) + else: + return self._get_weights_general(dist_files, modifiable_columns, sw_name, file_name) + + def _get_weights_general( + self, + dist_files: list, + modifiable_columns: list, + sw_name: str, + file_name: str + ) -> dict: + """ + Get weights for a general file that does not require special treatment. + Files that require special treatment are those associated with + supply curve switches. + + Args: + dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) + modifiable_columns (list of str): List of columns that can be directly multiplied by the weights. + sw_name (str): Name of the switch we are getting the weights for. + file_name (str): Name of the file we are getting the weights for. + + Returns: + dict: Dictionary with the weights for each reference file and sample. + """ + # Number of reference files/values. Sicne all sw_assignments + # have the same number of files, we can use the first one. + n_files = len(self.sample_group["sw_assignments"][0]) + + # Identify relevant columns that exist in the hierarchy + #Examples: p1, p2, New_England, ...(covers NG and LOAD) + columns_in_hierarchy = [col for col in dist_files[0].keys() if col in set(self.r_weights.keys())] + + # We have as many unique weights as len(unique_sample_levels) + unique_sample_levels = self.hierarchy_file[self.sample_hierarchy_lvl].unique() + single_r_weight = len(unique_sample_levels) == 1 + + # Dictionary to store computed weights for the modifiable columns + # (sample, file) -> pd.DataFrame + dict_df_weights = {} + + # Handle the simple case where there is only one weight for all regions + # (or no regions). + if single_r_weight: + # Get the first region key + first_region = next(iter(self.r_weights)) + weight_matrix = self.r_weights[first_region] + + if file_name == "switches.csv": + for s in range(self.n_samples): + for f in range(n_files): + dict_df_weights[(s, f)] = weight_matrix[s, f] + else: + for s in range(self.n_samples): + for f in range(n_files): + dict_df_weights[(s, f)] = pd.DataFrame( + data=weight_matrix[s, f], + columns=modifiable_columns, + index=dist_files[0].index, + ) + + # Cases that have regional columns from columns_in_hierarchy + # and the weights are not the same for all regions + elif not single_r_weight and len(columns_in_hierarchy) and file_name != "switches.csv" : + for s in range(self.n_samples): + for f in range(n_files): + + w_df_tmp = pd.DataFrame( + {col: self.r_weights[col][s, f] for col in columns_in_hierarchy}, + index=dist_files[0].index + ) + + dict_df_weights[(s, f)] = w_df_tmp + + return dict_df_weights + + def _get_weights_supply_curve( + self, + dist_files: list, + modifiable_columns: list, + sw_name: str + ) -> dict: + """ + Get the weights for supply curve files. These files require special treatment + because some columns are dependent on the capacity of each sc_point_gid. + + Args: + dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) + modifiable_columns (list of str): List of columns that can be directly multiplied by the weights. + sw_name (str): Name of the switch we are getting the weights for. + + Returns: + dict: Dictionary with the weights for each reference file and sample. + """ + # Dictionary to store computed weights for the modifiable columns + # (sample, file) -> pd.DataFrame + dict_df_weights = {} + + # Store weights to use later in the recf files (CF files) + self.recf_weights_map [sw_name] = {} + + # Create a new column with the class|region combination (like in the CF file) + dist_files_copy = [copy.deepcopy(df) for df in dist_files] + + for df in dist_files_copy: + df["old c|r"] = ( + df["class"].astype(int).astype(str) + "|" + df["region"].astype(str) + ) + + for s in range(self.n_samples): + for f, df in enumerate(dist_files_copy): + # Initial skeleton of the weights DataFrame + w_df_tmp = df[["region", "sc_point_gid", "old c|r"]] + + # Create a mapping from each unique region to its corresponding weight + region_to_weight = { + r: self.r_weights[r][s, f] + for r in w_df_tmp["region"].unique() + } + + # Compute the region weights for each row + region_weights = w_df_tmp["region"].map(region_to_weight).values + + # Build a new DataFrame for the modifiable columns using a dict comprehension. + # For "capacity", we assign the raw region weight; for others, multiply by capacity. + modifiable_df = pd.DataFrame({ + col: (region_weights if col == "capacity" else region_weights * df["capacity"]) + for col in modifiable_columns + }, index=w_df_tmp.index) + + # Join the modifiable columns back into the original DataFrame + w_df_tmp = w_df_tmp.join(modifiable_df) + + # Save the intermediate weights for the recf files (These are weights multiplied by capacity) + self.recf_weights_map [sw_name][(s, f)] = w_df_tmp[["old c|r","class"]].rename(columns={"class": "weight"}) + + # Store in dictionary + dict_df_weights[(s, f)] = w_df_tmp.drop(columns=["old c|r"]) + + # Normalize the weights to sum to 1 + # Divide the weights by the sum of the weights across all files + sum_weights = sum(dict_df_weights[(s, f)][modifiable_columns] for f in range(len(dist_files))) + sum_weights[sum_weights == 0] = 1 + + for f in range(len(dist_files)): + dict_df_weights[(s, f)][modifiable_columns] /= sum_weights + # recf_weights_map is not normalized here because it will be normalized later + # values need to be aggregated according to the new c|r column from supply curves + + return dict_df_weights + + def _get_weights_exog_prescribed(self, dist_files: list) -> dict: + """ + Get the weights for exogenous capacity and prescribed builds files. + + Args: + dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) + + Returns: + dict: Dictionary with the weights for each reference file and sample. + """ + dict_df_weights = {} + for s in range(self.n_samples): + for f, df in enumerate(dist_files): + + region_to_weight = { + r: self.r_weights[r][s, f] + for r in df["region"].unique() + } + + dict_df_weights[(s, f)] = pd.DataFrame( + data=df["region"].map(region_to_weight).values, + columns=["capacity"], + index=df.index, + ) + + return dict_df_weights + + def _get_weights_recf(self, sw_name: str) -> dict: + """ + Get the weights for the recf files (CF files). This file construction is + dependent on the new supply curve samples and therefore is computed after + the supply curve files are sampled. + + Args: + sw_name (str): Name of the switch we are getting the weights for. + """ + # From get_dist_instructions(.) the supply curve file is deliberaly + # placed before the recf files, so that recf_weights_map is already populated. + + # Check if recf_weights_map is not empty and that it was normalized. + if not self.recf_weights_map[sw_name]: + raise ValueError( + f"The recf_weights_map for switch {sw_name} was not populated" + ) + + if not self.flag_recf_normalization[sw_name] : + raise ValueError( + f"The recf_weights_map for switch {sw_name} was not normalized" + ) + + return self.recf_weights_map[sw_name] + + def normalize_recf_weights_map(self, samples_sw: list, sw_name: str) -> None: + """ + The recf map is responsible for informing how the old class/region data files + need to be put together (weights) to form the new class/region data. + After creating the new supply curve sample, we normalize the weights to sum to 1. + + Args: + samples_sw (list of pd.DataFrame): List of samples for the supply curve files. + sw_name (str): Name of the switch being sampled. + + Updates: + self.recf_weights_map (dict): Dictionary with the normalized weights for the recf files. + Each element of this dictionary is a pd.DataFrame (for the sample s and reference file f) + with the normalized weights, indexed by new and old class|region (c|r). + """ + n_files = len({key[1] for key in self.recf_weights_map[sw_name].keys()}) + + for s in range(self.n_samples): + # The normalization can change depending on the sample # + for f in range(n_files): + # Add a new column with the new class|region combination + self.recf_weights_map[sw_name][(s, f)]["new c|r"] = ( + samples_sw[s]["class"].astype(str) + "|" + + samples_sw[s]["region"].astype(str) + ) + + # Sum the weights for each new class|region combination + self.recf_weights_map[sw_name][(s, f)] = self.recf_weights_map[sw_name][(s, f)].groupby( + ["new c|r","old c|r"], as_index=False).sum() + + # Remove cases with 0 weight (e.g old c|r had no capacity -> class 0) + self.recf_weights_map[sw_name][(s, f)] = self.recf_weights_map[sw_name][(s, f)][ + self.recf_weights_map[sw_name][(s, f)]["weight"] > 0 + ] + + # Go over all files and obtain the total sum of weights for each new class|region + sum_weights_recf_map = ( + pd.concat( + [self.recf_weights_map[sw_name][(s, f)] for f in range(n_files)] + ) + .groupby("new c|r")["weight"] + .sum() + .to_dict() + ) + + for f in range(n_files): + # Get current DataFrame + df = self.recf_weights_map[sw_name][(s, f)] + # Perform division using "new c|r" as the reference + df["weight"] = df["weight"] / df["new c|r"].map(sum_weights_recf_map) + # Assign back to original structure + self.recf_weights_map[sw_name][(s, f)] = df.set_index(["new c|r", "old c|r"]) + + # Flag to validade that recf_weights_map was normalized + self.flag_recf_normalization[sw_name] = True + + +#%% =========================================================================== +### --- MAIN SAMPLING CLASS --- +### =========================================================================== +class MCS_Sampler: + """ + Monte Carlo Sampling Distribution Manager for ReEDS. + + This class allows enforcing sampling variability at different ReEDS regions + (st, ba, ...) and enforcing correlation between samples from differen switches. + + The following sampling strategies have been implemented: + + 1. Dirichlet Sampling: + - Generates a Dirichlet sample (h1, ..., hn) ~ Dir(alpha1, ..., alphan). + - Uses these weights to compute a weighted average of reference files: + sample = h1*f1 + ... + hn*fn + + 2. Discrete Sampling: + - Chooses a single reference file based on a discrete probability distribution. + + 3. Multiplicative Sampling: + - Applies a random multiplier to a single reference file or switch. + - The multiplier is drawn from a uniform or triangular distribution: + sample = multiplier * f + + """ + def __init__(self, sample_group, aux_files, n_samples=1): + self.sample_group = sample_group + self.aux_files = aux_files + self.n_samples = n_samples + + # Derive parameters from inputs + self.reeds_path = sample_group['reeds_path'] + self.inputs_case = sample_group['inputs_case'] + self.distribution = sample_group['dist'] + self.dist_params = sample_group['dist_params'] + self.ReEDS_resolution = aux_files['sw']['GSw_RegionResolution'] + if self.ReEDS_resolution=='aggreg' and sample_group['weight_r']=='ba': + self.sample_hierarchy_lvl = 'aggreg' + else: + self.sample_hierarchy_lvl = sample_group['weight_r'] + + # Inputs that require special treatment + self.hierarchy_file = get_hierarchy_file(self.inputs_case, self.ReEDS_resolution) + + # Store the samples for each switch (a single sw may have multiple files that is + # why we refer to the switch by its adjusted name) + self.samples = {sw_name: [] for sw_name in self.sample_group['Sample_ID']} + + # Instantiate WeightCalculator. + self.weight_calc = WeightCalculator(sample_group, aux_files, n_samples) + + @staticmethod + def prepare_ref_data( + dist_files: list, + file_name: str, + sw_name: str | list, + aux_files, + ) -> Tuple[list, list, dict]: + """ + This function prepares the reference dataframes for the Monte Carlo sampling. + For some files like those related to supply curves we need to expand/modify + the reference files to include additional rows/columns. + + Args: + dist_files (list of pd.DataFrame): List of reference dataframes to be modified. + file_name (str): name given by reeds to the files in dist_files. + sw_name (str or list): Name of the switch being sampled. For the special case + of float switches, this will be a list of switch names. + + Returns: + list of pd.DataFrame: List of modified reference dataframes. + list of str: List of columns that can be directly multiplied by the weights. + dict: Dictionary with the number of decimal places for columns we will modify + """ + ### =========================================================================== + ### --- Expand dist_files if necessary --- + ### =========================================================================== + # For each file map the columns we need to verify in the df expansion + # (e.g For the supply curves we will make sure that all files are + # ajusted to contain all regions and sc_point_gid combinations) + map_files2ref_columns = { + **{file: ["region", "sc_point_gid"] for file in MCSConstants.SUPPLY_CURVE_FILES}, + **{file: ["region", "year", "sc_point_gid"] for file in MCSConstants.EXOG_CAP_FILES}, + **{file: ["region", "year"] for file in MCSConstants.PRESCRIBED_BUILDS_FILES}, + } + + if file_name in map_files2ref_columns: + ref_columns = map_files2ref_columns[file_name] + + # Get all unique combination for the reference columns + unique_reg_gid_point = pd.concat( + [df[ref_columns] for df in dist_files], + ignore_index=True + ).drop_duplicates().reset_index(drop=True) + + # Modify dfs in the dist_files list adding missing ref_columns combinations + # and initializing the modifiable rows with 0 + for i, df in enumerate(dist_files): + dist_files[i] = unique_reg_gid_point.merge( + df.reset_index(drop=True), + on=ref_columns, + how="left", + ).fillna(0).sort_values(by=ref_columns).reset_index(drop=True) + + ### =========================================================================== + ### --- Get a list of the columns we are allowed to apply weights directly --- + ### =========================================================================== + # Get the base set of general (modifiable) columns from dist_files[0] + general_mult_columns = { + col for col in dist_files[0].keys() if col not in MCSConstants.FIXED_COLUMN_NAMES + } + + # Ensure all dist_files have the same set of general columns + if file_name not in MCSConstants.RECF_FILES: + for i, df in enumerate(dist_files[1:], start=1): + current_cols = {col for col in df.keys() if col not in MCSConstants.FIXED_COLUMN_NAMES} + if current_cols != general_mult_columns: + error_msg = ( + f"Column mismatch between dist_files[0] and dist_files[i]:\n" + "This usually happens when you run MCS on a file whose columns " + "vary by switch assignment (e.g. RECF_FILES).\n If you really need to support " + f"'{file_name}' here, add the necessary handling in prepare_ref_data()." + ) + raise ValueError(error_msg) + + exceptions_mult_col = { + **{file: ["class"] + list(general_mult_columns) for file in MCSConstants.SUPPLY_CURVE_FILES}, + **{file: ["capacity"] for file in MCSConstants.EXOG_CAP_FILES}, + **{file: ["capacity"] for file in MCSConstants.PRESCRIBED_BUILDS_FILES}, + **{file: [] for file in MCSConstants.RECF_FILES}, # treated separately + } + modifiable_columns = exceptions_mult_col.get(file_name, list(general_mult_columns)) + + ### =========================================================================== + ### --- Map for the number of decimals in each column we will change + ### =========================================================================== + if file_name in ["switches.csv"]: + n_decimals = max_decimal_places(dist_files[0].loc[sw_name,1]) + else: + n_decimals_list = [ + max_decimal_places(df[modifiable_columns]) for df in dist_files + ] + + # Take the max decimal count per column across all files, capped at 6 + n_decimals = { + col: min(max(d[col] for d in n_decimals_list), 6) + for col in n_decimals_list[0] + } + + return dist_files, modifiable_columns, n_decimals + + def load_ref_files(self, sample_idx: int) -> List[pd.DataFrame]: + """ + Load the reference files associated with the sample. + + Args: + sample_idx (int): Index of the Sample_ID in sample_group. + Some switches have multiple files associated with them that is why we + track samples using Sample_ID in the sample_group. + + Returns: + List[pd.DataFrame]: List of DataFrames with the switch files. + """ + + sw_name = self.sample_group['switch_names'][sample_idx] + + # Create a list of dataframes with the data related to the switch + dist_files = [] + + for sw_assignment in self.sample_group['sw_assignments'][sample_idx]: + + sw_runfiles_csv = self.sample_group['runfiles_csv'][sample_idx].copy(deep=True) + sw_runfiles_csv['sw_assignment'] = sw_assignment + + if not pd.isna(sw_runfiles_csv['filepath']): + sw_runfiles_csv['full_filepath'] = os.path.join( + self.reeds_path, + sw_runfiles_csv['filepath'].replace(f'{{{sw_name}}}', sw_assignment), + ) + + df = read_csv_h5_file(sw_runfiles_csv, self.aux_files, self.reeds_path, self.inputs_case) + dist_files.append(df) + + return dist_files + + # ----------------------- Weight Application Helpers ----------------------- + def _adjust_supply_curve_sample(self, samples_sw: list, sw_name: str, sample_idx: int) -> list: + """ + Adjust samples for supply curve files: + - Convert the 'class' column to integers. + - Normalize the weights map. + - Remove rows with no capacity. + + Args: + samples_sw (list of pd.DataFrame): List of samples for the supply curve files. + sw_name (str): Name of the switch being sampled. + sample_idx (int): Index of the Sample_ID in sample_group. + + Returns: + list of pd.DataFrame: List of adjusted samples for the supply curve files. + """ + + for s in range(self.n_samples): + # Convert class to integer + samples_sw[s]["class"] = samples_sw[s]["class"].astype(int) + + # Update the recf weights map (weight_calc.recf_weights_map) + self.weight_calc.normalize_recf_weights_map(samples_sw, sw_name) + + # Remove samples with no capacity. + # Need to do this after normalizing the recf weights + samples_sw = [df[df["capacity"] > 0].copy() for df in samples_sw] + + return samples_sw + + def _adjust_exog_cap_samples(self, samples_sw: list, file_name: str) -> list: + """ + Adjust samples for exogenous capacity files: + - Remove rows with no capacity. + - Adjust the tech classes based on available classes per sc_point_gid. + + Args: + samples_sw (list of pd.DataFrame): List of samples for the exogenous capacity files. + file_name (str): Name of the file being sampled. + """ + # Remove samples with no capacity + samples_sw = [df[df["capacity"] > 0].copy() for df in samples_sw] + + tech_mapping = { + "exog_cap_upv.csv": ("upv", "supplycurve_upv.csv"), + "exog_cap_wind-ons.csv": ("wind-ons", "supplycurve_wind-ons.csv"), + } + tech_name, Sample_ID = tech_mapping[file_name] + + for s in range(self.n_samples): + # Get the class available for each sc_point_gid + class_sc_point_map = self.samples[Sample_ID][s][["sc_point_gid", "class"]] + class_sc_point_map = class_sc_point_map.set_index("sc_point_gid").to_dict()["class"] + + # Remove any rows from samples_sw[s] that cannot be mapped + # These are cases with zero supply in the region + valid_sc_point_gids = samples_sw[s]["sc_point_gid"].isin(class_sc_point_map.keys()) + samples_sw[s] = samples_sw[s][valid_sc_point_gids].copy() + + # Create a new tech name for each sc_point_gid + new_tech_name = [tech_name + "_" + str(int(c)) for c in + samples_sw[s]["sc_point_gid"].map(class_sc_point_map).values] + + samples_sw[s]["*tech"] = new_tech_name + + return samples_sw + + def _apply_weights_general( + self, + dist_files: list, + modifiable_columns: list, + n_decimals: dict|int, + dict_df_weights: dict, + sample_idx: int + ): + """ + Apply the distribution weights to the reference files. + Applicable to all cases but recf files and switches.csv. + + Args: + dist_files (List[pd.DataFrame]): List of input DataFrames for sampling. + modifiable_columns (List[str]): List of columns that can be directly multiplied by the weights. + n_decimals (Dict[str, int]): Dictionary with the number of decimal places for each column. + dict_df_weights (Dict[Tuple[int, int], pd.DataFrame]): Dictionary with the weights for each reference file and sample. + sample_idx (int): Index of the Sample_ID in sample_group. + + Update: + self.samples (Dict[str, List[pd.DataFrame]]): Dictionary with the samples for each switch/file_name. + """ + + Sample_ID = self.sample_group['Sample_ID'][sample_idx] + sw_name = self.sample_group['switch_names'][sample_idx] + file_name = self.sample_group["runfiles_csv"][sample_idx]["filename"] + + # Initialize samples with zero values + samples_sw = [dist_files[0].copy() for n in range(self.n_samples)] + + for s in range(self.n_samples): + # Initialize with zeros + samples_sw[s][modifiable_columns] = 0 + + for f, df in enumerate(dist_files): + samples_sw[s][modifiable_columns] += df[modifiable_columns] * dict_df_weights[s, f][modifiable_columns] + + for col in modifiable_columns: + samples_sw[s][col] = samples_sw[s][col].round(n_decimals[col]+1) + + if file_name in MCSConstants.SUPPLY_CURVE_FILES: + adjusted_samples = self._adjust_supply_curve_sample(samples_sw, sw_name, sample_idx) + + elif file_name in MCSConstants.EXOG_CAP_FILES: + adjusted_samples = self._adjust_exog_cap_samples(samples_sw, file_name) + + elif file_name in MCSConstants.PRESCRIBED_BUILDS_FILES: + # Remove samples with no capacity + adjusted_samples = [df[df["capacity"] > 0].copy() for df in samples_sw] + + else: + # For all other files we can directly apply the weights + adjusted_samples = samples_sw + + # Save the adjusted samples. + self.samples[Sample_ID] = adjusted_samples + + def _apply_weights_recf( + self, + dist_files: list, + sample_idx: int + ): + """ + Apply the distribution weights to the recf files. + This file gets compleatly overwriten so need to be treated separately + + Args: + dist_files (List[pd.DataFrame]): List of input DataFrames for sampling. + sample_idx (int): Index of the Sample_ID in sample_group. + + Update: + self.samples (Dict[str, List[pd.DataFrame]]): Dictionary with the samples for each switch/file_name. + """ + + Sample_ID = self.sample_group['Sample_ID'][sample_idx] + sw_name = self.sample_group['switch_names'][sample_idx] + + # For the recf files we need to apply the weights to the old class|region combinations + weights = self.weight_calc.recf_weights_map[sw_name] + # Index is the same for all files (time) + indexes = dist_files[0].index + + samples_sw = [] + + for s in range(self.n_samples): + sample_sw = defaultdict(int) + + for f, df in enumerate(dist_files): + # Get the old and new class|region combinations from weights[(s, f)] + for (new_c_r, old_c_r) in weights[(s, f)].index: + sample_sw[new_c_r] += df[old_c_r] * weights[(s, f)].loc[(new_c_r,old_c_r)].values[0] + + # Round numbers to 9 decimal places and allow a maxium values of 1 + for new_c_r in sample_sw.keys(): + sample_sw[new_c_r] = sample_sw[new_c_r].round(9).clip(0,1) + + samples_sw.append(pd.DataFrame(sample_sw, index=indexes)) + + self.samples[Sample_ID] = samples_sw + + def _apply_weights_switches_csv( + self, + dist_files: list, + n_decimals: dict, + dict_df_weights: dict, + sample_idx: int, + ): + """ + Apply the distribution weights to the switches.csv file. + + Args: + dist_files (List[pd.DataFrame]): List of input DataFrames for sampling. + n_decimals (Dict[str, int]): Dictionary with the number of decimal places for each column. + dict_df_weights (Dict[Tuple[int, int], pd.DataFrame]): Dictionary with the weights for each reference file and sample. + sample_idx (int): Index of the Sample_ID in sample_group. + + Update: + self.samples (Dict[str, List[pd.DataFrame]]): Dictionary with the samples for each switch/file_name. + """ + # Switches are saved only for the rows changed because this allow + # multiple json objects changing different switches using different distributions + + sw_name = self.sample_group['switch_names'][sample_idx] + + samples_sw = [None for n in range(self.n_samples)] + sw_assignments = self.sample_group['sw_assignments'][sample_idx] + + for s in range(self.n_samples): + for assingment_idx, sw_assignment in enumerate(sw_assignments): + + # The switch assignments case can be a int, a float or a string + # If it is a str or int it must be used in a discrete distribution + if isinstance(sw_assignment, (str, int)): + # Check if we have a discrete distribution + if self.distribution != "discrete": + raise ValueError( + f"You specified a str/int assignment for switch '{sw_name}', " + "but the distribution is not set to 'discrete'. " + "This file is likely hard-coded in `copy_files.py`.\n\n" + "To fix this, you can try to:\n" + " - Change the distribution to 'discrete'\n" + " - Use a float assignment instead, or\n" + " - Add support for this switch's files\n\n" + "A good place to start is the `read_exception_file()` function." + ) + + if isinstance(sw_assignment, str) and dict_df_weights[s, assingment_idx]: + # dict_df_weights[s,f] is a one hot encoding of the sw_assignment options + samples_sw[s] = sw_assignment + + elif isinstance(sw_assignment, int) and dict_df_weights[s, assingment_idx]: + # dict_df_weights[s,f] is a one hot encoding of the sw_assignment options + samples_sw[s] = str(sw_assignment) + + elif isinstance(sw_assignment, float): + if self.distribution == "discrete": + # dict_df_weights[s,f] is a one hot encoding of the sw_assignment options + samples_sw[s] = str(sw_assignment) + + elif self.distribution in MCSConstants.MULTIPLICATIVE_DISTRIBUTIONS: + # We have a validation process that makes sure that we only have one file + samples_sw[s] = ( + str(np.round(sw_assignment * dict_df_weights[s, 0], n_decimals+1)) + ) + else: + raise ValueError( + f"Float assignments can only be used with a discrete or multiplicative distribution. " + f"Check the distribution for switch '{sw_name}'." + ) + + self.samples[sw_name] = samples_sw + + def record_group_weights(self, log_folder: str) -> None: + """ + Record the weights for each distribution group in + lstfiles/mcs_group_weights.csv. Appends to the file if it exists. + + Args: + mcs_sampler (MCS): The MCS object containing the distribution groups and weights. + log_folder (str): Directory where the weights file will be stored. + """ + os.makedirs(log_folder, exist_ok=True) + save_path = os.path.join(log_folder, 'mcs_group_weights.csv') + r_weights = self.weight_calc.r_weights + group_name = self.sample_group["name"] + assignments_list = self.sample_group["assignments_list"] + + # Get shape of any region’s weight array + any_region = next(iter(r_weights)) + n_samples, n_assignments = r_weights[any_region].shape + + # Build column names + columns = ( + ['group_name', 'switch_name', 'sw_assignment', 'r'] + + [f"Weight Sample {i}" for i in range(1, n_samples + 1)] + ) + + data = [] + + for switch_idx, switch_dict in enumerate(assignments_list): + sw_name, sw_assignment = next(iter(switch_dict.items())) + for r in r_weights.keys(): + for assignment_idx, assignment_value in enumerate(sw_assignment): + weights = list(r_weights[r][:, assignment_idx]) # column for this switch + row = [group_name, sw_name, assignment_value, r] + weights + data.append(row) + + weight_record_df = pd.DataFrame(data, columns=columns) + + # Append if file exists, else write with header + if os.path.exists(save_path): + weight_record_df.to_csv(save_path, mode='a', index=False, header=False) + else: + weight_record_df.to_csv(save_path, mode='w', index=False, header=True) + # ----------------------- End of Weight Application Helpers ----------------------- + + def get_samples(self, aux_files): + """ + Generates Monte Carlo samples for each switch and applies the appropriate weight assignment. + + Returns: + Dict[str, List[pd.DataFrame]]: Dictionary with the samples for each switch/file_name. + """ + # Iterate over each switch file and apply the appropriate weight assignment method + for sample_idx, sample_ID in enumerate(self.sample_group['Sample_ID']): + + sw_name = self.sample_group['switch_names'][sample_idx] + file_name = self.sample_group["file_names"][sample_idx] + dist_files = self.load_ref_files(sample_idx) + + #In some small cases all dist_files are empty + if not all([len(df) for df in dist_files]): + self.samples[sample_ID] = [dist_files[0] for s in range(self.n_samples)] + continue + + # Extend/modify dist_files if necessary (e.g supply curve related data) + dist_files, modifiable_columns, n_decimals = self.prepare_ref_data( + dist_files, file_name, sw_name, aux_files, + ) + + # Get weights we will apply to the reference files + dict_df_weights = self.weight_calc.get_df_weights(dist_files, modifiable_columns, sw_name, file_name) + + # Dispatch weight application based on file type. + if file_name == "switches.csv": + self._apply_weights_switches_csv(dist_files, n_decimals, dict_df_weights, sample_idx) + elif file_name in MCSConstants.RECF_FILES: + self._apply_weights_recf(dist_files, sample_idx) + else: + self._apply_weights_general(dist_files, modifiable_columns, n_decimals, dict_df_weights, sample_idx) + + return self.samples + + +#%% =========================================================================== +### --- OUTPUT FUNCTIONS --- +### =========================================================================== +def write_samples( + sample_group: pd.Series, + samples_dict: dict, + aux_files: dict, + run_ReEDS: bool = True, +): + """ + Write the samples to the appropriate locations + + Args: + sample_group (pd.Series): Row of the input file with the sampling instructions. + samples_dict (dict): Dictionary with the samples for each switch/file_name. + aux_files (dict): Dictionary with the auxiliary files needed for sampling. + run_ReEDS (bool): If True, the script is being used to run ReEDS. + If False, the script is being used to test the samples before running ReEDS. + """ + + inputs_case = sample_group['inputs_case'] + + for sample_idx, sample_ID in enumerate(samples_dict.keys()): + samples = samples_dict[sample_ID] + sw_name = sample_group['switch_names'][sample_idx] + save_path_structure = sample_group['save_paths'][sample_idx] # Where the samples will be copied to + file_name = sample_group["file_names"][sample_idx] + + for n, sample in enumerate(samples): + save_path = save_path_structure.replace('{sample_n}', str(n)) + folder_path = os.path.dirname(save_path) # Folder path without the file name + file_termination = os.path.splitext(save_path)[-1] # File termination (.csv, .h5, etc.) + + # For run_ReEDS==False, create folder if it does not exist + if (not run_ReEDS): + os.makedirs(folder_path, exist_ok=True) + + # If we have a region-indexed file + if file_name in aux_files['region_files']['filename'].values: + # Get destination directory instead of save_path + dir_dst = os.path.dirname(save_path) + # Get the row of the region-indexed file + region_files_row = aux_files['region_files'].query('filename == @file_name').iloc[0] + copy_files.write_region_indexed_file(sample, dir_dst, aux_files['source_deflator_map'], + aux_files['sw'], region_files_row, + aux_files['regions_and_agglevel'], + aux_files['agglevel_variables']) + + elif file_termination == '.csv': + # Not a region-indexed file but it is a CSV file + if file_name != 'switches.csv': + sample.to_csv(save_path, index=False) + else: + # Read the original switches.csv file + original_switches = pd.read_csv(save_path, header=None, index_col=0) + # Update the original switches.csv file with the new samples + original_switches.loc[sw_name] = sample + original_switches.to_csv(save_path, header=False) + if run_ReEDS: + # Create gswitches.csv and .txt files + gswitches_path = reeds.io.write_gswitches(original_switches, inputs_case) + copy_files.scalar_csv_to_txt(gswitches_path) + + + if run_ReEDS: # Only print if running ReEDS optimization + reduced_path = os.sep.join(save_path.strip(os.sep).split(os.sep)[-3:]) + print(f"...Sample related to switch {sw_name} was copied to {reduced_path}") + + +#%% =========================================================================== +### --- MAIN PROCEDURE --- +### =========================================================================== +def main( + reeds_path: str, + inputs_case: str, + n_samples: int = 1, + seed: int = 0, + run_ReEDS: bool = True, +): + """ + Create samples for the Monte Carlo Simulation (MCS). + + Args: + reeds_path (str): Path to the ReEDS directory. + inputs_case (str): Path to the inputs_case directory. + n_samples (int): Number of samples to generate. + seed (int): Seed for the random number generator. + run_ReEDS (bool): If True, the script is being used to run ReEDS. + If False, the script is being used to test the samples before running ReEDS. + + Notes: + If run_ReEDS=False inputs_case only needs to be a folder containing switches.csv so + we can perform any spatial filtering needed to get the samples + """ + + if run_ReEDS: + # Set random seed as the (MCS run number + seed) to allow reproducibility without having + # the same sample for each MCS-ReEDS call + runs_folder_name = os.path.basename(os.path.dirname(inputs_case.rstrip(os.path.sep))) + mcs_run_number = int((runs_folder_name.split('_')[-1]).replace('MC', '')) + seed += mcs_run_number + np.random.seed(seed) + else: + # The script is being used to test the samples before running ReEDS + np.random.seed(seed) + + # Obtain instructions to sample the distributions for each switch + df_input_dist_instructions, aux_files = get_dist_instructions(reeds_path, inputs_case, run_ReEDS=run_ReEDS) + + print('Sampling...') + for _, sample_group in df_input_dist_instructions.iterrows(): + dist_switches = sample_group['switch_names'] + unique_switches = set(dist_switches) + + print(f"Sampling for switch(es): {unique_switches}") + # Create Samples + mcs_sampler = MCS_Sampler(sample_group, aux_files, n_samples) + samples_dict = mcs_sampler.get_samples(aux_files) + + # Record the weights of each sample group + mcs_sampler.record_group_weights( + os.path.join(os.path.dirname(inputs_case), 'lstfiles') + ) + # Write Samples + write_samples(sample_group, samples_dict, aux_files, run_ReEDS=run_ReEDS) + + +if __name__ == '__main__' and not hasattr(sys, 'ps1'): + parser = argparse.ArgumentParser(description='Copy files needed for this run') + parser.add_argument('reeds_path', help='ReEDS directory') + parser.add_argument('inputs_case', help='Output directory') + + args = parser.parse_args() + reeds_path = os.path.abspath(args.reeds_path) + inputs_case = os.path.abspath(args.inputs_case) + + # ---- Settings for testing ---- + # reeds_path = reeds.io.reeds_path + # inputs_case = os.path.join(reeds_path,'runs','v20250825_revM2_MonteCarlo_MC1','inputs_case') + # n_samples = 1 + # seed = 0 + # run_ReEDS = True + + # Set up logger + tic = datetime.datetime.now() + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(os.path.dirname(inputs_case), 'gamslog.txt'), + ) + + # Read switches and check if MCS_runs is enabled. + sw = reeds.io.get_switches(inputs_case) + MCS_Runs = int(sw.get('MCS_runs', 0)) + if MCS_Runs >= 1: + print('Starting mcs_sampler.py') + main(reeds_path, inputs_case, n_samples=1) + else: + print('MCS_runs switch is set to 0 or not found. No Monte Carlo sampling will be performed') + + # Final log/timing update. + reeds.log.toc( + tic=tic, + year=0, + process='inputs/mcs_sampler.py', + path=os.path.join(os.path.dirname(inputs_case)) + ) diff --git a/reeds/input_processing/outage_rates.py b/reeds/input_processing/outage_rates.py new file mode 100644 index 00000000..ea5aefd7 --- /dev/null +++ b/reeds/input_processing/outage_rates.py @@ -0,0 +1,507 @@ +#%%### Imports +import pandas as pd +import numpy as np +import os +import sys +import datetime +import argparse +import h5py +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +reeds_path = reeds.io.reeds_path +## Time the operation of this script +tic = datetime.datetime.now() + + +#%%### Fixed inputs +tz_in = 'UTC' +tz_out = 'Etc/GMT+6' +temp_min = -50 +temp_max = 60 +## Only use during_quarters for techs without a monthly scheduled outage rate +during_quarters = ['spring', 'fall'] +## Cap the extrapolation of forced outage rates at high/low temperatures to 0.4 because +## PJM uses a 60% capacity credit (40% = 0.4 derate) for gas CT: +## https://www.pjm.com/-/media/DotCom/planning/res-adeq/elcc/2026-27-bra-elcc-class-ratings.pdf +max_extrapolated_outage_forced = 0.4 +## assume temperature-dependent outage rates for ng-fuel-cell to be the same as for combined_cycle plants +primemover2techgroup = { + 'combined_cycle': ['GAS_CC', 'FUEL_CELL'], + 'combustion_turbine': ['GAS_CT', 'H2_COMBUSTION'], + 'diesel': ['OGS'], + 'hydro_and_psh': ['HYDRO', 'PSH'], + 'nuclear': ['NUCLEAR'], + 'steam': ['COAL', 'BIO'], +} + +#%%### Functions +def extrapolate_forward_backward( + dfin, xmin, xmax, + numfitvals=2, polyfit_deg=1, ymin=0, ymax=1, + ): + """Extrapolate slopes forward and backward and fill gaps in integer steps. + Parameters + ---------- + dfin: pd.DataFrame + Dataframe with x values to extrapolate as the index + xmin: int + Minimum x value to extrapolate to + xmax: int + Maximum x value to extrapolate to + numfitvals: int, default 2 + Number of values from the beginning and end of the series to use to fit the + backward and forward slopes, respectively + polyfit_deg: int, default 1 + Degree of fitting polynomial to use in forward/backward extrapolations + ymin: float, default 0 + Lower limit for extrapolated values + ymax: float, default 1 + Upper limit for extrapolated values + + Returns + ------- + pd.DataFrame + Dataframe extrapolated forward and backward + """ + xs_low = dfin.index[:numfitvals].values + xs_high = dfin.index[-numfitvals:].values + + slope_low, intercept_low = np.polyfit( + x=xs_low, y=dfin.loc[xs_low].values, deg=polyfit_deg) + slope_high, intercept_high = np.polyfit( + x=xs_high, y=dfin.loc[xs_high].values, deg=polyfit_deg) + + ## Combine back-casted, input, and forward-casted data into a single dataframe + dfout = pd.concat([ + pd.DataFrame( + {xmin: intercept_low + slope_low * xmin}, + index=dfin.columns, + ).T, + dfin, + pd.DataFrame( + {xmax: intercept_high + slope_high * xmax}, + index=dfin.columns, + ).T, + ]).reindex(range(xmin, xmax+1)).interpolate('linear').clip(upper=ymax, lower=ymin) + + return dfout + + +def pm_to_tech(df, inputs_case): + """ + Broadcast prime mover timeseries data to techs. + + Parameters + ---------- + df: pd.DataFrame + index = timeseries + top column level = prime movers + inputs_case: str + path to ReEDS-2.0/runs/{case}/inputs_case + + Returns + ------- + pd.DataFrame + Same format as df but with prime movers broadcasted to techs + """ + tech_subset_table = reeds.techs.get_tech_subset_table(inputs_case) + df_prefill = pd.concat( + { + i: df[pm] + for pm in df.columns.get_level_values('prime_mover').unique() + for i in tech_subset_table.loc[primemover2techgroup[pm]].unique() + }, + axis=1, names=('i',) + ) + + return df_prefill + + +def fill_empty_techs(df_prefill, inputs_case, fillvalues_tech=None, during_quarters='all'): + """ + Parameters + ---------- + fillvalues_tech: pd.Series or dict with average values with which to fill missing techs. + keys = technologies. + during_quarters: 'all' or list of quarters. + If a list of quarters is provided, the annual fill values will be scaled and + applied only during the provided quarters. + + Returns + ------- + """ + ### Parse inputs + quarters = pd.read_csv( + os.path.join(inputs_case, 'sets', 'quarter.csv'), + header=None, + ).squeeze(1).map(lambda x: x[:4]).tolist() + if isinstance(during_quarters, str): + assert during_quarters == 'all' + elif isinstance(during_quarters, list): + during_quarters = [q[:4] for q in during_quarters] + assert all([i in quarters for i in during_quarters]) + else: + raise ValueError( + f"during_quarters={during_quarters} but must be 'all' or a list of quarters" + ) + + ### Case inputs + techlist = reeds.techs.get_techlist_after_bans(inputs_case) + hierarchy = reeds.io.get_hierarchy(reeds.io.standardize_case(inputs_case)) + + ### Identify techs with nonzero values that are not yet included in dataframe + keep_techs = [i for i in fillvalues_tech.index if i in techlist] + + included_techs = df_prefill.columns.get_level_values('i').unique() + missing_techs = [ + c for c in fillvalues_tech.index + if ((c.lower() not in included_techs) and (c.lower() in keep_techs)) + ] + print(f"included ({len(included_techs)}): {' '.join(sorted(included_techs))}") + print(f"missing ({len(missing_techs)}): {' '.join(sorted(missing_techs))}") + + ### Fill data for missing techs + if len(missing_techs): + if 'r' in df_prefill.columns.names: + dfout_filled = pd.concat( + { + (i,r): pd.Series(index=df_prefill.index, data=fillvalues_tech[i]) + for i in missing_techs for r in hierarchy.index + }, + axis=1, names=('i','r'), + ) + else: + dfout_filled = pd.concat( + { + i: pd.Series(index=df_prefill.index, data=fillvalues_tech[i]) + for i in missing_techs + }, + axis=1, names=('i'), + ) + + ## If during_quarters is provided, only apply outages during those quarters + if isinstance(during_quarters, list): + month2quarter = pd.read_csv( + os.path.join(inputs_case, 'month2quarter.csv'), + index_col='month', + ).squeeze(1).map(lambda x: x[:4]) + total_hours = len(dfout_filled) + outage_hours = ( + dfout_filled.index.month.map(month2quarter) + .isin(during_quarters).sum() + ) + dfout_filled *= (total_hours / outage_hours) + dfout_filled.loc[ + ~dfout_filled.index.month.map(month2quarter).isin(during_quarters) + ] = 0 + ## Make sure it worked + assert ( + dfout_filled.mean().round(3) + == fillvalues_tech.loc[dfout_filled.columns].round(3) + ).all() + + dfout = pd.concat([df_prefill, dfout_filled], axis=1) + else: + dfout = df_prefill + + return dfout + + +def calc_outage_forced( + reeds_path, + inputs_case, + max_extrapolated_outage_forced=max_extrapolated_outage_forced, +): + """ + """ + ### Derived inputs + sw = reeds.io.get_switches(inputs_case) + hierarchy = reeds.io.get_hierarchy(reeds.io.standardize_case(inputs_case)) + val_ba = ( + pd.read_csv(os.path.join(inputs_case, 'val_ba.csv'), header=None) + .squeeze(1).values + ) + ## Static forced outage rates (for filling empties) + outage_forced_static = pd.read_csv( + os.path.join(inputs_case, 'outage_forced_static.csv'), + header=None, index_col=0, + ).squeeze(1) + outage_forced_static.index = outage_forced_static.index.str.lower() + outage_forced_static = ( + outage_forced_static + .drop(reeds.techs.ignore_techs, errors='ignore') + .copy() + ) + + ### Load temperatures + print('Load temperatures and broadcast from states to zones') + temperatures = reeds.io.get_temperatures(inputs_case)[hierarchy.st] + temperatures.columns = hierarchy.index + + ### Input data + if sw.GSw_OutageScen.lower() == 'static': + ### Fill static data for all techs and modeled regions + df = pd.concat( + {r: outage_forced_static for r in val_ba}, + axis=0, + names=('r','i'), + ).reorder_levels(['i','r']).sort_index() + forcedoutage_prefill = pd.concat({i: df for i in temperatures.index}, axis=1).T + fits_forcedoutage = pd.DataFrame() + forcedoutage_pm = pd.DataFrame() + + else: + fits_forcedoutage_in = pd.read_csv( + os.path.join(inputs_case, 'outage_forced_temperature.csv'), + comment='#', + ).pivot(index='deg_celsius', columns='prime_mover', values='outage_frac') + + ### Extrapolate slopes and fill gaps in integer steps + fits_forcedoutage = extrapolate_forward_backward( + dfin=fits_forcedoutage_in, xmin=temp_min, xmax=temp_max, + ymax=max_extrapolated_outage_forced, + ) + + ### Get temperature-dependent outage rate by prime mover and state + forcedoutage_pm = pd.concat( + {pm: temperatures.replace(fits_forcedoutage[pm]) for pm in fits_forcedoutage}, + axis=1, names=('prime_mover',), + ).astype(np.float32) + + ### Map from prime movers to techs + forcedoutage_prefill = pm_to_tech(df=forcedoutage_pm, inputs_case=inputs_case) + + ### Fill missing hourly data with tech-specific static values + outage_forced_hourly = fill_empty_techs( + df_prefill=forcedoutage_prefill, + inputs_case=inputs_case, + fillvalues_tech=outage_forced_static, + ) + + return { + 'fits_forcedoutage': fits_forcedoutage, + 'forcedoutage_pm': forcedoutage_pm, + 'outage_forced_hourly': outage_forced_hourly, + } + + +def write_outages(df, h5path): + names = df.columns.names + namelengths = { + name: df.columns.get_level_values(name).map(len).max() + for name in names + } + column_level_type = f'S{max([len(i) for i in names])}' + with h5py.File(h5path, 'w') as f: + f.create_dataset('index', data=df.index, dtype='S29') + ## Save both the individual column levels and the single-level delimited version + for name in names: + f.create_dataset( + f'columns_{name}', + data=df.columns.get_level_values(name), + dtype=f'S{namelengths[name]}') + f.create_dataset( + 'columns', + data=( + df.columns.map(lambda x: '|'.join(x)).rename('|'.join(names)) + if isinstance(df.columns, pd.MultiIndex) + else df.columns + ), + dtype=f'S{sum(namelengths.values()) + 1}') + f.create_dataset( + 'data', data=df, dtype=np.float32, + compression='gzip', compression_opts=4, + ) + + f.create_dataset( + 'column_levels', + data=np.array(names, dtype=column_level_type), + dtype=column_level_type) + + +def plot_outage_forced( + case, + fits_forcedoutage, + forcedoutage_pm, + interactive=True, +): + ### Prep plots + import matplotlib.pyplot as plt + import matplotlib as mpl + reeds.plots.plotparams() + case = os.path.dirname(inputs_case.rstrip(os.sep)) + figpath = os.path.join(case, 'outputs', 'figures') + os.makedirs(figpath, exist_ok=True) + + ### Plot the fits + nicelabels = { + 'combined_cycle': 'Combined cycle', + 'combustion_turbine': 'Combustion turbine', + 'steam': 'Steam turbine', + 'nuclear': 'Nuclear', + 'hydro_and_psh': 'Hydro and PSH', + 'diesel': 'Diesel', + } + colors = dict(zip(nicelabels.values(), [f'C{i}' for i in range(10)])) + fits_in = pd.read_csv( + os.path.join(case, 'inputs_case', 'outage_forced_temperature.csv'), + comment='#', + ) + mintemp = fits_in.deg_celsius.min() + maxtemp = fits_in.deg_celsius.max() + + temperatures = reeds.io.get_temperatures(case) + + plt.close() + f,ax = plt.subplots() + df = fits_forcedoutage.rename(columns=nicelabels)*100 + for k, v in nicelabels.items(): + df.loc[mintemp:maxtemp, v].plot(ax=ax, color=colors[v], label=v, ls='-') + df.loc[:, v].plot(ax=ax, color=colors[v], ls='--', label='_nolabel') + ax.legend(frameon=False, title=None, fontsize='large') + ax.set_ylabel('Forced outage rate [%]') + ax.set_xlabel('Temperature [°C]') + ax.set_ylim(0) + ax.set_xlim(temperatures.min().min(), temperatures.max().max()) + ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(10)) + ax.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) + reeds.plots.despine(ax) + plt.savefig(os.path.join(figpath, 'FOR-fits.png')) + if interactive: + plt.show() + else: + plt.close() + + ### Plot forced outage rates + dfmap = reeds.io.get_dfmap(case) + dfzones = dfmap['r'] + aggfunc = 'mean' + + for pm in primemover2techgroup: + dfdata = forcedoutage_pm[pm].copy() * 100 + + plt.close() + f, ax = reeds.plots.map_years_months( + dfzones=dfzones, dfdata=dfdata, aggfunc=aggfunc, + title=f"Monthly {aggfunc}\nforced outage rate,\n{nicelabels.get(pm,pm)} [%]", + ) + plt.savefig(os.path.join(figpath, f'FOR_monthly-{aggfunc}-{pm}.png')) + if interactive: + plt.show() + else: + plt.close() + + +def calc_outage_scheduled(reeds_path, inputs_case, during_quarters=during_quarters): + sw = reeds.io.get_switches(inputs_case) + ## Static scheduled outage rates (for filling empties) + outage_scheduled_static = pd.read_csv( + os.path.join(inputs_case, 'outage_scheduled_static.csv'), + header=None, index_col=0, + ).squeeze(1) + outage_scheduled_static.index = outage_scheduled_static.index.str.lower() + outage_scheduled_static = ( + outage_scheduled_static + .drop(reeds.techs.ignore_techs, errors='ignore') + .copy() + ) + + ### Monthly scheduled outage rates + outage_scheduled_monthly = pd.read_csv( + os.path.join(inputs_case, 'outage_scheduled_monthly.csv'), + index_col=['prime_mover','month'], + ).squeeze(1).unstack('prime_mover') + + timeindex = pd.Series( + index=reeds.timeseries.get_timeindex(sw.resource_adequacy_years_list) + ) + outage_scheduled_pm = pd.DataFrame( + { + pm: timeindex.index.month.map(outage_scheduled_monthly[pm]).values + for pm in outage_scheduled_monthly + }, + index=timeindex.index, + dtype=np.float32, + ) + outage_scheduled_pm.columns = outage_scheduled_pm.columns.rename('prime_mover') + + outage_scheduled_tech = pm_to_tech(outage_scheduled_pm, inputs_case) + + outage_scheduled_hourly = fill_empty_techs( + df_prefill=outage_scheduled_tech, + inputs_case=inputs_case, + fillvalues_tech=outage_scheduled_static, + during_quarters=during_quarters, + ) + + return outage_scheduled_hourly + + +#%% +def main(reeds_path, inputs_case, debug=0, interactive=False): + ### Forced outages + print('Get forced outage rates') + dfforced = calc_outage_forced(reeds_path, inputs_case) + print('Write forced outage rates') + write_outages( + df=dfforced['outage_forced_hourly'], + h5path=os.path.join(inputs_case, 'outage_forced_hourly.h5'), + ) + ## Make sure it worked + reeds.io.get_outage_hourly(inputs_case, 'forced') + if debug: + plot_outage_forced( + case=os.path.abspath(os.path.join(inputs_case, '..')), + fits_forcedoutage=dfforced['fits_forcedoutage'], + forcedoutage_pm=dfforced['forcedoutage_pm'], + interactive=interactive, + ) + + ### Scheduled outages + print('Get scheduled outage rates') + outage_scheduled_hourly = calc_outage_scheduled(reeds_path, inputs_case) + print('Write scheduled outage rates') + write_outages( + df=outage_scheduled_hourly, + h5path=os.path.join(inputs_case, 'outage_scheduled_hourly.h5'), + ) + ## Make sure it worked + reeds.io.get_outage_hourly(inputs_case, 'scheduled') + + +#%%### Run it +if __name__ == '__main__': + #%% Parse args + parser = argparse.ArgumentParser( + description='Calculate temperature-dependent forced-outage rates', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('reeds_path', help='ReEDS-2.0 directory') + parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + # #%% Settings for testing + # reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + # inputs_case = os.path.join(reeds_path, 'runs', 'v20260113_temperatureM1_Everything', 'inputs_case') + # interactive = True + # debug = 1 + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + print('Starting outage_rates.py') + #%% Run it + main(reeds_path=reeds_path, inputs_case=inputs_case) + + #%% All done + reeds.log.toc( + tic=tic, year=0, process='inputs/outage_rates.py', + path=os.path.join(inputs_case,'..'), + ) + print('Finished outage_rates.py') diff --git a/reeds/input_processing/plantcostprep.py b/reeds/input_processing/plantcostprep.py new file mode 100644 index 00000000..fe009411 --- /dev/null +++ b/reeds/input_processing/plantcostprep.py @@ -0,0 +1,505 @@ +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import pandas as pd +import numpy as np +import os +import sys +import argparse +import datetime +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +# Time the operation of this script +tic = datetime.datetime.now() + +#%% Parse arguments +parser = argparse.ArgumentParser(description="""This file processes plant cost data by tech""") +parser.add_argument("reeds_path", help="ReEDS directory") +parser.add_argument("inputs_case", help="output directory") + +args = parser.parse_args() +reeds_path = args.reeds_path +inputs_case = args.inputs_case + +# #%% Settings for testing +#reeds_path = os.path.expanduser('~/github2/ReEDS-2.0/') +#inputs_case = os.path.join(reeds_path,'runs','v20220421_prmM0_ercot_seq','inputs_case') +#reeds_path = '/Users/apham/Documents/GitHub/ReEDS/ReEDS-2.0/' +#inputs_case = '/Users/apham/Documents/GitHub/ReEDS/ReEDS-2.0/runs/test_Pacific/inputs_case/' + +#%% Set up logger +log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), +) +print('Starting plantcostprep.py') + +#%% Inputs from switches +sw = reeds.io.get_switches(inputs_case) + + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +def deflate_func(data,case): + deflate = dollaryear.loc[dollaryear['Scenario'] == case,'Deflator'].values[0] + if 'capcost' in data.columns: + data['capcost'] *= deflate + if 'capcost_energy' in data.columns: + data['capcost_energy'] *= deflate + if 'fom' in data.columns: + data['fom'] *= deflate + data['vom'] *= deflate + if 'fom_energy' in data.columns: + data['fom_energy'] *= deflate + return data + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +dollaryear = pd.concat( + [pd.read_csv(os.path.join(inputs_case,"dollaryear_plant.csv")), + pd.read_csv(os.path.join(inputs_case,"dollaryear_consume.csv"))] +) +deflator = pd.read_csv( + os.path.join(inputs_case,"deflator.csv"), + header=0, names=['Dollar.Year','Deflator'], index_col='Dollar.Year').squeeze(1) + +dollaryear = dollaryear.merge(deflator,on="Dollar.Year",how="left") + +#%% Get ILR_ATB from scalars +scalars = reeds.io.get_scalars(inputs_case) + +#%%############### +# -- PV -- # +################## + +# Adjust cost data to 2004$ +upv = pd.read_csv(os.path.join(inputs_case,'plantchar_upv.csv')) +upv = deflate_func(upv, sw.plantchar_upv).set_index('t') + +# Prior to ATB 2020, PV costs are in $/kW_DC +# Starting with ATB 2020 cost inputs, PV costs are in $/kW_AC +# ReEDS uses DC capacity, so divide by inverter loading ratio +if '2019' not in sw.plantchar_upv: + upv[['capcost','fom','vom']] = upv[['capcost','fom','vom']] / scalars['ilr_utility'] + +# Broadcast costs to all UPV resource classes +upv_stack = pd.concat( + {'UPV_{}'.format(c): upv for c in range(1,11)}, + axis=0, names=['i','t'] +).reset_index() + +#%%######################## +# -- Other Techs -- # +########################### + +conv_in = [] +conv_techs = ['gas', 'gas_ccs', 'coal', 'coal_ccs', 'biopower', 'nuclear', 'nuclear_smr','fuelcell', 'other'] +for ct in conv_techs: + print(f"Loading plantchar_{ct}") + df = pd.read_csv(os.path.join(inputs_case,f'plantchar_{ct}.csv')) + df = deflate_func(df, sw[f'plantchar_{ct}']) + conv_in.append(df) +# combine into one set of "conventional" generators +conv = pd.concat(conv_in).reset_index(drop=True) + +ccsflex = pd.read_csv(os.path.join(inputs_case,'plantchar_ccsflex_cost.csv')) +ccsflex = deflate_func(ccsflex, sw.ccsflexscen) + +beccs = pd.read_csv(os.path.join(inputs_case,'plantchar_beccs.csv')) +beccs = deflate_func(beccs, sw.plantchar_beccs) + +h2combustion = pd.read_csv(os.path.join(inputs_case,'plantchar_h2combustion.csv')) +h2combustion = deflate_func(h2combustion, sw.plantchar_h2combustion) + +if sw.upgradescen != 'default': + upgrade = pd.read_csv(os.path.join(inputs_case,'plantchar_upgrades.csv')) + upgrade = deflate_func(upgrade, sw.upgradescen) + upgrade = upgrade.rename(columns={'capcost':'upgradecost'}) + upgrade = upgrade[['i','t','upgradecost']] + upgrade['upgradecost'] *= 1000 + upgrade['upgradecost'] = upgrade['upgradecost'].round(0).astype(int) + +#%%######################### +# -- Onshore Wind -- # +############################ + +onswinddata = pd.read_csv(os.path.join(inputs_case,'plantchar_onswind.csv')) +#We will have a 'Turbine' column. For each turbine, we assume 10 classes +onswinddata.columns= ['turbine','t','cf_mult','capcost','fom','vom'] +onswinddata['tech'] = 'ONSHORE' +class_bin_num = 10 +turb_ls = [] +for turb in onswinddata['turbine'].unique(): + turb_ls += [turb]*class_bin_num +df_class_turb = pd.DataFrame({'turbine':turb_ls, 'class':range(1, len(turb_ls) + 1)}) +onswinddata = onswinddata.merge(df_class_turb, on='turbine', how='inner') +onswinddata = onswinddata[['tech','class','t','cf_mult','capcost','fom','vom']] +onswinddata = deflate_func(onswinddata, sw.plantchar_onswind) + +#%%########################## +# -- Offshore Wind -- # +############################# + +ofswinddata = pd.read_csv(os.path.join(inputs_case,'plantchar_ofswind.csv')) +if 'Turbine' in ofswinddata: + #ATB 2024 style + #We will have a 'Turbine' column (fixed vs floating). For each turbine, we assume 5 classes + #(fixed = 1-5 and floating = 6-10) + ofswinddata.columns= ['turbine','t','cf_mult','capcost','fom','vom','rsc_mult'] + ofswinddata['tech'] = 'OFFSHORE' + class_bin_num = 5 + turb_ls = [] + for turb in ofswinddata['turbine'].unique(): + turb_ls += [turb]*class_bin_num + df_class_turb = pd.DataFrame({'turbine':turb_ls, 'class':range(1, len(turb_ls) + 1)}) + ofswinddata = ofswinddata.merge(df_class_turb, on='turbine', how='inner') + ofswind_rsc_mult = ofswinddata[['t','class','rsc_mult']].copy() + ofswind_rsc_mult['tech'] = 'wind-ofs_' + ofswind_rsc_mult['class'].astype(str) + ofswind_rsc_mult = ofswind_rsc_mult.pivot_table(index='t',columns='tech', values='rsc_mult') + ofswinddata = ofswinddata[['tech','class','t','cf_mult','capcost','fom','vom']] +else: + #ATB 2023 style + #We need to reduce to 5 classes for fixed and 5 for floating. We'll leave classes 1-5 alone (for fixed), remove classes 6,7,13, and 14, and then rename classes 8-12 to 6-10 (for floating) + ofswinddata = ofswinddata[~ofswinddata['Wind class'].isin([6,7,13,14])] + float_cond = ofswinddata['Wind class'] > 7 + ofswinddata.loc[float_cond, 'Wind class'] = ofswinddata.loc[float_cond, 'Wind class'] - 2 + ofswind_rsc_mult = ofswinddata[['Year','Wind class','rsc_mult']].copy() + ofswind_rsc_mult['tech'] = 'wind-ofs_' + ofswind_rsc_mult['Wind class'].astype(str) + ofswind_rsc_mult = ofswind_rsc_mult.rename(columns={'Year':'t'}) + ofswind_rsc_mult = ofswind_rsc_mult.pivot_table(index='t',columns='tech', values='rsc_mult') + ofswinddata = ofswinddata.drop(columns=['rsc_mult']) + ofswinddata.columns = ['tech','class','t','cf_mult','capcost','fom','vom'] +ofswinddata = deflate_func(ofswinddata, sw.plantchar_ofswind) +winddata = pd.concat([onswinddata.copy(),ofswinddata.copy()]) + +winddata.loc[winddata['tech'].str.contains('ONSHORE'),'tech'] = 'wind-ons' +winddata.loc[winddata['tech'].str.contains('OFFSHORE'),'tech'] = 'wind-ofs' +winddata['i'] = winddata['tech'] + '_' + winddata['class'].astype(str) +wind_stack = winddata[['t','i','capcost','fom','vom']].copy() + +#%%####################### +# -- Geothermal -- # +########################## + +geodata = pd.read_csv(os.path.join(inputs_case,'plantchar_geo.csv')) +geodata.columns = ['tech','class','depth','t','capcost','fom','vom'] +geodata['i'] = geodata['tech'] + '_' + geodata['depth'] + '_' + geodata['class'].astype(str) +geodata = deflate_func(geodata, sw.plantchar_geo) +geo_stack = geodata[['t','i','capcost','fom','vom']].copy() + + +#%%################ +# -- CSP -- # +################### + + +csp = pd.read_csv(os.path.join(inputs_case,'plantchar_csp.csv')) +csp = deflate_func(csp, sw.plantchar_csp) + +csp_stack = pd.DataFrame(columns=csp.columns) + +#create categories for all upv categories +for n in range(1,13): + tcsp = csp.copy() + tcsp['i'] = csp['type']+"_"+str(n) + csp_stack = pd.concat([csp_stack,tcsp],sort=False) + +csp_stack = csp_stack[['t','capcost','fom','vom','i']] + +#%%#################### +# -- Storage -- # +####################### + +battery = pd.read_csv(os.path.join(inputs_case,'plantchar_battery.csv')) +battery = deflate_func(battery, sw.plantchar_battery) + +evmc_storage = pd.read_csv(os.path.join(inputs_case,'plantchar_evmc_storage.csv')) +evmc_storage = deflate_func(evmc_storage, 'evmc_storage_' + sw.evmcscen) +evmc_shape = pd.read_csv(os.path.join(inputs_case,'plantchar_evmc_shape.csv'), dtype = {'fom':float,'vom':float,'rte':float}) +evmc_shape = deflate_func(evmc_shape, 'evmc_shape_' + sw.evmcscen) + +#%%############################ +# -- Concat all data -- # +############################### + +alldata = pd.concat([conv,upv_stack,wind_stack,geo_stack,csp_stack,battery, + evmc_storage,evmc_shape,beccs,ccsflex,h2combustion],sort=False) + +if sw.upgradescen != 'default': + alldata = pd.concat([alldata,upgrade]) + +alldata['t'] = alldata['t'].astype(int) + +#Convert from $/kw to $/MW +alldata['capcost'] = alldata['capcost']*1000 +alldata['capcost_energy'] = alldata.get('capcost_energy', 0) * 1000 +alldata['fom'] = alldata['fom']*1000 +alldata['fom_energy'] = alldata.get('fom_energy', 0) * 1000 + +alldata['capcost'] = alldata['capcost'].round(0).astype(int) +alldata['capcost_energy'] = (alldata['capcost_energy'].fillna(0)).round(0).astype(int) +alldata['fom'] = alldata['fom'].round(0).astype(int) +alldata['fom_energy'] = (alldata['fom_energy'].fillna(0)).round(0).astype(int) +alldata['vom'] = alldata['vom'].round(4) +alldata['heatrate'] = alldata['heatrate'].round(4) + +# Fill empty values with 0, melt to long format +outdata = ( + alldata.fillna(0) + .melt(id_vars=['i','t']) + ### Rename the columns so GAMS reads them as a comment + .rename(columns={'i':'*i'}) +) + +outdata = outdata.loc[outdata.variable.isin(['capcost', 'capcost_energy', + 'fom', 'fom_energy', + 'vom','heatrate','upgradecost','rte'])] + +#%%################################## +# -- Wind Capacity Factors -- # +##################################### + +windcfmult = winddata[['t','i','cf_mult']].set_index(['i','t'])['cf_mult'] +windcfmult = windcfmult.round(6) +outwindcfmult = windcfmult.reset_index().pivot_table(index='t',columns='i', values='cf_mult') + +#%%########################################################### +# -- Electrolyzer Stack Replacement Cost Adjustment -- # +############################################################## + +consume_char = pd.read_csv(os.path.join(inputs_case,'consume_char.csv')) + +# grab the electrolyzer cost 'h2_elec_stack_replace_year' years into the future +current_year = datetime.date.today().year +mask = (consume_char['*i'].isin(['electrolyzer'])) & (consume_char['parameter'].isin(['cost_cap']) & (consume_char['t'].isin([current_year+scalars['h2_elec_stack_replace_year']]))) +elec_cost_future = consume_char[mask]['value'].values[0] + +# read in financials_sys from inputs_case and take the average of all past years to get an average discount rate +financials_sys = pd.read_csv(os.path.join(inputs_case,'financials_sys.csv')) +discount_rate = np.average(financials_sys[(financials_sys['t'] <= current_year)]['d_real'].values) + +# the capital cost of electrolyzers needs to be increased by the cost to replace the stack ('h2_elec_stack_replace_perc') +# this replacement cost is represented as a percent of the capital cost of a new electrolyzer in that year. This cost occurs 'h2_elec_stack_replace_year' years in the future so we discount it. +mask = (consume_char['*i'].isin(['electrolyzer'])) & (consume_char['parameter'].isin(['cost_cap'])) +consume_char.loc[mask, 'value'] = consume_char[mask]['value'] + round( (elec_cost_future * scalars['h2_elec_stack_replace_perc'])/(discount_rate**scalars['h2_elec_stack_replace_year']) ,3) + +#%%############################### +# -- DR Shed -- # +################################## +# Capital cost multipliers for DR Shed vary by state and year +# Input cost data are state-level and are assigned to model region resolution here +# We assume all regions within the same state have uniform costs +dr_shed = pd.read_csv(os.path.join(inputs_case,'plantchar_dr_shed.csv'), index_col=0).round(6) +# FOM & VOM inputs are also state-level and need to be disaggregated +fom = pd.read_csv(os.path.join(inputs_case,'plantchar_dr_shed_fom.csv'), index_col=0).round(6) +vom = pd.read_csv(os.path.join(inputs_case,'plantchar_dr_shed_vom.csv'), index_col=0).round(6) +# If there are no DR shed data for regions being run, write dr_shed_capcostmult.csv +if dr_shed.empty: + dr_shed_capcost_mult = dr_shed.copy() + dr_shed_fom_regional = fom.copy() + dr_shed_vom_regional = vom.copy() +else: + state2r = pd.read_csv(os.path.join(inputs_case,'disagg_state_lpf.csv'),usecols=['state','r']) + # Map each unique state to all r values within that state + state2r = state2r.groupby('state')['r'].unique().apply(list).to_dict() + + def disaggregate_to_regions(data, state2r): + regional_data = {} + for st in data['r'].unique(): + bas_in_st = state2r[st] + data2r = {} + for r in bas_in_st: + copy_state = data.loc[data['r'] == st].copy() + copy_state['r'] = r + data2r[r] = copy_state + regional_data[st] = pd.concat(data2r.values()) + return pd.concat(regional_data.values()) + + dr_shed_capcost_mult = disaggregate_to_regions(dr_shed, state2r) + dr_shed_fom_regional = disaggregate_to_regions(fom, state2r) + dr_shed_vom_regional = disaggregate_to_regions(vom, state2r) + +#%%############################### +# -- Other Technologies -- # +################################## + +ccsflex_perf = pd.read_csv(os.path.join(inputs_case,'plantchar_ccsflex_perf.csv'),index_col=0).round(6) +hydro = pd.read_csv(os.path.join(inputs_case,'plantchar_hydro.csv'), index_col=0).round(6) +degrade = pd.read_csv( + os.path.join(inputs_case,'degradation_annual.csv'), + header=None) +degrade.columns = ['i','rate'] +degrade = reeds.techs.expand_GAMS_tech_groups(degrade) +degrade = degrade.set_index('i').round(6) + +#%%################################## +# -- PV+Battery Cost Model -- # +##################################### + +# Get PVB designs +pvb_ilr = pd.read_csv( + os.path.join(inputs_case, 'pvb_ilr.csv'), + header=0, names=['pvb_type','ilr'], index_col='pvb_type').squeeze(1) +pvb_bir = pd.read_csv( + os.path.join(inputs_case, 'pvb_bir.csv'), + header=0, names=['pvb_type','bir'], index_col='pvb_type').squeeze(1) +# Get PV and battery $/Wac costs for PVB +battery_USDperWac = ( + battery.loc[battery.i==f'battery_li'].set_index('t').capcost + + float(sw.GSw_PVB_Dur) + * battery.loc[battery.i==f'battery_li'].set_index('t').capcost_energy +) +UPV_defaultILR_USDperWac = upv.capcost * scalars['ilr_utility'] +# Get cost-sharing assumptions +pvbvalues = pd.read_csv(os.path.join(inputs_case,'plantchar_pvb.csv'), index_col='parameter') +fixed_ac_noninverter_cost_USDperWac = ( + pvbvalues.loc['fixed','value'] + * deflator[pvbvalues.loc['fixed','dollaryear']] + # Input units are in $/Wac, so convert to $/MWac to match units used in ReEDS + * 1000 +) + +def get_pvb_cost( + UPV_defaultILR_USDperWac, battery_USDperWac, + ILR_user=1.8, BIR_user=0.5, + inverter_cost_ac_fraction=0.05, + fixed_ac_noninverter_cost_USDperWac=0.0455, + ILR_ATB=1.3, +): + # Inverter cost is taken as a fixed fraction of UPV AC cost + inverter_USDperWac = UPV_defaultILR_USDperWac * inverter_cost_ac_fraction + # Standalone PV $/Wdc is [$/Wac] * [Wac/Wdc], where [$/Wac] is the full + # $/Wac minus the inverter cost and AC fixed costs + UPV_USDperWdc = ( + UPV_defaultILR_USDperWac + - inverter_USDperWac + - fixed_ac_noninverter_cost_USDperWac + ) / ILR_ATB + # Standalone PV $/Wac for the user-defined ILR: [$/Wac] + [$/Wdc] * ILR + UPV_USDperWac = ( + inverter_USDperWac + + fixed_ac_noninverter_cost_USDperWac + + UPV_USDperWdc * ILR_user + ) + # PVB system cost + PVB_USDperWac = ( + ## PV + UPV_USDperWac + ## Battery sized relative to PV + + BIR_user * ( + battery_USDperWac + ## Minus fixed AC costs + - fixed_ac_noninverter_cost_USDperWac + - inverter_USDperWac + ) + ) + # Standalone system cost for two systems with same DC capacity; + # here the battery needs its own inverter and AC fixed costs, + # so we don't subtract them out as we did above for PVB + standalone_USDperWac = ( + # PV + UPV_USDperWac + # Battery sized relative to PV + + BIR_user * battery_USDperWac + ) + ### Savings vs standalone + # savings_fraction = 1 - PVB_USDperWac / standalone_USDperWac + # savings_USDperWac = standalone_USDperWac - PVB_USDperWac + pvb_cost_fraction = PVB_USDperWac / standalone_USDperWac + ### Outputs + out = { + 'pvb': PVB_USDperWac, + 'standalone_pv_dc': UPV_USDperWdc, + 'standalone_pv_ac': UPV_USDperWac, + 'standalone_both': standalone_USDperWac, + 'inverter': inverter_USDperWac, + 'pvb_cost_fraction': pvb_cost_fraction, + } + return out + +#%% Calculate PVB cost fraction for each PVB design +pvb = {} +for i in sw['GSw_PVB_Types'].split('_'): + pvb['pvb{}'.format(i)] = get_pvb_cost( + UPV_defaultILR_USDperWac=UPV_defaultILR_USDperWac, + battery_USDperWac=battery_USDperWac, + ILR_user=pvb_ilr['pvb{}'.format(i)], + BIR_user=pvb_bir['pvb{}'.format(i)], + inverter_cost_ac_fraction=pvbvalues.loc['inverter_fraction','value'], + fixed_ac_noninverter_cost_USDperWac=fixed_ac_noninverter_cost_USDperWac, + ILR_ATB=scalars['ilr_utility'], + )['pvb_cost_fraction'] +pvb = pd.concat(pvb, axis=1) + + +## Create Electric DAC scenario output +# For electric DAC, we assume a sorbent system: https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a +# FYI, for DAC-gas we assume a solvent system: https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987 +dac_elec = pd.read_csv(os.path.join(inputs_case,'dac_elec.csv')) +dac_elec = deflate_func(dac_elec, f'dac_elec_{sw.dacscen}').round(4) +# Fill empty values with 0, melt to long format +outdac_elec = ( + dac_elec.fillna(0) + .melt(id_vars=['i','t'],value_vars=['capcost','fom','vom','conversionrate']) + ### Rename the columns so GAMS reads them as a comment + .rename(columns={'i':'*i'}) +) + + +## Create Gas DAC scenario output +# For DAC-gas we assume a solvent system: https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987 +dac_gas = pd.read_csv(os.path.join(inputs_case,'dac_gas.csv')) +dac_gas = deflate_func(dac_gas, f'dac_gas_{sw.GSw_DAC_Gas_Case}').round(4) + + +#%%################################################### +# -- Cost Adjustment for cost_upgrade Techs -- # +###################################################### +upgrade_mult_mid = pd.read_csv(os.path.join(inputs_case,"upgrade_mult_mid.csv")) +upgrade_mult_advanced = pd.read_csv(os.path.join(inputs_case,"upgrade_mult_advanced.csv")) +upgrade_mult_conservative = pd.read_csv(os.path.join(inputs_case,"upgrade_mult_conservative.csv")) + +if int(sw.GSw_UpgradeCost_Mult) == 0: + upgrade_mult = upgrade_mult_mid +elif int(sw.GSw_UpgradeCost_Mult) == 1: + upgrade_mult = upgrade_mult_advanced +elif int(sw.GSw_UpgradeCost_Mult) == 2: + upgrade_mult = upgrade_mult_conservative +elif int(sw.GSw_UpgradeCost_Mult) == 3: + upgrade_mult = 1 +elif int(sw.GSw_UpgradeCost_Mult) == 4: + upgrade_mult = 1 + (1-upgrade_mult_mid) +elif int(sw.GSw_UpgradeCost_Mult) == 5: + upgrade_mult = 1.2 + +#%%########################### +# -- Data Write-Out -- # +############################## + +print('writing plant data to:', os.getcwd()) +outdata.to_csv(os.path.join(inputs_case,'plantcharout.csv'), index=False) +upv.cf_improvement.round(3).to_csv(os.path.join(inputs_case,'pv_cf_improve.csv'), header=False) +outwindcfmult.to_csv(os.path.join(inputs_case,'windcfmult.csv')) +ccsflex_perf.to_csv(os.path.join(inputs_case,'ccsflex_perf.csv')) +consume_char.to_csv(os.path.join(inputs_case,'consume_char.csv'),index=False) +hydro.to_csv(os.path.join(inputs_case,'hydrocapcostmult.csv')) +dr_shed_capcost_mult.to_csv(os.path.join(inputs_case,'dr_shed_capcostmult.csv')) +dr_shed_fom_regional.to_csv(os.path.join(inputs_case,'plantchar_dr_shed_fom.csv')) +dr_shed_vom_regional.to_csv(os.path.join(inputs_case,'plantchar_dr_shed_vom.csv')) +ofswind_rsc_mult.to_csv(os.path.join(inputs_case,'ofswind_rsc_mult.csv')) +degrade.to_csv(os.path.join(inputs_case,'degradation_annual.csv'),header=False) +pvb.to_csv(os.path.join(inputs_case,'pvbcapcostmult.csv')) +upgrade_mult.round(4).to_csv(os.path.join(inputs_case,'upgrade_mult_final.csv'), index=False) +outdac_elec.to_csv(os.path.join(inputs_case,'consumechardac.csv'), index=False) +dac_gas.to_csv(os.path.join(inputs_case,'dac_gas.csv'), index=False) + +reeds.log.toc(tic=tic, year=0, process='inputs/plantcostprep.py', + path=os.path.join(inputs_case,'..')) + +print('Finished plantcostprep.py') diff --git a/reeds/input_processing/recf.py b/reeds/input_processing/recf.py new file mode 100644 index 00000000..3a160be5 --- /dev/null +++ b/reeds/input_processing/recf.py @@ -0,0 +1,509 @@ +''' +This script handles the modifications of static inputs for the first solve year. These inputs +include the 8760 renewable energy capacity factor (RECF) profiles. RECF and resource data for +various technologies are combined into single files for output: + +Resources: + - Creates a resource-to-(i,r,ccreg) lookup table for use in hourly_writesupplycurves.py + and Augur + - Add the distributed PV resources +RECF: + - Add the distributed PV recf profiles + - Sort the columns in recf to be in the same order as the rows in resources + - Scale distributed resource CF profiles by distribution loss factor and tiein loss factor +''' + +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import datetime +import numpy as np +import os +import pandas as pd +import sys +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds + + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +def csp_dispatch(cfcsp, sm=2.4, storage_duration=10): + """ + Use a simple no-foresight heuristic to dispatch CSP. + Excess energy from the solar field (i.e. energy above the max plant power output) + is sent to storage, and energy in storage is dispatched as soon as possible. + + --- Inputs --- + cfcsp: hourly energy output of solar field [fraction of max field output] + sm: solar multiple [solar field max output / plant max power output] + storage_duration: hours of storage as multiple of plant max power output + """ + ### Calculate derived dataframes + ## Field energy output as fraction of plant max output + dfcf = cfcsp * sm + ## Excess energy as fraction of plant max output + clipped = (dfcf - 1).clip(lower=0) + ## Remaining generator capacity after direct dispatch (can be used for storage dispatch) + headspace = (1 - dfcf).clip(lower=0) + ## Direct generation from solar field + direct_dispatch = dfcf.clip(upper=1) + + ### Numpy arrays + clipped_val = clipped.values + headspace_val = headspace.values + hours = range(len(clipped_val)) + storage_dispatch = np.zeros(clipped_val.shape) + ## Need one extra storage hour at the end, though it doesn't affect dispatch + storage_energy_hourstart = np.zeros((len(hours)+1, clipped_val.shape[1])) + + ### Loop over all hours and simulate dispatch + for h in hours: + ### storage dispatch is... + storage_dispatch[h] = np.where( + clipped_val[h], + ## zero if there's clipping in hour + 0, + ## otherwise... + np.where( + headspace_val[h] > storage_energy_hourstart[h], + ## storage energy at start of hour if more headspace than energy + storage_energy_hourstart[h], + ## headspace if more storage energy than headspace + headspace_val[h] + ) + ) + ### storage energy at start of next hour is... + storage_energy_hourstart[h+1] = np.where( + clipped_val[h], + ## storage energy in current hour plus clipping if clipping + storage_energy_hourstart[h] + clipped_val[h], + ## storage energy in current hour minus dispatch if not clipping + storage_energy_hourstart[h] - storage_dispatch[h] + ) + storage_energy_hourstart[h+1] = np.where( + storage_energy_hourstart[h+1] > storage_duration, + ## clip storage energy to storage duration if energy > duration + storage_duration, + ## otherwise no change + storage_energy_hourstart[h+1] + ) + + ### Format as dataframe and calculate total plant dispatch + storage_dispatch = pd.DataFrame( + index=clipped.index, columns=clipped.columns, data=storage_dispatch) + + total_dispatch = direct_dispatch + storage_dispatch + + return total_dispatch + + +def calculate_class_region_cf_hourly( + inputs_case, + tech, + weather_years, + tz_out='Etc/GMT+6' +): + if not tz_out.startswith('Etc/GMT'): + raise ValueError("tz_out must be formatted as 'Etc/GMT[+/-][number].") + + # Get supply curve information + df_sc = reeds.io.assemble_supplycurve( + os.path.join(inputs_case, f'supplycurve_{tech}.csv'), + case=os.path.dirname(inputs_case), + agg=True, + ) + # Calculate total capacity for each class-region pair + df_sc['class_region'] = ( + df_sc['class'].astype(str) + '|' + df_sc['region'] + ) + class_region_cap = ( + df_sc.groupby('class_region') + ['capacity'] + .sum() + ) + # Note we calculate on a per-year basis to avoid loading + # all of the site-level hourly data in memory at once + df_list = [] + for year in weather_years: + # Get site-level hourly CFs + weather_year_site_cf_hourly = reeds.io.get_site_cf_hourly( + tech=tech, + year=year, + case=inputs_case, + ) + # Downselect to relevant sites + weather_year_site_cf_hourly = weather_year_site_cf_hourly[df_sc.index] + # Calculate the capacity-weighted average CF for each class-region pair + weather_year_class_region_cf_hourly = ( + weather_year_site_cf_hourly.mul(df_sc['capacity']) + .rename(columns=df_sc['class_region']) + .groupby(axis=1, level=0) + .sum() + .div(class_region_cap) + ) + # For timezone conversion, we need a few hours of CF data for the next + # year. If we don't have data for the next year, assume the profile + # for the last day of this year is repeated for the first day of + # the next year and append to the end of the set of profiles. + next_year = year + 1 + if next_year not in weather_years: + next_year_first_day_data = ( + weather_year_class_region_cf_hourly.tail(24) + ) + next_year_first_day_data.index += pd.Timedelta(days=1) + weather_year_class_region_cf_hourly = ( + pd.concat([ + weather_year_class_region_cf_hourly, + next_year_first_day_data + ]) + ) + # Append to list of yearly data + df_list.append(weather_year_class_region_cf_hourly) + + # Concatenate all CF data + class_region_cf_hourly = pd.concat(df_list) + + # Shift timezone from UTC to tz_out + utc_offset = -1 * int(tz_out.split('Etc/GMT')[1]) + class_region_cf_hourly = ( + class_region_cf_hourly.shift(utc_offset) + .tz_localize(None) + .tz_localize(tz_out) + ) + class_region_cf_hourly = class_region_cf_hourly.loc[( + class_region_cf_hourly.index.year.isin(weather_years) + )] + class_region_cf_hourly.index.names = ['datetime'] + + return class_region_cf_hourly + + +def calculate_regional_distpv_cf(inputs_case, cap_min=0.0001): + # Get county-to-region mapping + county2zone = reeds.io.get_county2zone(os.path.dirname(inputs_case)) + county2zone.index = 'p' + county2zone.index + # Read county-level distpv capacity factors and + # downselect to relevant counties + county_distpv_cf = reeds.io.get_distpv_cf_hourly() + county_distpv_cf = county_distpv_cf[county2zone.index] + # Read county- and model region-level distpv capacities to use + # in capacity-weighted averages + sw = reeds.io.get_switches(inputs_case) + county_distpv_cap = reeds.io.get_distpv_capacities(distpvscen=sw.distpvscen) + regional_distpv_cap = reeds.io.get_distpv_capacities(inputs_case) + # Increment hourly cluster year if there is no data for the provided year + GSw_HourlyClusterYear = sw.GSw_HourlyClusterYear + if GSw_HourlyClusterYear not in county_distpv_cap: + GSw_HourlyClusterYear = str(int(GSw_HourlyClusterYear) + 1) + # Downselect to relevant counties and hourly cluster year values. + # Some counties (and regions defined by small groups of counties) have zero + # distpv capacity. We assign these an arbitrarily small capacity for the + # weighting to avoid division-by-zero errors. + county_distpv_cap = ( + county_distpv_cap.loc[county_distpv_cf.columns, GSw_HourlyClusterYear] + .clip(lower=cap_min) + ) + regional_distpv_cap = regional_distpv_cap[GSw_HourlyClusterYear].clip(lower=cap_min) + # Calculate capacity-weighted average capacity factors by calculating + # regional distpv generation and dividing by each region's distpv capacity + regional_distpv_cf = ( + county_distpv_cf.mul(county_distpv_cap) + .rename(columns=county2zone) + .groupby(axis=1, level=0) + .sum() + .div(regional_distpv_cap) + ) + + return regional_distpv_cf + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== +def main(reeds_path, inputs_case): + print('Starting recf.py') + + # #%% Settings for testing + # reeds_path = os.path.realpath(os.path.join(os.path.dirname(__file__),'..')) + # inputs_case = os.path.join( + # reeds_path,'runs','v20250129_cspfixM0_ISONE','inputs_case') + + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + resource_adequacy_years = sw['resource_adequacy_years_list'] + GSw_CSP_Types = [int(i) for i in sw.GSw_CSP_Types.split('_')] + GSw_PVB_Types = sw.GSw_PVB_Types + GSw_PVB = int(sw.GSw_PVB) + + + #%%### Load inputs + ### Load the input parameters + scalars = reeds.io.get_scalars(inputs_case) + ### distloss + distloss = scalars['distloss'] + + ### Load spatial hierarchy + hierarchy = pd.read_csv( + os.path.join(inputs_case,'hierarchy.csv') + ).rename(columns={'*r':'r'}).set_index('r') + hierarchy_original = ( + pd.read_csv(os.path.join(inputs_case, 'hierarchy_original.csv')) + .rename(columns={'ba':'r'}) + .set_index('r') + ) + ### Add ccreg column with the desired hierarchy level + if sw['capcredit_hierarchy_level'] == 'r': + hierarchy['ccreg'] = hierarchy.index.copy() + hierarchy_original['ccreg'] = hierarchy_original.index.copy() + else: + hierarchy['ccreg'] = hierarchy[sw.capcredit_hierarchy_level].copy() + hierarchy_original['ccreg'] = hierarchy_original[sw.capcredit_hierarchy_level].copy() + ### Map regions to new ccreg's + r2ccreg = hierarchy['ccreg'] + + # Get technology subsets + tech_table = pd.read_csv( + os.path.join(inputs_case,'tech-subset-table.csv'), index_col=0).fillna(False).astype(bool) + techs = {tech:list() for tech in list(tech_table)} + for tech in techs.keys(): + techs[tech] = tech_table[tech_table[tech]].index.values.tolist() + techs[tech] = [x.lower() for x in techs[tech]] + temp_save = [] + temp_remove = [] + # Interpreting GAMS syntax in tech-subset-table.csv + for subset in techs[tech]: + if '*' in subset: + temp_remove.append(subset) + temp = subset.split('*') + temp2 = temp[0].split('_') + temp_low = pd.to_numeric(temp[0].split('_')[-1]) + temp_high = pd.to_numeric(temp[1].split('_')[-1]) + temp_tech = '' + for n in range(0,len(temp2)-1): + temp_tech += temp2[n] + if not n == len(temp2)-2: + temp_tech += '_' + for c in range(temp_low,temp_high+1): + temp_save.append('{}_{}'.format(temp_tech,str(c))) + for subset in temp_remove: + techs[tech].remove(subset) + techs[tech].extend(temp_save) + vre_dist = techs['VRE_DISTRIBUTED'] + + # ------- Read in the static inputs for this run ------- + + ### Onshore Wind + df_windons = calculate_class_region_cf_hourly( + inputs_case, + 'wind-ons', + resource_adequacy_years + ) + df_windons.columns = ['wind-ons_' + col for col in df_windons] + ### Don't do aggregation in this case, so make a 1:1 lookup table + lookup = pd.DataFrame({'ragg':df_windons.columns.values}) + lookup['r'] = lookup.ragg.map(lambda x: x.rsplit('|',1)[1]) + lookup['i'] = lookup.ragg.map(lambda x: x.rsplit('|',1)[0]) + + ### Offshore Wind + if int(sw['GSw_OfsWind']) != 0: + df_windofs = calculate_class_region_cf_hourly( + inputs_case, + 'wind-ofs', + resource_adequacy_years + ) + df_windofs.columns = ['wind-ofs_' + col for col in df_windofs] + + ### UPV + df_upv = calculate_class_region_cf_hourly( + inputs_case, + 'upv', + resource_adequacy_years + ) + df_upv.columns = ['upv_' + col for col in df_upv] + + # If DistPV is turned off, create an empty dataframe with the same index as df_upv to concat + if int(sw['GSw_distpv']) == 0: + df_distpv = pd.DataFrame(index=df_upv.index) + else: + df_distpv = calculate_regional_distpv_cf(inputs_case) + df_distpv.columns = [f"distpv|{col}" for col in df_distpv.columns] + + ### CSP + # If CSP is turned off, create an empty dataframe with the same index as df_upv to concat + if int(sw['GSw_CSP']) == 0: + cspcf = pd.DataFrame(index=df_upv.index) + else: + cspcf = reeds.io.read_file( + os.path.join(inputs_case, 'recf_csp.h5'), + parse_timestamps=True, + ) + + ### Format PV+battery profiles + # Get the PVB types + pvb_ilr = pd.read_csv( + os.path.join(inputs_case, 'pvb_ilr.csv'), + header=0, names=['pvb_type','ilr'], index_col='pvb_type').squeeze(1) + df_pvb = {} + # Override GSw_PVB_Types if GSw_PVB is turned off + GSw_PVB_Types = ( + [int(i) for i in GSw_PVB_Types.split('_')] if int(GSw_PVB) + else [] + ) + for pvb_type in GSw_PVB_Types: + ilr = int(pvb_ilr['pvb{}'.format(pvb_type)] * 100) + # If PVB uses same ILR as UPV then use its profile + infile = 'recf_upv' if ilr == scalars['ilr_utility'] * 100 else f'recf_upv_{ilr}AC' + df_pvb[pvb_type] = reeds.io.read_file( + os.path.join(inputs_case,infile+'.h5'), + parse_timestamps=True, + ) + df_pvb[pvb_type].columns = [f'pvb{pvb_type}_{c}' + for c in df_pvb[pvb_type].columns] + df_pvb[pvb_type].index = df_upv.index.copy() + + ### Concat RECF data + recf = pd.concat( + [df_windons, df_windofs, df_upv, df_distpv] + + [df_pvb[pvb_type] for pvb_type in df_pvb], + sort=False, axis=1, copy=False) + + ### Downselect RECF data to resource adequacy and weather years + recf = recf.loc[recf.index.year.isin(resource_adequacy_years)] + + ### Add the other recf techs to the resources lookup table + toadd = pd.DataFrame({'ragg': [c for c in recf.columns if c not in lookup.ragg.values]}) + toadd['r'] = [c.rsplit('|', 1)[1] for c in toadd.ragg.values] + toadd['i'] = [c.rsplit('|', 1)[0] for c in toadd.ragg.values] + resources = ( + pd.concat([lookup, toadd], axis=0, ignore_index=True) + .rename(columns={'ragg':'resource','r':'area','i':'tech'}) + .sort_values('resource').reset_index(drop=True) + ) + + #%%%############################################# + # -- Performing Resource Modifications -- # + ################################################# + if int(sw['GSw_OfsWind']) == 0: + wind_ofs_resource = ['wind-ofs_' + str(n) for n in range(1,16)] + resources = resources[~resources['tech'].isin(wind_ofs_resource)] + + # Sorting profiles of resources to match the order of the rows in resources + resources = resources.sort_values(['resource','area']) + recf = recf.reindex(labels=resources['resource'].drop_duplicates(), axis=1, copy=False) + + ### Scale up distpv by 1/(1-distloss) + recf.loc[ + :, resources.loc[resources.tech.isin(vre_dist),'resource'].values + ] /= (1 - distloss) + + # Set the column names for resources to match ReEDS-2.0 + resources['ccreg'] = resources.area.map(r2ccreg) + resources.rename(columns={'area':'r','tech':'i'}, inplace=True) + resources = resources[['r','i','ccreg','resource']] + + + #%%### Concentrated solar thermal power (CSP) + ### Create CSP resource label for each CSP type (labeled by "tech" as csp1, csp2, etc) + csptechs = [f'csp{c}' for c in GSw_CSP_Types] + csp_resources = pd.concat({ + tech: + pd.DataFrame({ + 'resource': cspcf.columns, + 'r': cspcf.columns.map(lambda x: x.split('|')[1]), + 'class': cspcf.columns.map(lambda x: x.split('|')[0]), + }) + for tech in csptechs + }, axis=0, names=('tech',)).reset_index(level='tech') + + csp_resources = ( + csp_resources + .assign(i=csp_resources['tech'] + '_' + csp_resources['class'].astype(str)) + .assign(resource=csp_resources['tech'] + '_' + csp_resources['resource']) + .assign(ccreg=csp_resources.r.map(r2ccreg)) + [['i','r','resource','ccreg']] + ) + ###### Simulate CSP dispatch for each design + ### Get solar multiples + sms = {tech: scalars[f'csp_sm_{tech.strip("csp")}'] for tech in csptechs} + ### Get storage durations + storage_duration = pd.read_csv( + os.path.join(inputs_case,'storage_duration.csv'), header=None, index_col=0).squeeze(1) + ## All CSP resource classes have the same duration for a given tech, so just take the first one + durations = {tech: storage_duration[f'csp{tech.strip("csp")}_1'] for tech in csptechs} + ### Run the dispatch simulation for modeled regions + + csp_system_cf = pd.concat({ + tech: csp_dispatch(cspcf, sm=sms[tech], storage_duration=durations[tech]) + for tech in csptechs + }, axis=1) + ## Collapse multiindex column labels to single strings + csp_system_cf.columns = ['_'.join(c) for c in csp_system_cf.columns] + + ### Add CSP to RE output dataframes + csp_system_cf = csp_system_cf.loc[recf.index] + recf = pd.concat([recf, csp_system_cf], axis=1) + resources = pd.concat([resources, csp_resources], axis=0) + + #%% Check for errors + nulls = recf.isnull().sum() + missing = nulls.loc[nulls > 0] + if len(missing): + print(missing) + err = f"Missing RECF values for {len(missing)} columns" + raise ValueError(err) + + + #%%########################### + # -- Data Write-Out -- # + ############################## + + reeds.io.write_profile_to_h5(recf.astype(np.float16), 'recf.h5', inputs_case) + resources.to_csv(os.path.join(inputs_case,'resources.csv'), index=False) + ### Write the CSP solar field CF (no SM or storage) for hourly_writetimeseries.py + cspcf = cspcf.rename(columns=dict(zip(cspcf.columns, [f'csp_{i}' for i in cspcf.columns]))) + reeds.io.write_profile_to_h5(cspcf.astype(np.float32), 'csp.h5', inputs_case) + ### Overwrite the original hierarchy.csv based on capcredit_hierarchy_level + hierarchy.rename_axis('*r').to_csv( + os.path.join(inputs_case, 'hierarchy.csv'), index=True, header=True) + pd.Series(hierarchy.ccreg.unique()).to_csv( + os.path.join(inputs_case,'ccreg.csv'), index=False, header=False) + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if __name__ == '__main__': + # Time the operation of this script + tic = datetime.datetime.now() + + ### Parse arguments + parser = argparse.ArgumentParser( + description='Create run-specific hourly profiles', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('reeds_path', help='ReEDS-2.0 directory') + parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + #%% Run it + main(reeds_path=reeds_path, inputs_case=inputs_case) + + reeds.log.toc(tic=tic, year=0, process='inputs/recf.py', + path=os.path.join(inputs_case,'..')) + + print('Finished recf.py') diff --git a/reeds/input_processing/runfiles.csv b/reeds/input_processing/runfiles.csv new file mode 100644 index 00000000..890b202b --- /dev/null +++ b/reeds/input_processing/runfiles.csv @@ -0,0 +1,493 @@ +filename,filepath,required_if,aggfunc,disaggfunc,region_col,fix_cols,i_col,wide,header,key,post_copy,GAMStype,GAMSname,comment,notes +#,for input files only,"conditions determinining whether or not the file is required (1 if always required, 0 if always optional)",,,,,,1 if any parameters are in wide format,0 if file has column labels,,files are created after copy_files,for auto-imported files,,, +i.csv,inputs/sets/i.csv,1,ignore,ignore,,,,,,,1,set,i,generation technologies, +ctt.csv,inputs/sets/ctt.csv,1,ignore,ignore,,,,,,,1,set,ctt,cooling technology type, +wst.csv,inputs/sets/wst.csv,1,ignore,ignore,,,,,,,1,set,wst,water source type, +w.csv,inputs/sets/w.csv,1,ignore,ignore,,,,,,,1,set,w,form of water use (withdrawal or consumption), +geotech.csv,inputs/sets/geotech.csv,1,ignore,ignore,,,,,,,1,set,geotech,broader geothermal categories, +i_subtech.csv,inputs/sets/i_subtech.csv,1,ignore,ignore,,,,,,,1,set,i_subtech,technology subset categories, +i_h2_ptc_gen.csv,inputs/sets/i_h2_ptc_gen.csv,1,ignore,ignore,,,,,,,1,set,i_h2_ptc_gen,technology subset category for clean generators which qualify for the hydrogen production tax credit, +sdbin.csv,inputs/sets/sdbin.csv,1,ignore,ignore,,,,,,,1,set,sdbin,storage duration bins, +tg.csv,inputs/sets/tg.csv,1,ignore,ignore,,,,,,,1,set,tg,tech groups for growth constraints, +pcat.csv,inputs/sets/pcat.csv,1,ignore,ignore,,,,,,,1,set,pcat,prescribed capacity categories, +ccseason.csv,,1,ignore,ignore,,,,,,,1,set,ccseason,seasons used for capacity credit; cold is Oct 15-April 14 and hot is April 15-Oct 14, +quarter.csv,inputs/sets/quarter.csv,1,ignore,ignore,,,,,,,1,set,quarter,original h17 seasons (four per year), +month.csv,inputs/sets/month.csv,1,ignore,ignore,,,,,,,1,set,month,calendar months in a year, +RPSCat.csv,inputs/sets/RPSCat.csv,1,ignore,ignore,,,,,,,,,RPSCat,, +aclike.csv,inputs/sets/aclike.csv,1,ignore,ignore,,,,,0,,,,aclike,, +acp_disallowed.csv,inputs/state_policies/acp_disallowed.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,*st,"RPSCat,val",,,0,,,,,, +acp_prices.csv,inputs/state_policies/acp_prices.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,st,,1,0,,,,,, +tscbin.csv,,1,ignore,ignore,,,,,,,1,set,tscbin,transmission upgrade supply curve bins, +numpartitions.csv,,1,ignore,ignore,,,,,0,,,,,, +agglevels.csv,,1,ignore,ignore,ignore,,,,0,,,,,, +aggreg.csv,,1,ignore,ignore,ignore,,,0,0,,,,,, +aggreg2anchorreg.csv,,1,ignore,ignore,ignore,,,0,0,,,,,, +anchorreg2aggreg.csv,,1,ignore,ignore,ignore,,,0,0,,,,,, +allt.csv,inputs/sets/allt.csv,1,ignore,ignore,,,,,,,,,allt,, +alpha.csv,inputs/fuelprices/alpha_{ngscen}.csv,1,ignore,ignore,wide_cendiv,t,,1,0,,,,,, +bio_supplycurve.csv,inputs/supply_curve/bio_supplycurve.csv,1,ignore,ignore,usda_region,,,,0,,,,,, +bioclass.csv,inputs/sets/bioclass.csv,1,ignore,ignore,,,,,,,,,bioclass,, +can_exports.csv,inputs/canada_imports/can_exports.csv,int(sw.GSw_Canada) != 0,sum,ignore,r,wide,,1,0,,,,,, +can_exports_h_frac.csv,,1,ignore,ignore,,,,,0,,,,,, +can_exports_szn_frac.csv,inputs/canada_imports/can_exports_szn_frac.csv,int(sw.GSw_Canada) != 0,ignore,ignore,,,,,0,,,,,, +can_imports.csv,inputs/canada_imports/can_imports.csv,int(sw.GSw_Canada) != 0,sum,ignore,r,wide,,1,0,,,,,, +can_imports_capacity.csv,,1,sum,ignore,*r,t,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +can_imports_quarter_frac.csv,inputs/canada_imports/can_imports_quarter_frac.csv,int(sw.GSw_Canada) != 0,ignore,ignore,,,,,0,,,,,, +can_imports_szn_frac.csv,,1,ignore,ignore,,,,,0,,1,,,, +cangrowth.csv,inputs/load/cangrowth.csv,int(sw.GSw_Canada) != 0,ignore,ignore,st,,,1,0,,,,,, +canmexload.csv,,1,sum,ignore,*r,h,,0,0,,1,,,, +cap_cspns.csv,,1,sum,ignore,*r,t,,0,0,,1,,,,disaggfunc set to ignore because data is pulled from the county-indexed generator database +cap_existing_hydro.csv,inputs/hydro/cap_existing_hydro.csv,1,ignore,ignore,,t,,0,0,,,,,, +cap_existing_psh.csv,inputs/storage/cap_existing_psh.csv,(int(sw.GSw_Storage) != 0) and ((int(sw.GSw_HydroPSHDurData) == 1) or (sw.GSw_HydroStorInMaxFrac == 'data')),sum,ignore,r,"*i,v",,0,0,,,,,, +cap_hyd_ccseason_adj.csv,,1,mean,uniform,r,"*i,ccseason",,0,0,,1,,,, +cap_hyd_szn_adj.csv,,1,mean,uniform,r,"*i,szn",,0,0,,1,,,, +cap_limit.csv,,1,ignore,ignore,,,,1,0,,,,,, +cap_penalty.csv,inputs/financials/cap_penalty.csv,1,ignore,ignore,,tg,,,0,,,,,, +capnonrsc.csv,,1,sum,ignore,r,i,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +capnonrsc_energy.csv,,1,sum,ignore,r,i,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +cappayments_ba.csv,inputs/capacity_exogenous/cappayments_ba.csv,1,ignore,ignore,,*r,,0,0,,,,,,not done but not used +caprsc.csv,,1,sum,ignore,r,i,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +ccreg.csv,,1,ignore,ignore,,,,,0,,,,,, +ccs_link.csv,inputs/emission_constraints/ccs_link.csv,1,ignore,ignore,,,,,0,,,,,, +ccs_link_water.csv,inputs/emission_constraints/ccs_link_water.csv,1,ignore,ignore,,,,,0,,,,,, +ccseason_dates.csv,,1,ignore,ignore,,,,,0,,,,,, +ccsflex_cat.csv,inputs/sets/ccsflex_cat.csv,1,ignore,ignore,,,,,,,,,ccsflex_cat,, +ccsflex_perf.csv,,1,ignore,ignore,,,,,0,,,,,, +cd_beta0.csv,inputs/fuelprices/cd_beta0.csv,1,ignore,ignore,*cendiv,,,,0,,,,,, +cd_beta0_allsector.csv,inputs/fuelprices/cd_beta0_allsector.csv,1,ignore,ignore,*cendiv,,,,0,,,,,, +cendivweights.csv,inputs/fuelprices/cendivweights.csv,1,mean,ignore,r_cendiv,wide,,1,0,,,,,,Includes two region definitions; disaggfunc set to ignore because data is already at county resolution in inputs folder +ces_fraction.csv,inputs/state_policies/ces_fraction.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,st,,1,0,,,,,, +cf_hyd.csv,,1,mean,uniform,r,"*i,szn,t",,0,0,,1,,,, +cf_vre.csv,,1,mean_cap,ignore,r,"*i,h",*i,0,0,,1,,,,disaggfunc set to ignore because data will be written in correct spatial resolution by hourly_writetimeseries in hourly_repperiods.py +climate_UnappWaterMult.csv,,1,ignore,ignore,,,,,0,,1,,,,created by hourly_writetimeseries.py from temp_UnappWaterMult.csv data +climate_UnappWaterMultAnn.csv,,1,ignore,ignore,,,,,0,,1,,,,created by climateprep.py +climate_UnappWaterSeaAnnDistr.csv,,1,ignore,ignore,,,,,0,,1,,,,created by hourly_writetimeseries.py from temp_UnappWaterSeaAnnDistr.csv data +climate_heuristics_finalyear.csv,,1,ignore,ignore,,,,,0,,,,,, +climate_heuristics_yearfrac.csv,,1,ignore,ignore,,,,,0,,,,,, +climate_hydadjann.csv,,1,ignore,ignore,,,,,0,,1,,,,created by climateprep.py +climate_hydadjsea.csv,,1,ignore,ignore,,,,,0,,1,,,,created by hourly_writetimeseries.py from temp_hydadjsea.csv data +climate_loaddelta_timeslice.csv,,1,ignore,ignore,,"r,h",,1,0,,,,,,not done but rarely used; ignore for now +climate_param.csv,inputs/sets/climate_param.csv,1,ignore,ignore,,,,,,,,,climate_param,, +co2_cap.csv,,int(sw.GSw_AnnualCap) != 0,ignore,ignore,,,,0,0,,,,,, +co2_capture_incentive.csv,,1,ignore,ignore,,,,,0,,,,,, +co2_site_char.csv,inputs/ctus/co2_site_char.csv,1,ignore,ignore,,,,0,,,,,,, +co2_tax.csv,,int(sw.GSw_CarbTax) != 0,ignore,ignore,,,,0,,,,,,, +coal_price.csv,inputs/fuelprices/coal_{coalscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, +construction_schedules.csv,inputs/financials/construction_schedules_{construction_schedules_suffix}.csv,1,ignore,ignore,,,,1,0,,,,,, +construction_times.csv,inputs/financials/construction_times_{construction_times_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +consume_char.csv,inputs/consume/consume_char_{GSw_H2_Inputs}.csv,int(sw.GSw_H2) != 0,ignore,ignore,,"*i,t,parameter",,0,0,,,,,, +consumecat.csv,inputs/sets/consumecat.csv,1,ignore,ignore,,,,,,,,,consumecat,, +consumechardac.csv,,1,ignore,ignore,,"*i,t,variable",,0,0,,,,,, +cost_cap_mult.csv,inputs/waterclimate/cost_cap_mult.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, +cost_hurdle_country.csv,inputs/transmission/cost_hurdle_country.csv,1,ignore,ignore,*country,,,,0,,,,,, +cost_hurdle_intra.csv,inputs/transmission/cost_hurdle_intra.csv,1,ignore,ignore,,t,,,0,,,,,, +cost_hurdle_rate1.csv,,1,ignore,ignore,,t,,0,0,,1,,,, +cost_hurdle_rate2.csv,,1,ignore,ignore,,t,,0,0,,1,,,, +cost_opres.csv,,1,ignore,ignore,,,,,0,,,,,, +cost_opres_default.csv,inputs/plant_characteristics/cost_opres_default.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, +cost_opres_market.csv,inputs/plant_characteristics/cost_opres_market.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, +cost_vom.csv,,1,mean,ignore,r,"i,v,t",,0,0,,1,,,,ReEDS-to-PLEXOS output +cost_vom_mult.csv,inputs/waterclimate/cost_vom_mult.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, +county2zone.csv,,1,ignore,ignore,,,,,,,1,,,, +county2zone_original.csv,,1,ignore,ignore,,,,,,,1,,,, +crf.csv,,1,ignore,ignore,,,,0,,,,,,, +crf_co2_incentive.csv,,1,ignore,ignore,,,,,0,,,,,, +crf_h2_incentive.csv,,1,ignore,ignore,,,,,0,,,,,, +csapr_cat.csv,inputs/sets/csapr_cat.csv,1,ignore,ignore,,,,,,,,,csapr_cat,, +csapr_group.csv,inputs/sets/csapr_group.csv,1,ignore,ignore,,,,,,,,,csapr_group,, +csapr_group1_ex.csv,inputs/emission_constraints/csapr_group1_ex.csv,int(sw.GSw_CSAPR) != 0,ignore,ignore,*st,,,,0,,,,,, +csapr_group2_ex.csv,inputs/emission_constraints/csapr_group2_ex.csv,int(sw.GSw_CSAPR) != 0,ignore,ignore,*st,,,,0,,,,,, +csapr_ozone_season.csv,inputs/emission_constraints/csapr_ozone_season.csv,int(sw.GSw_CSAPR) != 0,ignore,ignore,st,,,,0,,,,,, +ctus_r_cs_spurlines_200mi.csv,,1,ignore,ignore,,,,,,,1,,,, +currency_incentives.csv,inputs/financials/currency_incentives.csv,1,ignore,ignore,,,,,0,,,,,, +dac_elec.csv,inputs/consume/dac_elec_{dacscen}.csv,int(sw.GSw_DAC) != 0,ignore,ignore,,,,1,0,,,,,, +dac_gas.csv,inputs/consume/dac_gas_{GSw_DAC_Gas_Case}.csv,int(sw.GSw_DAC) != 0,ignore,ignore,,,,1,0,,1,,,, +deflator.csv,inputs/financials/deflator.csv,1,ignore,ignore,,,,,0,,,,,, +degradation_annual.csv,inputs/degradation/degradation_annual_{degrade_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +demonstration_plants.csv,inputs/capacity_exogenous/demonstration_plants.csv,int(sw.GSw_NuclearDemo) != 0,sum,ignore,r,"t,i,coolingwatertech,ctt,wst,notes",i,0,0,,,,,, +depreciation_schedules.csv,inputs/financials/depreciation_schedules_{depreciation_schedules_suffix}.csv,1,ignore,ignore,,,,1,0,,,,,, +diagnose.gms,postprocessing/diagnose/diagnose.gms,1,ignore,ignore,,,,,,,,,,, +disagg_geosize.csv,,1,ignore,ignore,,,,,0,,,,,, +disagg_hydroexist.csv,inputs/disaggregation/disagg_hydroexist.csv,1,ignore,ignore,,,,,0,,,,,, +disagg_population.csv,inputs/disaggregation/county_population.csv,1,ignore,ignore,FIPS,,,,0,,1,,,, +disagg_state_lpf.csv,inputs/disaggregation/county_state_lpf.csv,1,ignore,ignore,FIPS,,,,0,,1,,,, +distance_reinforcement.csv,,1,ignore,ignore,r,"*i,rscbin",*i,0,0,,1,,,, +distance_spur.csv,,1,ignore,ignore,r,"*i,rscbin",*i,0,0,,1,,,, +distpvcap.csv,inputs/dgen_model_inputs/{distpvscen}/distpvcap_{distpvscen}.csv,1,sum,ignore,r,wide,,1,0,,,,,, +dollaryear_consume.csv,inputs/consume/dollaryear.csv,int(sw.GSw_DAC) != 0,ignore,ignore,,,,,0,,,,,,Do we really need 3 separate instances of dollaryear? +dollaryear_plant.csv,inputs/plant_characteristics/dollaryear.csv,1,ignore,ignore,,,,,0,,,,,,Do we really need 3 separate instances of dollaryear? +dollaryear_fuel.csv,inputs/fuelprices/dollaryear.csv,1,ignore,ignore,,,,,0,,,,,, +dollaryear_sc.csv,inputs/supply_curve/dollaryear.csv,1,ignore,ignore,,,,,0,,,,,, +dr_shed_avail_scalar.csv,inputs/demand_response/dr_shed_avail_scalar.csv,1,ignore,ignore,,,,,0,,,,,, +dr_shed_cap.csv,inputs/supply_curve/dr_shed_cap_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,wide,tech,,1,0,,,,,,done +dr_shed_cost.csv,inputs/supply_curve/dr_shed_cost_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,wide,tech,,1,0,,,,,,done +dr_shed_capacity_scalar.csv,inputs/demand_response/dr_shed_capacity_scalar_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,done +dr_shed_hourly.h5,inputs/profiles_dr/dr_shed_hourly_{dr_shedscen}.h5,int(sw.GSw_DRShed) != 0,ignore,ignore,wide,"datetime,year",,1,keepindex,,,,,,Agg/Disagg handled in hourly_load +e.csv,inputs/sets/e.csv,1,ignore,ignore,,,,,0,,,,e,, +eall.csv,inputs/sets/eall.csv,1,ignore,ignore,,,,,,,,,eall,, +emit_rate.csv,,1,ignore,ignore,,"etype,e,i,v,r",,0,0,,,,,,ReEDS-to-PLEXOS output +emitrate.csv,inputs/emission_constraints/emitrate.csv,1,ignore,ignore,,,,,0,,,,,, +energy_communities.csv,inputs/financials/energy_communities.csv,1,ignore,ignore,,,,,0,,,,,,region aggregation and filtering is handled in copy_files +etype.csv,inputs/sets/etype.csv,1,ignore,ignore,,,,,,,,,etype,, +eval_period_adj_mult.csv,,1,ignore,ignore,,,,,0,,,,,, +exog_cap_geohydro.csv,inputs/capacity_exogenous/exog_cap_geohydro_{GSw_SitingGeo}.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,region,"*tech,sc_point_gid,year",,0,0,,,,,, +exog_cap_upv.csv,inputs/capacity_exogenous/exog_cap_upv_{GSw_SitingUPV}.csv,1,ignore,ignore,region,"*tech,sc_point_gid,year",,0,0,,,,,, +exog_cap_wind-ons.csv,inputs/capacity_exogenous/exog_cap_wind-ons_{GSw_SitingWindOns}.csv,1,ignore,ignore,region,"*tech,sc_point_gid,year",,0,0,,,,,, +f.csv,inputs/sets/f.csv,1,ignore,ignore,,,,,,,,,f,, +financials_hydrogen.csv,inputs/financials/financials_hydrogen.csv,int(sw.GSw_H2) != 0,ignore,ignore,,,,,0,,,,,, +financials_sys.csv,inputs/financials/financials_sys_{financials_sys_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +financials_tech.csv,inputs/financials/financials_tech_{financials_tech_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +financials_transmission.csv,inputs/financials/financials_transmission_{financials_trans_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +financing_risk_mult.csv,,1,ignore,ignore,,,,,0,,,,,, +firm_import_limit.csv,,1,ignore,ignore,,,,,0,,1,,,, +firstyear.csv,,1,ignore,ignore,,,,,0,,1,,,, +years_until_endogenous.csv,inputs/plant_characteristics/years_until_endogenous.csv,1,ignore,ignore,,,,,0,,,,,, +flex_frac_all.csv,,1,mean,population,r,"*flextype,h,wide",,1,0,,1,,,, +flex_type.csv,inputs/sets/flex_type.csv,1,ignore,ignore,,,,,,,,,flex_type,, +forced_retirements.csv,inputs/state_policies/forced_retirements.csv,1,ignore,ignore,st,"*i,t",,0,0,,,,,, +forceperiods.csv,,1,ignore,ignore,,,,,,,,,,, +frac_h_ccseason_weights.csv,,1,ignore,ignore,,,,,,,,,,, +frac_h_month_weights.csv,,1,ignore,ignore,,,,,,,,,,, +frac_h_quarter_weights.csv,,1,ignore,ignore,,,,,,,,,,, +fuel2tech.csv,inputs/sets/fuel2tech.csv,1,ignore,ignore,,,,,0,,,,fuel2tech,, +fuel_price.csv,,1,ignore,ignore,,"i,r",,0,0,,,,,,ReEDS-to-PLEXOS output +fuelbin.csv,inputs/sets/fuelbin.csv,1,ignore,ignore,,,,,,,,,fuelbin,, +futurefiles.csv,inputs/userinput/futurefiles.csv,1,ignore,ignore,,,,,0,,,,,, +gb.csv,inputs/sets/gb.csv,1,ignore,ignore,,,,,,,,,gb,, +gbin.csv,inputs/sets/gbin.csv,1,ignore,ignore,,,,,,,,,gbin,, +gbin_min.csv,inputs/growth_constraints/gbin_min.csv,1,ignore,ignore,,,,,0,,,,,, +gen_mandate_tech_list.csv,,1,ignore,ignore,,,,,0,,,,,, +gen_mandate_trajectory.csv,,1,ignore,ignore,,,,,0,,,,,, +geo_discovery_factor.csv,inputs/geothermal/geo_discovery_factor_{geohydrosupplycurve}.csv,int(sw.GSw_Geothermal) != 0,mean,uniform,r,*i,*i,0,0,,,,,, +geo_discovery_rate.csv,inputs/geothermal/geo_discovery_{geodiscov}.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,,,,0,,,,,,, +geo_fom.csv,,1,mean,uniform,r,*i,,0,0,,1,,,, +geo_fom_mult.csv,,1,ignore,ignore,,0,,0,,,,,,, +geo_retirements.csv,,1,sum,ignore,r,"i,v,wide",,1,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +geo_rsc.csv,inputs/geothermal/geo_rsc_ATB_2023.csv,int(sw.GSw_Geothermal) != 0,sc_cat,geosize,r,*i,,0,0,value,,,,, +geocapcostmult.csv,,1,ignore,ignore,,,,1,0,,,,,, +geoexist.csv,,1,ignore,ignore,r,*i,,0,0,,1,,,,written to proper spatial aggregation in writecapdat.py +growth_bin_size_mult.csv,inputs/growth_constraints/growth_bin_size_mult.csv,1,ignore,ignore,,,,,0,,,,,, +growth_limit_absolute.csv,inputs/growth_constraints/growth_limit_absolute.csv,1,ignore,ignore,,,,,0,,,,,, +growth_penalty.csv,inputs/growth_constraints/growth_penalty.csv,1,ignore,ignore,,,,,0,,,,,, +gswitches.csv,,1,ignore,ignore,,,,,0,,,,,, +h2_ba_share.csv,inputs/consume/h2_demand_county_share.csv,int(sw.GSw_H2) != 0,sum,ignore,*r,t,,0,0,,,,,, +gwp.csv,inputs/emission_constraints/gwp.csv,1,ignore,ignore,,,,,0,,,,gwp,, +h2_existing_smr_cap.csv,,1,sum,population,*r,t,,0,0,,1,,,, +h_preh.csv,,1,ignore,ignore,,,,,0,,,,,, +h2_leakage_rate.csv,inputs/emission_constraints/h2_leakage_rate.csv,1,ignore,ignore,,,,,0,,,,,, +h2_exogenous_demand.csv,inputs/consume/h2_exogenous_demand.csv,int(sw.GSw_H2) != 0,ignore,ignore,,p,,1,0,,,,,, +h2_pipeline_cap_cost_mult.csv,,1,ignore,ignore,,,,,,,1,,,, +h2_ptc.csv,,1,ignore,ignore,,,*i,,0,,,,,, +h2_st.csv,inputs/sets/h2_st.csv,1,ignore,ignore,,,,,,,,,h2_st,, +h2_stor.csv,inputs/sets/h2_stor.csv,1,ignore,ignore,,,,,0,,,,h2_stor,, +h2_storage_rb.csv,,int(sw.GSw_H2) != 0,ignore,ignore,rb,,,0,0,,1,,,, +h2_transport_and_storage_costs.csv,inputs/consume/h2_transport_and_storage_costs.csv,int(sw.GSw_H2) != 0,ignore,ignore,,,,,,,,,,, +h_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, +h_ccseason_prm.csv,,1,ignore,ignore,,,,,0,,,,,, +h_dt_szn.csv,,1,ignore,ignore,,,,,0,,1,,,, +h_szn.csv,,1,ignore,ignore,,,,0,0,,,,,, +h_szn_end.csv,,1,ignore,ignore,,,,,0,,,,,, +h_szn_start.csv,,1,ignore,ignore,,,,,0,,,,,, +heat_rate.csv,,1,ignore,ignore,,"i,v,r",,0,0,,,,,,ReEDS-to-PLEXOS output +heat_rate_adj.csv,inputs/plant_characteristics/heat_rate_adj.csv,1,ignore,ignore,,,,,0,,,,,, +heat_rate_mult.csv,inputs/waterclimate/heat_rate_mult.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, +heat_rate_penalty_spin.csv,inputs/plant_characteristics/heat_rate_penalty_spin.csv,1,ignore,ignore,,,,,0,,,,,, +hierarchy.csv,,1,first,ignore,*r,"nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg",,0,0,,1,,,,post_copy column set to 1 since copy_files filters this file separately +hierarchy_itlgrp.csv,,1,ignore,ignore,,,,,0,,1,,,,post_copy column set to 1 since copy_files copies this file separately +hierarchy_original.csv,,1,ignore,ignore,,,,,0,,1,,,,post_copy column set to 1 since copy_files copies this file separately +hierarchy_with_res.csv,,1,ignore,ignore,,,,,0,,1,,,,post_copy column set to 1 since copy_files copies this file separately +hintage_char.csv,inputs/sets/hintage_char.csv,1,ignore,ignore,,,,,,,,,hintage_char,, +hintage_data.csv,,1,ignore,ignore,,,,,0,,,,,,handled separately in WriteHintage.py +hmap_allyrs.csv,,1,ignore,ignore,,,,,0,,,,,, +hmap_myr.csv,,1,ignore,ignore,,,,,0,,,,,, +hour_szn_group.csv,,1,ignore,ignore,,,,,,,,,,, +hourly_szn_end.csv,,1,ignore,ignore,,,,,,,,,,, +hourly_szn_start.csv,,1,ignore,ignore,,,,,,,,,,, +hours_hourly.csv,,1,ignore,ignore,,,,,,,,,,, +hset_hourly.csv,,1,ignore,ignore,,,,,,,,,,, +hyd_add_upg_cap.csv,inputs/supply_curve/hyd_add_upg_cap.csv,int(sw.GSw_HydroCapEnerUpgradeType) == 2,sum,hydroexist,r,"i,rscbin,wide",,1,0,,,,,, +hyd_fom.csv,inputs/hydro/hyd_fom.csv,1,mean,uniform,wide,i,,1,0,,,,,, +hydadjann.csv,inputs/climate/{climatescen}/hydadjann.csv,int(sw.GSw_ClimateHydro) != 0,mean,uniform,r,t,,0,0,,,,,, +hydadjsea.csv,inputs/climate/{climatescen}/hydadjsea.csv,int(sw.GSw_ClimateHydro) != 0,mean,uniform,r,"month,t",,0,0,,,,,, +hydcap.csv,inputs/supply_curve/hydcap.csv,1,sum,geosize,wide,"tech,class",,1,0,,,,,, +hydcapadj.csv,inputs/hydro/SeaCapAdj_hy.csv,1,mean,uniform,r,"*i,month",,0,0,,,,,, +hydcf.csv,,1,ignore,ignore,r,"t,*i,month",,0,0,,1,,,, +hydcf_fixed.csv,inputs/hydro/hydcf_fixed.csv,1,mean,uniform,r,"*i,month",,0,0,,,,,, +hydcost.csv,inputs/supply_curve/hydcost.csv,1,mean,uniform,wide,"tech,class",,1,0,,,,,, +hydro_mingen.csv,inputs/hydro/hydro_mingen.csv,1,mean,uniform,r,"*i,quarter",,0,0,,,,,,might be better to do something capacity-weighted +hydrocapcostmult.csv,,1,ignore,ignore,,,,1,0,,,,,, +hydrofrac_policy.csv,inputs/state_policies/hydrofrac_policy.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,"RPS_All,CES",,,0,,,,,, +hydrogen_price.csv,inputs/fuelprices/h2-combustion_{h2combustionfuelscen}.csv,int(sw.GSw_H2Combustion) != 0,ignore,ignore,,,,0,0,,,,,, +i_coolingtech_watersource.csv,inputs/waterclimate/i_coolingtech_watersource.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, +i_coolingtech_watersource_link.csv,inputs/waterclimate/i_coolingtech_watersource_link.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, +i_coolingtech_watersource_upgrades.csv,inputs/upgrades/i_coolingtech_watersource_upgrades.csv,1,ignore,ignore,,,,,0,,,,,, +i_coolingtech_watersource_upgrades_link.csv,inputs/upgrades/i_coolingtech_watersource_upgrades_link.csv,1,ignore,ignore,,,,,0,,,,,, +i_geotech.csv,inputs/sets/i_geotech.csv,1,ignore,ignore,,,,,0,,,,i_geotech,, +i_p.csv,inputs/sets/i_p.csv,1,ignore,ignore,,,,,0,,,,i_p,, +i_water_nocooling.csv,inputs/sets/i_water_nocooling.csv,1,ignore,ignore,,,,,0,,,,i_water_nocooling,, +incentives.csv,inputs/financials/incentives_{incentives_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +inflation.csv,inputs/financials/inflation_{inflation_suffix}.csv,1,ignore,ignore,,,,0,0,,,,,, +interconnection_queues.csv,inputs/capacity_exogenous/interconnection_queues.csv,1,ignore,ignore,r,"tg,r",,1,0,,,,,, +itc_energy_comm_bonus.csv,,1,mean,ignore,r,*i,,0,0,,1,,,, +itc_frac_monetized.csv,,1,ignore,ignore,,,,,0,,,,,, +itc_fractions.csv,,1,ignore,ignore,,"i,country,t",,0,0,,,,,, +ivt.csv,,1,ignore,ignore,,,,,0,,,,,,created in run.py +ivt_step.csv,,1,ignore,ignore,,,,,0,,,,,, +lcclike.csv,inputs/sets/lcclike.csv,1,ignore,ignore,,,,,0,,,,lcclike,, +load_2010.csv,,1,sum,ignore,r,wide,,1,0,,1,,,,disaggfunc set to ignore because load will already be in correct spatial resolution +load_allyear.csv,,1,sum,ignore,*r,"h,t",,0,0,,1,,,,disaggfunc set to ignore because load will already be in correct spatial resolution +load_multiplier.csv,inputs/load/demand_{demandscen}.csv,1,ignore,ignore,,,,,0,,,,,, +load_multiplier_r.csv,,1,ignore,ignore,,,,1,0,,,,,, +loadsite_annual.csv,inputs/load/loadsite_{GSw_LoadSiteTrajectory}.csv,float(sw.GSw_LoadSiteCF) > 0,ignore,ignore,*loadsitereg,t,,,0,,,,,, +maps.gpkg,,1,ignore,ignore,,,,,,,1,,,, +maxage.csv,inputs/plant_characteristics/maxage.csv,1,ignore,ignore,,,,,0,,,,,, +maxdailycf.csv,inputs/plant_characteristics/maxdailycf.csv,int(sw.GSw_MaxDailyCF) != 0,ignore,ignore,,,,,0,,,,,, +mcs_distributions.yaml,inputs/userinput/mcs_distributions_{MCS_dist}.yaml,int(sw.MCS_runs) != 0,ignore,ignore,,,,,,,,,,, +mcs_group_weights.csv,,1,ignore,ignore,,,,,,,,,,, +methane_leakage_rate.csv,,1,ignore,ignore,,,,0,0,,,,,, +mex_growth_rate.csv,inputs/load/mex_growth_rate.csv,1,ignore,ignore,,,,0,,,,,,, +minCF.csv,inputs/plant_characteristics/minCF.csv,int(sw.GSw_MinCF) != 0,ignore,ignore,,,,,0,,,,,, +min_retire_age.csv,inputs/plant_characteristics/min_retire_age.csv,1,ignore,ignore,,,,,0,,,,,, +mingen_fixed.csv,inputs/plant_characteristics/mingen_fixed.csv,int(sw.GSw_MingenFixed) != 0,ignore,ignore,,,*i,,0,,,,,, +minloadfrac0.csv,inputs/plant_characteristics/minloadfrac0.csv,(int(sw.GSw_Mingen) != 0) or (int(sw.GSw_MinLoading) != 0),ignore,ignore,,,,,0,,,,,, +modeled_regions.csv,inputs/userinput/modeled_regions.csv,1,ignore,ignore,,,,,,,,,,, +modeledyears.csv,,1,ignore,ignore,,,,,0,,,,,, +month2quarter.csv,inputs/temporal/month2quarter.csv,1,ignore,ignore,,,,,0,,,,,, +mttr.csv,inputs/plant_characteristics/mttr.csv,1,ignore,ignore,,,tech,,0,,,,,, +natgas_price_cendiv.csv,inputs/fuelprices/ng_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, +national_rps_frac_allScen.csv,inputs/national_generation/national_rps_frac_allScen.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,,,,1,0,,,,,, +net_gen_existing_hydro.csv,inputs/hydro/net_gen_existing_hydro.csv,1,ignore,ignore,,"t,month",,0,0,,,,,, +peak_net_imports.csv,inputs/reserves/peak_net_imports.csv,1,ignore,ignore,nercr,t,,0,0,,,,,, +nexth.csv,,1,ignore,ignore,,,,,0,,,,,, +nexth_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, +nextpartition.csv,,1,ignore,ignore,,,,,0,,,,,, +ng_crf_penalty.csv,,1,ignore,ignore,,,,0,0,,,,,, +ng_crf_penalty_st.csv,inputs/state_policies/ng_crf_penalty_st.csv,1,ignore,ignore,st,*t,,0,0,,,,,, +ng_demand_elec.csv,inputs/fuelprices/ng_demand_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, +ng_demand_tot.csv,inputs/fuelprices/ng_tot_demand_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, +noretire.csv,inputs/sets/noretire.csv,1,ignore,ignore,,,,,0,,,,noretire,, +notvsc.csv,inputs/sets/notvsc.csv,1,ignore,ignore,,,,,0,,,,notvsc,, +nuclear_ba_ban_list.csv,,int(sw.GSw_NukeStateBan) != 0,ignore,ignore,,,,,,,,,,, +nuclear_energy_communities.csv,inputs/financials/nuclear_energy_communities.csv,1,ignore,ignore,,,,,0,,,,,,region aggregation and filtering is handled in copy_files +nuclear_subsidies.csv,inputs/state_policies/nuclear_subsidies.csv,1,ignore,ignore,*st,year,,0,0,,,,,, +numhours.csv,,1,ignore,ignore,,,,,0,,,,,, +numhours_nexth.csv,,1,ignore,ignore,,,,,0,,1,,,, +objective_function_params.yaml,tests/objective_function_params.yaml,1,ignore,ignore,,,,,,,,,,, +offshore_req.csv,inputs/state_policies/offshore_req_{GSw_OfsWindForceScen}.csv,(int(sw.GSw_StateRPS) != 0) and (int(sw.GSw_OfsWind) != 0),ignore,ignore,st,,,1,0,,,,,, +offshore.csv,,1,ignore,ignore,,,,0,none,,,,,, +ofstype.csv,inputs/sets/ofstype.csv,1,ignore,ignore,,,,,,,,,ofstype,, +ofstype_i.csv,inputs/sets/ofstype_i.csv,1,ignore,ignore,,,,,0,,,,ofstype_i,, +ofswind_rsc_mult.csv,,1,ignore,ignore,,,,1,0,,,,,, +oosfrac.csv,inputs/state_policies/oosfrac.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,*st,,,,0,,,,,, +opres_periods.csv,inputs/reserves/opres_periods.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, +orcat.csv,inputs/sets/orcat.csv,1,ignore,ignore,,,,,,,,,orcat,, +orperc.csv,inputs/reserves/orperc.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, +ortype.csv,inputs/sets/ortype.csv,1,ignore,ignore,,,,,,,,,ortype,, +outage_forced_h.csv,,1,ignore,ignore,r,"*i,h",,0,0,,1,,,,handled in outage_rates.py +outage_forced_hourly.h5,,1,ignore,ignore,wide,index,index,1,keepindex,,1,,,,handled in outage_rates.py +outage_forced_static.csv,inputs/plant_characteristics/outage_forced_static.csv,1,ignore,ignore,,,,,none,,,,,, +outage_forced_temperature.csv,inputs/plant_characteristics/outage_forced_temperature_{GSw_OutageScen}.csv,sw.GSw_OutageScen != 'static',ignore,ignore,,,,,0,,,,,, +outage_scheduled_static.csv,inputs/plant_characteristics/outage_scheduled_static.csv,1,ignore,ignore,,,,,0,,,,,, +outage_scheduled_monthly.csv,inputs/plant_characteristics/outage_scheduled_monthly.csv,1,ignore,ignore,,,,,0,,,,,, +p.csv,inputs/sets/p.csv,1,ignore,ignore,,,,,,,,,p,, +peak_ccseason.csv,,1,sum,ignore,*r,"ccseason,t",,0,0,,1,,,,ok because it's load during peak NERC hour +peak_h.csv,,1,sum,ignore,r,"h,wide",,1,0,,1,,,, +peakload.csv,,1,ignore,ignore,,,,,0,,1,,,, +peakload_nercr.csv,,1,ignore,ignore,,,,,0,,1,,,, +period_szn.csv,,1,ignore,ignore,,,,,0,,,,,, +period_szn_user.csv,inputs/temporal/period_szn_{GSw_HourlyClusterAlgorithm}.csv,sw.GSw_HourlyClusterAlgorithm == 'user*',ignore,ignore,,,,,,,,,,, +period_weights.csv,,1,ignore,ignore,,,,,0,,,,,, +periodmap_1yr.csv,,1,ignore,ignore,,,,0,0,,,,,, +pipeline_cost_mult.csv,,int(sw.GSw_H2) != 0,trans_lookup,uniform,"*r,rr",,,0,0,drop_dup_r,1,,,, +plantcat.csv,inputs/sets/plantcat.csv,1,ignore,ignore,,,,,,,,,plantcat,, +plantcharout.csv,,1,ignore,ignore,,"0,2",,0,,,,,,, +plantchar_beccs.csv,inputs/plant_characteristics/{plantchar_beccs}.csv,int(sw.GSw_BECCS) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_biopower.csv,inputs/plant_characteristics/{plantchar_biopower}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_ccsflex_cost.csv,inputs/plant_characteristics/{ccsflexscen}_cost.csv,(int(sw.GSw_CCSFLEX_BYP) != 0) or (int(sw.GSw_CCSFLEX_DAC) != 0) or (int(sw.GSw_CCSFLEX_STO) != 0),ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_ccsflex_perf.csv,inputs/plant_characteristics/{ccsflexscen}_perf.csv,(int(sw.GSw_CCSFLEX_BYP) != 0) or (int(sw.GSw_CCSFLEX_DAC) != 0) or (int(sw.GSw_CCSFLEX_STO) != 0),ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_coal_ccs.csv,inputs/plant_characteristics/{plantchar_coal_ccs}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_coal.csv,inputs/plant_characteristics/{plantchar_coal}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_battery.csv,inputs/plant_characteristics/{plantchar_battery}.csv,int(sw.GSw_Storage) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_csp.csv,inputs/plant_characteristics/{plantchar_csp}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_dr_shed.csv,inputs/plant_characteristics/dr_shed_capcost_scalars_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,used to define plantcharout.csv +plantchar_dr_shed_vom.csv,inputs/plant_characteristics/dr_shed_vom_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,used to define plantcharout.csv +plantchar_dr_shed_fom.csv,inputs/plant_characteristics/dr_shed_fom_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,used to define plantcharout.csv +plantchar_evmc_shape.csv,inputs/plant_characteristics/evmc_shape_{evmcscen}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_evmc_storage.csv,inputs/plant_characteristics/evmc_storage_{evmcscen}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_fuelcell.csv,inputs/plant_characteristics/{plantchar_fuelcell}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_gas_ccs.csv,inputs/plant_characteristics/{plantchar_gas_ccs}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_gas.csv,inputs/plant_characteristics/{plantchar_gas}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_geo.csv,inputs/plant_characteristics/{plantchar_geo}.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_h2combustion.csv,inputs/plant_characteristics/{plantchar_h2combustion}.csv,int(sw.GSw_H2Combustion) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_hydro.csv,inputs/plant_characteristics/{plantchar_hydro}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_nuclear_smr.csv,inputs/plant_characteristics/{plantchar_nuclear_smr}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_nuclear.csv,inputs/plant_characteristics/{plantchar_nuclear}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_ofswind.csv,inputs/plant_characteristics/{plantchar_ofswind}.csv,int(sw.GSw_OfsWind) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_onswind.csv,inputs/plant_characteristics/{plantchar_onswind}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_other.csv,inputs/plant_characteristics/{plantchar_other}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_pvb.csv,inputs/plant_characteristics/pvb_{pvbscen}.csv,int(sw.GSw_PVB) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_upgrades.csv,inputs/upgrades/{upgradescen}.csv,sw.upgradescen != 'default',ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +plantchar_upv.csv,inputs/plant_characteristics/{plantchar_upv}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv +poi_cap_init.csv,,1,sum,ignore,*r,,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +prepost.csv,inputs/sets/prepost.csv,1,ignore,ignore,,,,,,,,,prepost,, +prescribed_builds_wind-ofs.csv,inputs/capacity_exogenous/prescribed_builds_wind-ofs_{GSw_OffshoreFiles}_{GSw_SitingWindOfs}.csv,int(sw.GSw_OfsWind) != 0,sum,ignore,region,year,,0,0,,,,,,disaggfunc set to ignore because data will be read in at the correct spatial resolution +prescribed_builds_wind-ons.csv,inputs/capacity_exogenous/prescribed_builds_wind-ons_{GSw_SitingWindOns}.csv,1,sum,ignore,region,year,,0,0,,,,,,disaggfunc set to ignore because data will be read in at the correct spatial resolution +prescribed_nonRSC.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +prescribed_rsc.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +prescriptivelink0.csv,inputs/sets/prescriptivelink0.csv,1,ignore,ignore,,,,,0,,,,prescriptivelink0,, +prm_initial.csv,,1,ignore,ignore,*r,,,,0,,1,,,, +prm.csv,,1,ignore,ignore,*r,,,,0,,1,,,, +psh_sc_duration.csv,,1,ignore,ignore,,,,,0,,1,,,,Delete once aggregate_regions.py is moved up +psh_supply_curves_duration.csv,inputs/storage/PSH_supply_curves_durations.csv,int(sw.GSw_Storage) != 0,ignore,ignore,,,,,0,,1,,,, +psh_supply_curves_capacity.csv,inputs/supply_curve/PSH_supply_curves_capacity_{pshsupplycurve}.csv,int(sw.GSw_Storage) != 0,sum,geosize,r,wide,,1,0,,,,,, +psh_supply_curves_cost.csv,inputs/supply_curve/PSH_supply_curves_cost_{pshsupplycurve}.csv,int(sw.GSw_Storage) != 0,mean,uniform,r,wide,,1,0,,,,,, +pv_cf_improve.csv,,1,ignore,ignore,,,,0,,,,,,, +pvb_agg.csv,inputs/sets/pvb_agg.csv,1,ignore,ignore,,,,,0,,,,pvb_agg,, +pvb_bir.csv,,1,ignore,ignore,,,,,0,,,,,, +pvb_config.csv,inputs/sets/pvb_config.csv,1,ignore,ignore,,,,,,,,,pvb_config,, +pvb_ilr.csv,,1,ignore,ignore,,,,,0,,,,,, +pvbcapcostmult.csv,,1,ignore,ignore,,,,0,0,,,,,, +r.csv,,1,first,ignore,0,,,0,,,1,,,,disaggfunc set to ignore because this file is dynamic to the user-defined spatial aggregation level +r_ba.csv,,1,ignore,ignore,,,,,,,,,,, +r_cendiv.csv,,1,ignore,ignore,,,,,,,,,,, +r_county.csv,,1,ignore,ignore,,,,,,,,,,, +r_cs.csv,,1,first,ignore,*r,cs,,0,0,,1,,,,disaggfunc set to ignore because the spatial aggregation level of this file is controlled by the agglevel switch +r_cs_distance_mi.csv,,1,mean,ignore,*r,cs,,0,0,,1,,,,disaggfunc set to ignore because the spatial aggregation level of this file is controlled by the agglevel switch +routes_adjacent.csv,,1,trans_lookup,ignore,"*r,rr",,,0,0,,1,,,,disaggfunc set to ignore because the spatial aggregation level of this file is controlled by the agglevel switch +ramprate.csv,inputs/plant_characteristics/ramprate.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, +ramptime.csv,inputs/reserves/ramptime.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, +rb.csv,,1,first,ignore,0,,,0,,,1,,,,disaggfunc set to ignore because this file is specifically a collection of all valid BA regions +rb_aggreg.csv,,1,ignore,ignore,,,,,,,,,,, +recstyle.csv,inputs/state_policies/recstyle.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,*st,"RPSCat,style",,,0,,,,,, +rectable.csv,inputs/state_policies/rectable.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st_st,st,,,0,,,,,, +regional_cap_cost_diff.csv,inputs/financials/reg_cap_cost_diff_{reg_cap_cost_diff_suffix}.csv,1,mean,ignore,r,wide,,1,0,,,,,,precursor data to reg_cap_cost_diff.csv +regions.csv,,1,ignore,ignore,,,,,0,,,,,,not done but only for retail +resourceclass.csv,inputs/sets/resourceclass.csv,1,ignore,ignore,,,,,,,,,resourceclass,, +resources.csv,,1,resources,ignore,r,"i,ccreg",i,0,0,,1,,,,disaggfunc set to ignore because this file contains all spatial resolutions valid to the model run +retire_penalty.csv,inputs/financials/retire_penalty.csv,1,ignore,ignore,,,,,0,,,,,, +retirements.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +retirements_energy.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +rev_paths.csv,inputs/supply_curve/rev_paths.csv,1,ignore,ignore,,,,,0,,,,,, +rev_transmission_basecost.csv,inputs/transmission/rev_transmission_basecost.csv,1,ignore,ignore,,,,,0,,,,,, +rggi_states.csv,inputs/emission_constraints/rggi_states.csv,int(sw.GSw_RGGI) != 0,ignore,ignore,*st,,,,0,,,,,, +rggicon.csv,inputs/emission_constraints/rggicon.csv,int(sw.GSw_RGGI) != 0,ignore,ignore,,,,0,,,,,,, +rps_fraction.csv,inputs/state_policies/rps_fraction.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,st,,1,0,,,,,, +rsc_combined.csv,,1,sc_cat,ignore,r,"*i,rscbin",*i,0,0,value,1,,,,done for upv/csp/wind; disaggfunc set to ignore because supply curve data is already at county level +rsc_wsc.csv,,1,ignore,ignore,,,,,0,,,,,, +sc_cat.csv,inputs/sets/sc_cat.csv,1,ignore,ignore,,,,,0,,,,sc_cat,, +scalars.csv,inputs/scalars.csv,1,ignore,ignore,,,,,0,,1,,,, +set_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, +set_allh.csv,,1,ignore,ignore,,,,,0,,,,,, +set_allszn.csv,,1,ignore,ignore,,,,,0,,,,,, +set_h.csv,,1,ignore,ignore,,,,,,,,,,, +set_szn.csv,,1,ignore,ignore,,,,,,,,,,, +site_bin_map.csv,,1,ignore,ignore,,,,,0,,,,,, +spur_parameters.csv,,1,ignore,ignore,,,,,0,,,,,,"TODO (only used for plotting so ignoring for now, but should be fixed)" +spurline_cost.csv,,1,ignore,ignore,,,,,0,,1,,,,Delete once aggregate_regions.py is moved up +spurline_sitemap.csv,,1,ignore,ignore,,,,,0,,,,,,handled in writesupplycurves.py +startcost.csv,inputs/plant_characteristics/startcost.csv,int(sw.GSw_StartCost) != 0,ignore,ignore,,,*i,,0,,,,,, +state_cap.csv,inputs/emission_constraints/state_cap.csv,int(sw.GSw_StateCap) != 0,ignore,ignore,*st,t,,0,0,,,,,, +storage_duration.csv,inputs/storage/storage_duration.csv,int(sw.GSw_Storage) != 0,ignore,ignore,,,,,0,,,,,, +storage_duration_pshdata.csv,,1,ignore,ignore,r,"*i,v",,0,0,,1,,,, +storage_mandates.csv,inputs/state_policies/storage_mandates.csv,int(sw.GSw_BatteryMandate) != 0,ignore,ignore,*st,t,,0,0,,,,,, +storinmaxfrac.csv,,1,ignore,ignore,r,"*i,v",,0,0,,1,,,, +stressperiods_user.csv,inputs/temporal/stressperiods_{GSw_PRM_StressModel}.csv,sw.GSw_PRM_StressModel == 'user*',ignore,ignore,,,,,,,,,,, +stressperiods_seed.csv,,1,ignore,ignore,,,,,0,,,,,, +supply_chain_adjust.csv,inputs/financials/supply_chain_adjust.csv,1,ignore,ignore,,,,,0,,,,,, +supplycurve_egs.csv,inputs/supply_curve/supplycurve_egs-reference.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file +supplycurve_upv.csv,inputs/supply_curve/supplycurve_upv-{GSw_SitingUPV}.csv,1,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file +supplycurve_wind-ofs.csv,inputs/supply_curve/supplycurve_wind-ofs-{GSw_SitingWindOfs}.csv,int(sw.GSw_OfsWind) != 0,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file +supplycurve_wind-ons.csv,inputs/supply_curve/supplycurve_wind-ons-{GSw_SitingWindOns}.csv,1,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file +sw.csv,inputs/sets/sw.csv,1,ignore,ignore,,,,,0,,,,sw,, +switches.csv,,1,ignore,ignore,,,,,0,,,,,, +szn_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, +tc_phaseout_schedule.csv,inputs/financials/tc_phaseout_schedule_{GSw_TCPhaseout_schedule}.csv,int(sw.GSw_TCPhaseout) != 0,ignore,ignore,,,,,0,,,,,, +tech-subset-table.csv,inputs/tech-subset-table.csv,1,ignore,ignore,,,,,0,,,,,, +tech_resourceclass.csv,inputs/techs/tech_resourceclass.csv,1,ignore,ignore,,,,,0,,,,,, +techs.csv,inputs/techs/techs_{techs_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +techs_banned.csv,inputs/state_policies/techs_banned.yaml,1,ignore,ignore,wide,i,,,0,,,,,, +techs_banned_ces.csv,inputs/state_policies/techs_banned_ces.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,wide_st,i,,,0,,,,,, +techs_banned_imports_rps.csv,inputs/state_policies/techs_banned_imports_rps.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,wide_st,i,,,0,,,,,, +techs_banned_rps.csv,inputs/state_policies/techs_banned_rps.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,wide_st,i,,,0,,,,,, +temp_UnappWaterMult.csv,,1,ignore,ignore,,,,,0,,1,,,,intermediate file created in climateprep.py to create climate_UnappWaterMult.csv +temp_UnappWaterSeaAnnDistr.csv,,1,ignore,ignore,,,,,0,,1,,,,intermediate file created in climateprep.py to create climate_UnappWaterSeaAnnDistr.csv +temp_hydadjsea.csv,,1,ignore,ignore,,,,,0,,1,,,,intermediate file created in climateprep.py to create climate_hydadjsea.csv +tg_rsc_cspagg.csv,inputs/sets/tg_rsc_cspagg.csv,1,ignore,ignore,,,,,0,,,,tg_rsc_cspagg,, +tg_rsc_cspagg_tmp.csv,inputs/waterclimate/tg_rsc_cspagg_tmp.csv,(int(sw.GSw_CSP) != 0) and (int(sw.GSw_WaterMain) != 0),ignore,ignore,,,,,0,,,,,, +tg_rsc_upvagg.csv,inputs/sets/tg_rsc_upvagg.csv,1,ignore,ignore,,,,,0,,,,tg_rsc_upvagg,, +timestamps.csv,,1,ignore,ignore,,,,,0,,,,,, +trancap_fut.csv,,1,sum,ignore,"*r,rr","status,trtype,t",,0,0,drop_dup_r,1,,,,disaggfunc set to ignore because all transmisison data will be read into model at the correct spatial resolution +trancap_fut_cat.csv,inputs/sets/trancap_fut_cat.csv,1,ignore,ignore,,,,,,,,,trancap_fut_cat,, +trancap_init_energy.csv,,1,ignore,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,, +trancap_init_prm.csv,,1,ignore,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,, +trancap_init_transgroup.csv,,1,ignore,ignore,,,,0,0,,,,,, +trancap_init_itlgrp.csv,,1,ignore,ignore,,,,0,0,,,,,, +tranloss.csv,,1,trans_lookup,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,,disaggfunc set to ignore because all transmisison data will be read into model at the correct spatial resolution +trans_itc_fractions.csv,,1,ignore,ignore,,,,,0,,,,,, +transmission_capacity_future.csv,inputs/transmission/transmission_capacity_future_{lvl}_{GSw_TransScen}.csv,1,sum,ignore,"r,rr","status,trtype,t",,0,0,drop_dup_r,,,,,'ignore’ in disaggfunc because all transmisison data will be read into model at appropriate spatial resolution +transmission_capacity_future_baseline.csv,inputs/transmission/transmission_capacity_future_{lvl}_baseline.csv,1,sum,ignore,"r,rr","status,trtype,t",,0,0,drop_dup_r,,,,,'ignore’ in disaggfunc because all transmisison data will be read into model at appropriate spatial resolution +transmission_cost_ac.csv,inputs/transmission/transmission_cost_ac_{GSw_TransUpgradeMethod}_{lvl}.h5,1,trans_lookup,ignore,"r,rr",tscbin,,0,0,drop_dup_r,,,,, +transmission_cost_dc.csv,inputs/transmission/transmission_cost_dc_{lvl}.csv,1,trans_lookup,ignore,"r,rr",,,0,0,drop_dup_r,,,,, +transmission_distance.csv,inputs/transmission/transmission_distance_{lvl}.h5,1,trans_lookup,ignore,"r,rr",,,0,0,drop_dup_r,,,,,Stored in wide-format h5 to reduce county filesize but converted to long in copy_files.py +transmission_line_fom.csv,,1,trans_lookup,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,,disaggfunc set to ignore because all transmisison data will be read into model at the correct spatial resolution +trtype.csv,inputs/sets/trtype.csv,1,ignore,ignore,,,,,,,,,trtype,, +unapp_water_sea_distr.csv,inputs/waterclimate/unapp_water_sea_distr.csv,int(sw.GSw_WaterMain) != 0,mean,geosize,r,"wst,wide",,1,0,,,,,, +UnappWaterMult.csv,inputs/climate/{climatescen}/UnappWaterMult.csv,int(sw.GSw_ClimateWater) != 0,mean,uniform,r,"wst,month,t",,0,0,,,,,, +UnappWaterMultAnn.csv,inputs/climate/{climatescen}/UnappWaterMultAnn.csv,int(sw.GSw_ClimateWater) != 0,mean,uniform,r,"wst,t",,0,0,,,,,, +UnappWaterSeaAnnDistr.csv,inputs/climate/{climatescen}/UnappWaterSeaAnnDistr.csv,int(sw.GSw_ClimateWater) != 0,mean,uniform,r,"wst,month,t",,0,0,,,,,, +unbundled_limit_ces.csv,inputs/state_policies/unbundled_limit_ces.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,,,,0,,,,,, +unbundled_limit_rps.csv,inputs/state_policies/unbundled_limit_rps.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,,,,0,,,,,, +unitdata.csv,inputs/capacity_exogenous/ReEDS_generator_database_final_{unitdata}.csv,1,ignore,ignore,FIPS,,,,0,,,,,, +unitsize.csv,,1,ignore,ignore,,,,,0,,1,,,, +unitspec_upgrades.csv,inputs/sets/unitspec_upgrades.csv,1,ignore,ignore,,,,,0,,,,unitspec_upgrades,, +upgrade_costs_ccs_coal.csv,,1,ignore,ignore,,,,,0,,,,,, +upgrade_costs_ccs_gas.csv,,1,ignore,ignore,,,,,0,,,,,, +upgrade_hintage_char.csv,inputs/sets/upgrade_hintage_char.csv,1,ignore,ignore,,,,,0,,,,upgrade_hintage_char,, +upgrade_link.csv,inputs/upgrades/upgrade_link.csv,1,ignore,ignore,,,,,0,,,,,, +upgrade_mult_advanced.csv,inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv,int(sw.GSw_UpgradeCost_Mult) != 0,ignore,ignore,,,,,0,,,,,, +upgrade_mult_conservative.csv,inputs/upgrades/upgrade_mult_atb23_ccs_con.csv,int(sw.GSw_UpgradeCost_Mult) == 2,ignore,ignore,,,,,0,,,,,, +upgrade_mult_final.csv,,1,ignore,ignore,,,,,0,,,,,, +upgrade_mult_mid.csv,inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv,"int(sw.GSw_UpgradeCost_Mult) in [0, 4]",ignore,ignore,,,,,0,,,,,, +upgradelink_water.csv,inputs/upgrades/upgradelink_water.csv,1,ignore,ignore,,,,,0,,,,,, +uranium_price.csv,inputs/fuelprices/uranium_{uraniumscen}.csv,1,ignore,ignore,,,,0,0,,,,,, +va_ng_crf_penalty.csv,,1,ignore,ignore,,,,0,0,,,,,, +val_aggreg.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_ba.csv,,1,ignore,ignore,,,,,,,1,,,, +val_itlgrp.csv,,1,ignore,ignore,,,,,,,1,,,, +val_cendiv.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_country.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_county.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_cs.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_hurdlereg.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_interconnect.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_nercr.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_h2ptcreg.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_r.csv,,1,ignore,ignore,0,,,0,none,,1,,,, +val_r_all.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_st.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_transgrp.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_transreg.csv,,1,ignore,ignore,,,,0,none,,,,,, +val_usda_region.csv,,1,ignore,ignore,,,,0,none,,,,,, +var_map.csv,inputs/valuestreams/var_map.csv,1,ignore,ignore,,,,,0,,,,,, +wat_access_cap_cost.csv,inputs/waterclimate/wat_access_cap_cost.csv,int(sw.GSw_WaterMain) != 0,sc_cat,geosize,r,*wst,,0,0,value,,,,, +water_req_psh_10h_1_51.csv,inputs/waterclimate/water_req_psh_10h_1_51.csv,(int(sw.GSw_PSHwatercon) != 0) and (int(sw.GSw_WaterMain) != 0),mean,geosize,r,wide,,1,0,,,,,,not yet ready for county-level disaggregation +water_with_cons_rate.csv,inputs/waterclimate/water_with_cons_rate.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, +wind_retirements.csv,,1,sum,ignore,r,"i,v,wide",,1,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py +windcfmult.csv,,1,ignore,ignore,,,,1,0,,,,,, +windcfout.csv,,1,ignore,ignore,,,,1,0,,,,,, +windows.csv,inputs/userinput/windows_{windows_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, +wst_climate.csv,inputs/sets/wst_climate.csv,1,ignore,ignore,,,,,0,,,,wst_climate,, +x.csv,,1,ignore,ignore,,,,,0,,,,,, +x_r.csv,,1,first,ignore,"*x,r",,,0,0,,1,,,,handled in writesupplycurves.py +yearafter.csv,inputs/sets/yearafter.csv,1,ignore,ignore,,,,,,,,,yearafter,, +inputs.gdx,,1,ignore,ignore,,,,,0,,,,,, +plexos_inputs.gdx,,1,ignore,ignore,,,,,0,,,,,, +load.h5,,1,sum,ignore,wide,"year,hour",,1,keepindex,,1,,,,Disaggregation handled in hourly_load.py +recf.h5,,1,recf,ignore,wide,datetime,,1,keepindex,,1,,,, +csp.h5,,1,csp,ignore,wide,datetime,,1,keepindex,,1,,,, +gswitches.txt,,1,ignore,ignore,,,,,0,,,,,, +scalars.txt,,1,ignore,ignore,,,,,0,,,,,, +Project.toml,Project.toml,1,ignore,ignore,,,,,,,,,,, +gamslice.txt,gamslice.txt,0,ignore,ignore,,,,,,,,,,, +max_hintage_number.txt,,1,ignore,ignore,,,,,0,,,,,, +run.py,run.py,1,ignore,ignore,,,,,,,,,,, diff --git a/reeds/input_processing/transmission.py b/reeds/input_processing/transmission.py new file mode 100644 index 00000000..6d137faf --- /dev/null +++ b/reeds/input_processing/transmission.py @@ -0,0 +1,641 @@ +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import geopandas as gpd +import pandas as pd +import numpy as np +import os +import sys +import datetime +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +tic = datetime.datetime.now() + +#%% Parse arguments +parser = argparse.ArgumentParser(description="Format and write climate inputs") +parser.add_argument('reeds_path', help='ReEDS directory') +parser.add_argument('inputs_case', help='output directory (inputs_case)') + +args = parser.parse_args() +reeds_path = args.reeds_path +inputs_case = args.inputs_case + +# #%% Settings for testing ### +# reeds_path = reeds.io.reeds_path +# inputs_case = str(Path(reeds_path,'runs','v20260409_itlM0_WECC_county','inputs_case')) + +#%%################# +### FIXED INPUTS ### + +decimals = 5 +drop_canmex = True +dollar_year = 2004 +weight = 'cost' + +costcol = f'USD{dollar_year}perMW' + +#%% Set up logger +log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), +) +print('Starting transmission.py', flush=True) + +#%% Inputs from switches +sw = reeds.io.get_switches(inputs_case) + +## networksource must end in a 4-digit year indicating the year represented by the network +trans_init_year = int(sw.GSw_TransNetworkSource[-4:]) + +valid_regions = {} +for level in ['r','itlgrp','transgrp']: + valid_regions[level] = pd.read_csv( + os.path.join(inputs_case, f'val_{level}.csv'), header=None).squeeze(1).tolist() + + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== +def get_trancap_init(case, networksource='NARIS2024', level='r'): + """ + AC capacity is defined for each direction and calculated using the scripts at + https://github.nrel.gov/ReEDS/TSC + """ + sw = reeds.io.get_switches(case) + trancap_init_ac = ( + reeds.parse.get_itls(case, level=level, GSw_ZoneSet=sw.GSw_ZoneSet) + [['r', 'rr', 'MW_forward', 'MW_reverse']] + .assign(trtype='AC') + ) + ### DEPRECATED: p19 is islanded with NARIS transmission data, so connect it manually + if ( + (networksource == 'NARIS2024') + and (level != 'transgrp') + and ('p19' in valid_regions['r']) + and ('p20' in valid_regions['r']) + ): + trancap_init_ac = pd.concat([ + trancap_init_ac, + pd.Series({ + 'r':'p19', + 'rr':'p20', + 'MW_forward':0.001, + 'MW_reverse':0.001, + 'trtype':'AC', + }).to_frame().T + ], ignore_index=True) + + ### DC + if level == 'r': + ## transgrp capacity is only defined for AC + hvdc = reeds.parse.map_hvdc_lines_to_interfaces(case).assign(trtype='LCC') + b2b = reeds.parse.get_b2b(case).assign(trtype='B2B') + ## DC capacity is only defined in one direction, + ## so duplicate it for the opposite direction + trancap_init_nonac_undup = pd.concat([hvdc, b2b])[['r', 'rr', 'trtype', 'MW']] + trancap_init_nonac = pd.concat([ + trancap_init_nonac_undup, + trancap_init_nonac_undup.rename(columns={'r':'rr', 'rr':'r'}) + ], axis=0) + else: + trancap_init_nonac = pd.DataFrame(columns=['r', 'rr', 'trtype', 'MW']) + + ### Initial trading limit, using contingency levels specified by contingency level + ### (but assuming full capacity of DC is available for both energy and capcity) + dfout = ( + pd.concat( + [ + ## AC + pd.concat([ + ## Forward direction + (trancap_init_ac[['r', 'rr', 'trtype', 'MW_forward']] + .rename(columns={'MW_forward':'MW'})), + ## Reverse direction + (trancap_init_ac[['r', 'rr', 'trtype', 'MW_reverse']] + .rename(columns={'r':'rr', 'rr':'r', 'MW_reverse':'MW'})) + ], axis=0), + ## DC + trancap_init_nonac[['r', 'rr', 'trtype', 'MW']] + ], + axis=0 + ) + ## Drop entries with zero capacity + .replace(0.,np.nan).dropna() + .groupby(['r', 'rr', 'trtype']).sum().reset_index() + ) + dfout = dfout.loc[ + dfout['r'].isin(valid_regions[level]) + & dfout['rr'].isin(valid_regions[level]) + ].copy() + + ## Get alias for level (e.g. rr, transgrpp) + levell = level + level[-1] + return dfout.rename(columns={'r':level, 'rr':levell}) + + +def calculate_adjacent_routes(dfzones): + routes_adjacent = dfzones.copy() + routes_adjacent['r_adj'] = routes_adjacent.apply( + axis=1, + func=lambda x: ( + routes_adjacent.loc[( + routes_adjacent.touches(x['geometry']) + | routes_adjacent.overlaps(x['geometry']) + )] + .index + .values + .tolist() + ) + ) + # Reformat so that each row represents a pair of regions + routes_adjacent = ( + routes_adjacent.drop(columns='geometry') + .explode('r_adj') + .reset_index(names=['r']) + .rename(columns={'r': '*r', 'r_adj': 'rr'}) + [['*r', 'rr']] + .dropna() + ) + + return routes_adjacent + + +def calculate_co2_storage_routes(dfzones, co2_storage_sites): + # Determine the storage sites that are within 200 miles + # of each region's transmission endpoint + region_centroids = ( + gpd.GeoDataFrame( + dfzones[['x', 'y']], + geometry=gpd.points_from_xy(dfzones.x, dfzones.y), + crs=dfzones.crs + ) + [['geometry']] + .rename_axis(index='*r') + .reset_index() + ) + region_centroids['cs'] = region_centroids.apply( + axis=1, + func=lambda x: ( + co2_storage_sites.loc[( + co2_storage_sites.distance(x['geometry']) / 1609.34 <= 200 + )] + ['cs'] + .tolist() + ) + ) + + # Calculate the lengths of the spurlines between regions and storage sites, + # excluding routes not completely within the U.S. + routes_cs = ( + region_centroids.explode('cs') + .merge( + co2_storage_sites[['cs', 'geometry']], + on='cs', + suffixes=('_region', '_site') + ) + .assign( + geometry=lambda x: ( + gpd.GeoSeries(x['geometry_region']) + .shortest_line(gpd.GeoSeries(x['geometry_site'])) + ) + ) + [['*r', 'cs', 'geometry']] + ) + routes_cs = gpd.GeoDataFrame(routes_cs, geometry='geometry', crs=dfzones.crs) + routes_cs = routes_cs.loc[( + routes_cs.within( + reeds.io.get_dfmap(levels=['country'])['country'].loc['USA','geometry'] + ) + | (routes_cs.length == 0) + )] + routes_cs['distance_m'] = routes_cs.length + routes_cs['miles'] = (routes_cs['distance_m'] / 1609.34).round(2) + + return routes_cs + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +#%% Limits on PRMTRADE across nercr boundaries +if not int(sw.GSw_PRM_NetImportLimit): + ## No limit + firm_import_limit = pd.DataFrame(columns=['*nercr','t','fraction']).set_index(['*nercr','t']) +else: + limits = pd.Series( + {int(i.split('_')[0]): i.split('_')[1] for i in sw.GSw_PRM_NetImportLimitScen.split('/')} + ) + + solveyears = pd.read_csv( + os.path.join(inputs_case,'modeledyears.csv') + ).columns.astype(int).tolist() + startyear = min(solveyears) + endyear = max(solveyears) + allyears = range(startyear, max(endyear, limits.index.max())+1) + + ## calculate the historical net_firm_import fraction for each region and drop negative values + peak_net_imports = pd.read_csv( + os.path.join(inputs_case,'peak_net_imports.csv'), + index_col=['nercr'] + ) + net_firm_import_frac = ( + peak_net_imports.MW / peak_net_imports.MW_TotalDemand + ).clip(lower=0) + nercrs = net_firm_import_frac.index + + _dfout = {} + for key, val in limits.items(): + ## If 'hist' is in GSw_PRM_NetImportLimitScen, + ## all years up until that year use the historical regional max + if val == 'hist': + for y in range(startyear, key+1): + _dfout[y] = net_firm_import_frac + ## If 'histmax', all prior years use the historical max across all regions + elif val == 'histmax': + for y in range(startyear, key+1): + _dfout[y] = net_firm_import_frac.clip(lower=net_firm_import_frac.max()) + else: + ## Input values are percentages so convert to fractions + _dfout[key] = pd.Series(index=nercrs, data=float(val) / 100) + + firm_import_limit = ( + pd.concat(_dfout, names=('t',)).unstack('nercr') + ## Linear interpolation between values; flat projections before and after + .reindex(allyears).interpolate('linear').bfill().ffill() + .loc[solveyears] + .unstack('t').rename('fraction').rename_axis(['*nercr','t']) + ) + +firm_import_limit.to_csv(os.path.join(inputs_case, 'firm_import_limit.csv')) + + +#%% Load the transmission scalars +scalars = reeds.io.get_scalars(inputs_case) +### Put some in dicts for easier access +tranloss_permile = { + 'AC': scalars['tranloss_permile_ac'], + ### B2B converters are AC-AC/DC-DC/AC-AC, so use AC per-mile losses + 'B2B': scalars['tranloss_permile_ac'], + 'LCC': scalars['tranloss_permile_dc'], + 'VSC': scalars['tranloss_permile_dc'], +} +tranloss_fixed = { + 'AC': 1 - scalars['converter_efficiency_ac'], + 'B2B': 1 - scalars['converter_efficiency_lcc'], + 'LCC': 1 - scalars['converter_efficiency_lcc'], + 'VSC': 1 - scalars['converter_efficiency_vsc'], +} + + +#%% Get single-link distances and losses +interface_params = pd.read_csv( + os.path.join(inputs_case,'transmission_distance.csv'), +) +interface_params['r_rr'] = interface_params.r + '_' + interface_params.rr + +# Apply the distance multiplier +interface_params['miles'] = interface_params['miles'] * float(sw.GSw_TransSquiggliness) + +# Make sure there are no duplicates +if interface_params[['r','rr']].duplicated().sum(): + print( + interface_params.loc[ + interface_params[['r','rr']].duplicated(keep=False) + ].sort_values(['r','rr']) + ) + raise Exception('Duplicate entries in transmission_distance.csv') + +### Calculate losses +def getloss(row, trtype='AC'): + """ + Fixed losses are entered as per-endpoint values (e.g. for each AC/DC converter station + on a LCC DC line). There are two endpoints per line, so multiply fixed losses by 2. + Note that this approach only applies for LCC DC lines; VSC AC/DC losses are applied later. + """ + return row.miles * tranloss_permile[trtype] + tranloss_fixed[trtype] * 2 + +trtypes = ['AC', 'LCC', 'B2B', 'VSC'] +interface_params = pd.concat( + { + trtype: + interface_params.assign(loss=interface_params.apply(getloss, args=(trtype,), axis=1)) + for trtype in trtypes + }, + axis=0, + names=('trtype',), +).reset_index(level='trtype').set_index(['r','rr','trtype']) + + +#%% Include distances for existing lines +transmission_distance = interface_params.miles.copy() + +#%% Write the line-specific transmission FOM costs [$/MW/year] +trans_fom_region_mult = int(scalars['trans_fom_region_mult']) +trans_fom_frac = scalars['trans_fom_frac'] + +### For simplicity we just take the unweighted average base cost across +### the four regions for which we have transmission cost data. +### Future work should identify a better assumption. +rev_transcost_base = pd.read_csv( + os.path.join(inputs_case,'rev_transmission_basecost.csv'), + header=[0], skiprows=[1], +).replace({'500ACsingle':'AC','500DCbipole':'LCC'}).set_index('Voltage') + +transfom_USDperMWmileyear = { + trtype: ( + rev_transcost_base.loc[trtype][['TEPPC','SCE','MISO','Southeast']].mean() + * trans_fom_frac + ) + for trtype in ['AC','LCC'] +} +### B2B is treated like (AC line)-(AC/DC converter)-(AC/DC converter)-(AC line) so uses AC line FOM +transfom_USDperMWmileyear['B2B'] = transfom_USDperMWmileyear['AC'] +transfom_USDperMWmileyear['VSC'] = transfom_USDperMWmileyear['LCC'] + +if trans_fom_region_mult: + ### Multiply line-specific $/MW by FOM fraction to get $/MW/year + transmission_line_fom = interface_params[costcol] * trans_fom_frac + ### Use regional average * distance_initial for existing lines + append = transmission_distance.loc[ + transmission_distance.reset_index().trtype.isin( + ['AC','LCC','B2B','VSC']).set_axis(transmission_distance.index) + ] +else: + ### Multiply $/MW/mile/year by distance [miles] to get $/MW/year for ALL lines + transmission_line_fom = ( + transmission_distance.reset_index().trtype.map(transfom_USDperMWmileyear) + * transmission_distance.values + ).set_axis(transmission_distance.index).rename('USDperMWyear') + + +#%%### Write files for ReEDS (adding * to make GAMS read column names as comment) +### transmission_distance +transmission_distance.round(3).reset_index().rename(columns={'r':'*r'}).to_csv( + os.path.join(inputs_case,'transmission_miles.csv'), index=False) + +### tranloss +tranloss = interface_params['loss'].reset_index() +tranloss.round(decimals).rename(columns={'r':'*r'}).to_csv( + os.path.join(inputs_case,'tranloss.csv'), index=False, header=True) + +### transmission_line_fom +transmission_line_fom.round(2).rename_axis(('*r','rr','trtype')).to_csv( + os.path.join(inputs_case,'transmission_line_fom.csv')) + +#%% Write the initial capacities +case = Path(inputs_case).parent +trancap_init = {} +for captype, level in [ + ('energy', 'r'), + ('transgroup', 'transgrp'), +]: + trancap_init[captype] = get_trancap_init( + case=case, networksource=sw.GSw_TransNetworkSource, level=level) + ### TEMPORARY 20260402: Drop county interfaces with no distance/cost + if (level == 'r') and (sw.GSw_RegionResolution in ['county', 'mixed']): + indices = ['r', 'rr', 'trtype'] + drop = ( + trancap_init[captype] + .merge(transmission_line_fom.reset_index(), on=indices, how='left') + ) + drop = list(drop.loc[drop.USDperMWyear.isnull(), indices].itertuples(index=False)) + trancap_init[captype] = trancap_init[captype].set_index(indices).drop(drop).reset_index() + trancap_init[captype].rename(columns={level:'*'+level}).round(3).to_csv( + os.path.join(inputs_case,f'trancap_init_{captype}.csv'), + index=False, + ) +trancap_init['energy'].rename(columns={'r':'*r'}).round(3).to_csv( + os.path.join(inputs_case,'trancap_init_prm.csv'), + index=False, +) +### TEMPORARY 20260402: Skip itlgrp functionality until we fix it +# ### Also write itlgrp capacity +# trancap_itlgrp = trancap_init['energy'].copy() +# ## Map counties to itlgrp's +# hierarchy_itlgrp = pd.read_csv(os.path.join(inputs_case, 'hierarchy_itlgrp.csv')) +# itl_d = dict(zip(hierarchy_itlgrp['*r'], hierarchy_itlgrp['itlgrp'])) +# for r in ['r', 'rr']: +# trancap_itlgrp[r] = trancap_itlgrp[r].map(lambda x: itl_d.get(x,x)) +# trancap_itlgrp.rename(columns={'r':'*itlgrp', 'rr':'itlgrpp'}).round(3).to_csv( +# os.path.join(inputs_case, 'trancap_init_itlgrp.csv'), +# index=False, +# ) + + +#%%### Future transmission capacity +## note that '0' is used as a filler value in the t column for firstyear_trans, which is defined +## in inputs/scalars.csv. So we replace it whenever we load a transmission_capacity_future file. +trancap_fut = ( + pd.concat( + [ + pd.read_csv( + os.path.join(inputs_case, 'transmission_capacity_future_baseline.csv'), + comment='#', + ), + pd.read_csv( + os.path.join(inputs_case, 'transmission_capacity_future.csv'), + comment='#', + ) + ], + axis=0, + ignore_index=True, + ) + .astype({'t': int}) + .drop(['Notes', 'notes', 'Note', 'note'], axis=1, errors='ignore') + .replace({'t': {0: int(scalars['firstyear_trans_longterm'])}}) +) + +### Drop prospective AC lines from years <= trans_init_year +trancap_fut = trancap_fut.drop( + trancap_fut.loc[ + (trancap_fut.t <= trans_init_year) + & (trancap_fut.trtype == 'AC') + ].index, +).copy() + +trancap_fut.rename(columns={'r':'*r'}).astype({'t':int}).round(3).to_csv( + os.path.join(inputs_case,'trancap_fut.csv'), index=False) + + +#%%### Transmission upgrade supply curve +transmission_cost_ac = pd.read_csv( + os.path.join(inputs_case, 'transmission_cost_ac.csv') +) +### Interfaces are always defined with the zones sorted in lexicographic order +reverse_interfaces = transmission_cost_ac.loc[ + transmission_cost_ac.apply(lambda row: row.r > row.rr, axis=1) +] +for i, row in reverse_interfaces.iterrows(): + transmission_cost_ac.loc[i, ['r', 'rr']] = transmission_cost_ac.loc[i, ['rr', 'r']].values + transmission_cost_ac.loc[i, ['USD2004perMW_forward', 'USD2004perMW_reverse']] = ( + transmission_cost_ac.loc[i, ['USD2004perMW_reverse', 'USD2004perMW_forward']].values + ) + +_test = transmission_cost_ac.apply(lambda row: row.r < row.rr, axis=1) +if not _test.all(): + print(transmission_cost_ac.loc[~_test]) + err = ( + "Must have r < rr in AC transmission cost inputs but the interfaces " + "listed above are out of order" + ) + raise ValueError(err) + +labels = { + 'binwidth_USD2004': 'binwidth', + 'USD2004perMW_forward': 'forward', + 'USD2004perMW_reverse': 'reverse', +} +for col, label in labels.items(): + transmission_cost_ac[['r','rr','tscbin',col]].rename(columns={'r':'*r'}).round(2).to_csv( + os.path.join(inputs_case, f'tsc_{label}.csv'), + index=False, + ) +transmission_cost_ac.tscbin.drop_duplicates().to_csv( + os.path.join(inputs_case, 'tscbin.csv'), + index=False, + header=False, +) + + +#%% DC and B2B transmission cost +## Get DC line cost +transmission_cost_dc = pd.read_csv(os.path.join(inputs_case, 'transmission_cost_dc.csv')) + +## B2B is: (zone center)--------(AC/DC converter)(DC/AC converter)--------(zone center) +## ^ AC line ^ AC line +## so use AC per-mile costs. +b2b_links = trancap_init['energy'].loc[ + (trancap_init['energy'].trtype=='B2B') + & (trancap_init['energy'].r < trancap_init['energy'].rr) +].set_index(['r','rr']).index +## Take the weighted average of the whole supply curve (for the default 500 kV assumption +## the supply curve only has one bin per interface, so it doesn't matter; when we add the +## full supply curve, we'll need to include entries for these B2B-containing interfaces). +df = transmission_cost_ac.set_index(['r','rr']).loc[b2b_links].copy() +df['cost_weighted'] = ( + df.binwidth_USD2004 + * df.USD2004perMW_forward +) +transmission_cost_b2b = ( + df.groupby(['r','rr','tscbin']).cost_weighted.sum() + / df.groupby(['r','rr','tscbin']).binwidth_USD2004.sum() +).reset_index(level='tscbin', drop=True).rename('USD2004perMW').reset_index() +## Add the reverse direction and write it +transmission_cost_b2b = pd.concat([ + transmission_cost_b2b, + transmission_cost_b2b.rename(columns={'r':'rr', 'rr':'r'}) +]) + +### Write the combined cost table +transmission_cost_nonac = ( + pd.concat({ + 'LCC': transmission_cost_dc, + 'B2B': transmission_cost_b2b, + 'VSC': transmission_cost_dc, + }, names=('trtype','drop')) + .reset_index('drop', drop=True) + .reset_index() + .rename(columns={'r':'*r'}) + [['*r','rr','trtype','USD2004perMW']] +) +transmission_cost_nonac.round(2).to_csv( + os.path.join(inputs_case, 'transmission_cost_nonac.csv'), + index=False, +) + + +#%%### Hurdle rates +hurdle_levels = [1, 2] +cost_hurdle_intra = ( + pd.read_csv(os.path.join(inputs_case, 'cost_hurdle_intra.csv')) + .rename(columns={'t':'*t'}).set_index('*t').round(3) +) +cost_hurdle_rate = { + i: ( + cost_hurdle_intra[sw[f'GSw_TransHurdleLevel{i}']] if int(sw.GSw_TransHurdleRate) + else pd.Series(name='region').rename_axis('*t') + ) + for i in hurdle_levels +} +for i in hurdle_levels: + cost_hurdle_rate[i].to_csv(os.path.join(inputs_case, f'cost_hurdle_rate{i}.csv')) + + +#%%### H2 pipeline cost multipliers +# Calculate H2 pipeline cost multipliers by dividing the [$/mile] cost of DC transmission +# between each pair of regions by the minimum interface [$/mile] cost for DC transmission +# and subtracting 1 to get a fractional adder (which is then added to 1 in b_inputs.gms) +fpath = os.path.join(inputs_case, 'pipeline_cost_mult.csv') +if len(transmission_cost_nonac): + dc_cost_permile = ( + transmission_cost_nonac.rename(columns={'*r':'r'}) + .set_index(['trtype','r','rr']).loc['LCC'].squeeze(1) + / interface_params.xs('LCC', 0, 'trtype').miles + ) + pipeline_cost_mult = dc_cost_permile.rename('multiplier') / dc_cost_permile.min() - 1 + + pipeline_cost_mult.reset_index().rename(columns={'r':'*r'}).round(3).to_csv( + fpath, + index=False, + ) +else: + pd.DataFrame(columns=['*r','rr','multiplier']).to_csv(fpath, index=False) + + +# Get model regions +dfzones = reeds.io.get_dfmap( + os.path.dirname(inputs_case), + levels=['r'], + exclude_water_areas=True +)['r'] + +#%%### Adjacent regions +# Determine which pairs of model regions are adjacent to each other +routes_adjacent = calculate_adjacent_routes(dfzones) +routes_adjacent.to_csv( + os.path.join(inputs_case,'routes_adjacent.csv'), + index=False +) + +#%%### CO2 storage sites +# Determine spurline routes from model regions to carbon storage sites +co2_storage_sites = reeds.io.get_co2_storage_sites() +routes_cs = calculate_co2_storage_routes(dfzones, co2_storage_sites) +routes_cs[['*r', 'cs']].to_csv( + os.path.join(inputs_case, 'r_cs.csv'), index=False +) +routes_cs[['*r', 'cs', 'miles']].to_csv( + os.path.join(inputs_case,'r_cs_distance_mi.csv'), + index=False +) + +# Determine sites that have valid routes to model regions +val_cs = pd.Series(routes_cs['cs'].unique()) +val_cs.to_csv(os.path.join(inputs_case, 'val_cs.csv'), header=False, index=False) + +# Subset CO2 site characteristics data to valid sites +co2_site_char = pd.read_csv(os.path.join(inputs_case, 'co2_site_char.csv')) +co2_site_char = co2_site_char.loc[co2_site_char['cs'].isin(val_cs)] +co2_site_char.to_csv(os.path.join(inputs_case, 'co2_site_char.csv'), index=False) + +# Create WKT file of region-to-site spurlines +r_cs_spurlines = ( + routes_cs.loc[routes_cs['distance_m'] > 0] + .rename(columns={'*r': 'ba_str', 'cs': 'FmnID'}) + .to_crs('EPSG:4326') + .assign(WKT=lambda x: x['geometry'].to_wkt()) + [['ba_str', 'FmnID', 'distance_m', 'WKT']] +) +r_cs_spurlines.to_csv( + os.path.join(inputs_case,'ctus_r_cs_spurlines_200mi.csv'), + index=False +) + +#%% Finish the timer +reeds.log.toc(tic=tic, year=0, process='inputs/transmission.py', + path=os.path.join(inputs_case,'..')) +print('Finished transmission.py', flush=True) diff --git a/reeds/input_processing/writecapdat.py b/reeds/input_processing/writecapdat.py new file mode 100644 index 00000000..64965f83 --- /dev/null +++ b/reeds/input_processing/writecapdat.py @@ -0,0 +1,897 @@ +""" +The purpose of this script is to gather individual generator data from the +NEMS generator database and organize this data into various categories, such as: + - Non-RSC Existing Capacity + - Non-RSC Prescribed Capacity + - RSC Existing Capacity + - RSC Prescribed Capacity + - SMR Existing Capacity + - Retirement Data + - Generator Retirements + - Wind Retirements + - Non-RSC Retirements + - Hydro Capacity Adjustment Factors - ccseasons + - Waterconstraint Indexing + - Canadian Imports +The categorized datasets are then written out to various csv files for use +throughout the ReEDS model. + +Some notes on the NEMS database: +* Capacity is assumed to retire at the BEGINNING of 'RetireYear'. So if a row's + 'RetireYear' is 2015, that capacity is assumed to retire at 2014-12-31T23:59:59. +""" + +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== +import argparse +import datetime +import numpy as np +import os +import sys +import pandas as pd +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds + + +#%%################# +### FIXED INPUTS ### + +# Generator database column selections: +Sw_onlineyearcol = 'StartYear' + + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== +def create_rsc_wsc(gendb,TECH,scalars,startyear): + + rsc_wsc = gendb.loc[(gendb['tech'].isin(TECH['rsc_wsc'])) & + (gendb[Sw_onlineyearcol] < startyear) & + (gendb['RetireYear'] > startyear) + ] + + rsc_wsc = rsc_wsc[['r','tech','summer_power_capacity_MW']].rename(columns={'tech':'i','summer_power_capacity_MW':'value'}) + # Multiply all PV capacities by ILR + for j,row in rsc_wsc.iterrows(): + if row['i'] == 'upv': + rsc_wsc.loc[j,'value'] *= scalars['ilr_utility'] + + return rsc_wsc + +#%% =========================================================================== +### --- SUPPLEMENTAL DATA --- +### =========================================================================== + +######################### +### STATIC DICTIONARY ### +''' +This dictionary must be placed at the module level of this script to be used with the +create_rsc_wsc() function in aggregate_regions +''' + +TECH = { + 'capnonrsc': [ + 'coaloldscr', 'coalolduns', 'biopower', 'coal-igcc', + 'coal-new', 'gas-cc', 'gas-ct', 'lfill-gas', + 'nuclear', 'o-g-s', 'battery_li', 'pumped-hydro' + ], + 'capnonrsc_energy': [ + 'battery_li' + ], + 'prescribed_nonRSC': [ + 'coal-new', 'lfill-gas', 'gas-ct', 'o-g-s', 'gas-cc', + 'hydED', 'hydEND', 'hydND', 'hydNPND', 'hydUD', 'hydUND', + 'geothermal', 'biopower', 'coal-igcc', 'nuclear', + 'battery_li','pumped-hydro','coaloldscr', + ], + 'prescribed_nonRSC_energy': [ + 'battery_li', + ], + 'storage' : ['battery_li', 'pumped-hydro' + ], + 'rsc_all': ['upv','pvb','csp-ns'], + 'rsc_csp': ['csp-ns'], + 'rsc_wsc': ['upv','pvb','csp-ns','csp-ws','wind-ons','wind-ofs', + 'geohydro_allkm','egs_allkm'], + 'prsc_all': ['upv','pvb','csp-ns','csp-ws'], + 'prsc_upv': ['upv','pvb'], + 'prsc_w': ['wind-ons','wind-ofs'], + 'prsc_csp': ['csp-ns','csp-ws'], + 'prsc_geo': ['geohydro_allkm','egs_allkm'], + 'retirements': [ + 'coalolduns', 'o-g-s', 'hydED', 'hydEND', 'gas-ct', 'lfill-gas', + 'coaloldscr', 'biopower', 'gas-cc', 'coal-new', + 'battery_li','nuclear', 'pumped-hydro', 'coal-igcc', + ], + 'retirements_energy': [ + 'battery_li' + ], + 'windret': ['wind-ons'], + 'georet': ['geohydro_allkm','egs_allkm'], + # This is not all technologies that do not having cooling, but technologies + # that are (or could be) in the plant database. + 'no_cooling': [ + 'upv', 'pvb', 'gas-ct', 'geohydro_allkm','egs_allkm', + 'battery_li', 'pumped-hydro', 'pumped-hydro-flex', + 'hydUD', 'hydUND', 'hydD', 'hydND', 'hydSD', 'hydSND', 'hydNPD', + 'hydNPND', 'hydED', 'hydEND', 'wind-ons', 'wind-ofs', + ], +} + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== + +def main(reeds_path, inputs_case, agglevel, regions): + + # #%% Settings for testing + #reeds_path = "/Users/apham/Documents/GitHub/ReEDS/ReEDS-2.0/" + #inputs_case = os.path.join(reeds_path,'runs','test_newNEMS_OR_water','inputs_case') + + + ######################### + ### SUPPLEMENTAL DATA ### + + quartershorten = {'spring':'spri','summer':'summ','fall':'fall','winter':'wint'} + + hotcold_months = {'NOV':'cold', 'DEC':'cold', 'JAN':'cold', 'FEB':'cold', + 'JUN':'hot', 'JUL':'hot', 'AUG':'hot' + } + + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + retscen = sw.retscen + GSw_WaterMain = int(sw.GSw_WaterMain) + GSw_PVB = int(sw.GSw_PVB) + startyear = int(sw.startyear) + endyear = int(sw.endyear) + + scalars = reeds.io.get_scalars(inputs_case) + + years = pd.read_csv( + os.path.join(inputs_case,'modeledyears.csv') + ).columns.astype(int).values.tolist() + + #################### + ### DICTIONARIES ### + + COLNAMES = { + 'capnonrsc': ( + ['tech','coolingwatertech','r','ctt','wst','summer_power_capacity_MW'], + ['i','coolingwatertech','r','ctt','wst','value'] + ), + 'capnonrsc_energy': ( + ['tech','r','energy_capacity_MWh'], + ['i','r','value'] + ), + 'prescribed_nonRSC': ( + [Sw_onlineyearcol,'r','tech','coolingwatertech','ctt','wst','summer_power_capacity_MW'], + ['t','r','i','coolingwatertech','ctt','wst','value'] + ), + 'prescribed_nonRSC_energy': ( + [Sw_onlineyearcol,'r','tech','coolingwatertech','ctt','wst','energy_capacity_MWh'], + ['t','r','i','coolingwatertech','ctt','wst','value'] + ), + 'rsc': ( + ['tech','r','ctt','wst','summer_power_capacity_MW'], + ['i','r','ctt','wst','value'] + ), + 'rsc_wsc': ( + ['r','tech','summer_power_capacity_MW'], + ['r','i','value'] + ), + 'prsc_upv': ( + [Sw_onlineyearcol,'r','tech','summer_power_capacity_MW'], + ['t','r','i','value'] + ), + 'prsc_w': ( + [Sw_onlineyearcol,'r','tech','summer_power_capacity_MW'], + ['t','r','i','value'] + ), + 'prsc_csp': ( + [Sw_onlineyearcol,'r','tech','ctt','wst','summer_power_capacity_MW'], + ['t','r','i','ctt','wst','value'] + ), + 'prsc_geo': ( + [Sw_onlineyearcol,'r','tech','summer_power_capacity_MW'], + ['t','r','i','value'] + ), + 'retirements': ( + [retscen,'r','tech','coolingwatertech','ctt','wst','summer_power_capacity_MW'], + ['t','r','i','coolingwatertech','ctt','wst','value'] + ), + 'retirements_energy': ( + [retscen,'r','tech','energy_capacity_MWh'], + ['t','r','i','value'] + ), + 'windret': ( + ['r','tech','RetireYear','summer_power_capacity_MW'], + ['r','i','t','value'] + ), + 'georet': ( + ['r','tech','RetireYear','summer_power_capacity_MW'], + ['r','i','t','value'] + ), + } + + + #%% + print('Importing generator database:') + gdb_use = pd.read_csv(os.path.join(inputs_case,'unitdata.csv'), low_memory=False) + + + rcol_dict = {'county':'FIPS', 'ba':'reeds_ba'} + # Create the 'r_col' column + if agglevel in ['county','ba']: + r_col = rcol_dict[agglevel] + gdb_use['r'] = gdb_use[r_col].copy() + # Filter generator database to regions that match the spatial resolution of the run + gdb_use = gdb_use[gdb_use['r'].isin(regions)] + elif agglevel == 'aggreg': + rb_aggreg = pd.read_csv(os.path.join(inputs_case,'rb_aggreg.csv'), index_col='ba').squeeze(1) + gdb_use = gdb_use.assign(r=gdb_use.reeds_ba.map(rb_aggreg)) + # Filter generator database to regions that match the spatial resolution of the run + gdb_use = gdb_use[gdb_use['r'].isin(regions)] + + # If PVB is turned off, consider all PVB as UPV and battery_li for existing and prescribed builds + # If PVB is turned on, consider all PVB as 'pvb' + if GSw_PVB == 0: + gdb_use['tech'] = gdb_use['tech'].replace('pvb_battery','battery_li') + gdb_use['tech'] = gdb_use['tech'].replace('pvb_pv','upv') + else: + gdb_use['tech'] = gdb_use['tech'].replace('pvb_battery','pvb') + gdb_use['tech'] = gdb_use['tech'].replace('pvb_pv','pvb') + + + # Consider all DUPV as UPV for existing and prescribed builds. + gdb_use['tech'] = gdb_use['tech'].replace('dupv','upv') + + # Change tech category of hydro that will be prescribed to use upgrade tech + # This is a coarse assumption that all recent new hydro is upgrades + # Existing hydro techs (hydED/hydEND) specifically refer to hydro that exists in startyear + # Future work could incorporate this change into unit database creation and possibly + # use data from ORNL HydroSource to assign a more accurate hydro category. + gdb_use.loc[ + (gdb_use['tech']=='hydEND') & (gdb_use[Sw_onlineyearcol] >= startyear), 'tech' + ] = 'hydUND' + gdb_use.loc[ + (gdb_use['tech']=='hydED') & (gdb_use[Sw_onlineyearcol] >= startyear), 'tech' + ] = 'hydUD' + + # We model csp-ns (CSP No Storage) as upv throughout ReEDS, but switch it back for reporting. + # So save the csp-ns capacity separately, then rename it. + csp_units = ( + gdb_use.loc[(gdb_use['tech']=='csp-ns') & (gdb_use['RetireYear'] > startyear)] + .groupby(['r','StartYear','RetireYear']).summer_power_capacity_MW.sum() + .reset_index() + ) + if len(csp_units): + cap_cspns = ( + pd.concat( + {i: pd.Series( + [row.summer_power_capacity_MW]*(row.RetireYear - row.StartYear + 2), + index=range(row.StartYear, row.RetireYear + 2) + ) for (i,row) in csp_units.iterrows()}, + axis=1) + .rename(columns=csp_units['r']).fillna(0) + .groupby(axis=1, level=0).sum() + .stack().replace(0,np.nan).dropna() + .rename_axis(['t','*r']).reorder_levels(['*r','t']).rename('MWac') + ) + cap_cspns = ( + cap_cspns.loc[cap_cspns.index.get_level_values('t') >= startyear].copy()) + else: + cap_cspns = pd.DataFrame(columns=['*r','t','MWac']).set_index(['*r','t']) + # csp-ns capacity is MWac measured at the power block, while PV capacity is MWdc, + # so multiply csp-ns capacity by the ILR [MWdc/MWac] of PV + gdb_use.loc[gdb_use['tech']=='csp-ns','summer_power_capacity_MW'] *= scalars['ilr_utility'] + # Rename csp-ns to upv + gdb_use.loc[gdb_use['tech']=='csp-ns','coolingwatertech'] = ( + gdb_use.loc[gdb_use['tech']=='csp-ns','coolingwatertech'] + .map(lambda x: x.replace('csp-ns','upv')) + ) + gdb_use.loc[gdb_use['tech']=='csp-ns','tech'] = 'upv' + + # If using cooling water, set the coolingwatertech of technologies with no + # cooling to be the same as the tech + if GSw_WaterMain == 1: + gdb_use.loc[gdb_use['tech'].isin(TECH['no_cooling']), + 'coolingwatertech'] = gdb_use.loc[gdb_use['tech'].isin(TECH['no_cooling']), + 'tech'] + + #%%################################## + # -- All Existing Capacity -- # + ##################################### + + ### Used as the starting point for intra-zone network reinforcement costs + # Power capacity in MW + poi_cap_init = gdb_use.loc[(gdb_use[Sw_onlineyearcol] < startyear) & + (gdb_use['RetireYear'] > startyear) + ].groupby('r').summer_power_capacity_MW.sum().rename('MW').round(3) + poi_cap_init.index = poi_cap_init.index.rename('*r') + + #%%###################################### + # -- non-RSC Existing Capacity -- # + ######################################### + + print('Gathering non-RSC Existing Capacity...') + capnonrsc = gdb_use.loc[(gdb_use['tech'].isin(TECH['capnonrsc'])) & + (gdb_use[Sw_onlineyearcol] < startyear) & + (gdb_use['RetireYear'] > startyear) + ] + capnonrsc = capnonrsc[COLNAMES['capnonrsc'][0]] + capnonrsc.columns = COLNAMES['capnonrsc'][1] + capnonrsc = capnonrsc.groupby(COLNAMES['capnonrsc'][1][:-1]).sum().reset_index() + + capnonrsc_energy = gdb_use.loc[(gdb_use['tech'].isin(TECH['capnonrsc_energy'])) & + (gdb_use[Sw_onlineyearcol] < startyear) & + (gdb_use['RetireYear'] > startyear) + ] + capnonrsc_energy = capnonrsc_energy[COLNAMES['capnonrsc_energy'][0]] + capnonrsc_energy.columns = COLNAMES['capnonrsc_energy'][1] + capnonrsc_energy = capnonrsc_energy.groupby(COLNAMES['capnonrsc_energy'][1][:-1]).sum().reset_index() + + + #%%######################################## + # -- non-RSC Prescribed Capacity -- # + ########################################### + + print('Gathering non-RSC Prescribed Capacity...') + ### prescribed power capacity + prescribed_nonRSC = gdb_use.loc[(gdb_use['tech'].isin(TECH['prescribed_nonRSC'])) & + (gdb_use[Sw_onlineyearcol] >= startyear) + ] + prescribed_nonRSC = prescribed_nonRSC[COLNAMES['prescribed_nonRSC'][0]] + prescribed_nonRSC.columns = COLNAMES['prescribed_nonRSC'][1] + # Remove ctt and wst data from storage, set coolingwatertech to tech type ('i') + for j, row in prescribed_nonRSC.iterrows(): + if row['i'] in TECH['storage']: + prescribed_nonRSC.loc[j,['ctt','wst','coolingwatertech']] = ['n','n',row['i']] + + + if int(sw.GSw_NuclearDemo)==1: + # Load in demo data and stack it on prescribed non-RSC + demo = pd.read_csv( + os.path.join(inputs_case,'demonstration_plants.csv')).drop("notes", axis=1) + # Filter demonstration plants to regions in function call + demo = demo[demo['r'].isin(regions)] + prescribed_nonRSC = pd.concat([prescribed_nonRSC,demo],sort=False) + + prescribed_nonRSC = ( + prescribed_nonRSC.groupby(COLNAMES['prescribed_nonRSC'][1][:-1]).sum().reset_index()) + + ### prescribed energy capacity + prescribed_nonRSC_energy = gdb_use.loc[(gdb_use['tech'].isin(TECH['prescribed_nonRSC_energy'])) & + (gdb_use[Sw_onlineyearcol] >= startyear) + ] + prescribed_nonRSC_energy = prescribed_nonRSC_energy[COLNAMES['prescribed_nonRSC_energy'][0]] + prescribed_nonRSC_energy.columns = COLNAMES['prescribed_nonRSC_energy'][1] + # Remove ctt and wst data from storage, set coolingwatertech to tech type ('i') + for j, row in prescribed_nonRSC_energy.iterrows(): + if row['i'] in TECH['storage']: + prescribed_nonRSC_energy.loc[j,['ctt','wst','coolingwatertech']] = ['n','n',row['i']] + + prescribed_nonRSC_energy = ( + prescribed_nonRSC_energy.groupby(COLNAMES['prescribed_nonRSC_energy'][1][:-1]).sum().reset_index()) + + #%%################################## + # -- RSC Existing Capacity -- # + ##################################### + ''' + The following are RSC tech that are treated differently in the model + ''' + print('Gathering RSC Existing Capacity...') + # DUPV and UPV values are collected at the same time here: + caprsc = gdb_use.loc[(gdb_use['tech'].isin(TECH['rsc_all'][:2])) & + (gdb_use[Sw_onlineyearcol] < startyear) & + (gdb_use['RetireYear'] > startyear) + ] + caprsc = caprsc[COLNAMES['rsc'][0]] + caprsc.columns = COLNAMES['rsc'][1] + caprsc = caprsc.groupby(COLNAMES['rsc'][1][:-2]).value.sum().reset_index() + # Multiply all PV capacities by ILR + caprsc['value'] = caprsc['value'] * scalars['ilr_utility'] + + # Add existing CSP builds: + # Note: Since CSP data is affected by GSw_WaterMain, it must be dealt with + # separate from the other RSC tech (UPV, DUPV, wind, etc) + csp = gdb_use.loc[(gdb_use['tech'].isin(TECH['rsc_csp'])) & + (gdb_use[Sw_onlineyearcol] < startyear) & + (gdb_use['RetireYear'] > startyear) + ] + csp = csp[COLNAMES['rsc'][0]] + csp.columns = COLNAMES['rsc'][1] + csp = csp.groupby(COLNAMES['rsc'][1][:-1]).sum().reset_index() + if GSw_WaterMain == 1: + csp['i'] = csp['i'] + '_' + csp['ctt'] + '_' + csp['wst'] + csp.drop('wst', axis=1, inplace=True) + + # Add existing hydro builds: + gendb = gdb_use[["tech", 'r', "summer_power_capacity_MW"]] + gendb = gendb[(gendb.tech == 'hydED') | (gendb.tech == 'hydEND')] + + hyd = gendb.groupby(['tech', 'r']).sum() \ + .reset_index() \ + .rename({"tech":"i","summer_power_capacity_MW":"value"}, axis=1) + + hyd['ctt'] = 'n' + + # Concat all RSC Existing Data to one dataframe: + caprsc = pd.concat([caprsc, csp, hyd]) + + # Export Existing RSC data specifically used in writesupplycurves.py + rsc_wsc = create_rsc_wsc(gdb_use, TECH=TECH, scalars=scalars,startyear=startyear) + + # Create geoexist.csv and copy to inputs_case + geoexist = gdb_use.loc[(gdb_use['tech'].isin(['geohydro_allkm','egs_allkm'])) & + (gdb_use[Sw_onlineyearcol] < startyear) & + (gdb_use['RetireYear'] > startyear) + ] + geoexist = (geoexist[['tech','r','summer_power_capacity_MW']] + .rename(columns={'tech':'*i','summer_power_capacity_MW':'MW'}) + ) + geoexist = geoexist.groupby(['*i','r']).sum().reset_index() + # Rename generic geothermal tech category to geohydro_allkm_1 + geoexist['*i'] = 'geohydro_allkm_1' + + + #%%#################################### + # -- RSC Prescribed Capacity -- # + ####################################### + + print('Gathering RSC Prescribed Capacity...') + # DUPV and UPV values are collected at the same time here: + pupv = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_upv'])) & + (gdb_use[Sw_onlineyearcol] >= startyear) + ] + pupv = pupv[COLNAMES['prsc_upv'][0]] + pupv.columns = COLNAMES['prsc_upv'][1] + pupv = pupv.groupby(['t','r','i']).sum().reset_index() + # Multiply all PV capacities by ILR + pupv['value'] = pupv['value'] * scalars['ilr_utility'] + + # Load in wind builds: + pwind = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_w'])) & + (gdb_use[Sw_onlineyearcol] >= startyear) + ] + pwind = pwind[COLNAMES['prsc_w'][0]] + pwind.columns = COLNAMES['prsc_w'][1] + + pwind = pwind.groupby(['t','r','i']).sum().reset_index() + pwind.sort_values(['t','r'], inplace=True) + + # Add prescribed csp builds: + # Note: Since csp is affected by GSw_WaterMain, it must be dealt with separate + # from the other RSC tech (dupv, upv, wind, etc) + pcsp = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_csp'])) & + (gdb_use[Sw_onlineyearcol] >= startyear) + ] + pcsp = pcsp[COLNAMES['prsc_csp'][0]] + pcsp.columns = COLNAMES['prsc_csp'][1] + if GSw_WaterMain == 1: + pcsp['i'] = np.where(pcsp['i']=='csp-ws',pcsp['i']+'_'+pcsp['ctt']+'_'+pcsp['wst'],'csp-ws') + + # Load in geo builds: + pgeo = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_geo'])) & + (gdb_use[Sw_onlineyearcol] >= startyear) + ] + pgeo = pgeo[COLNAMES['prsc_geo'][0]] + pgeo.columns = COLNAMES['prsc_geo'][1] + + pgeo = pgeo.groupby(['t','r','i']).sum().reset_index() + pgeo.sort_values(['t','r'], inplace=True) + + # Concat all RSC Existing Data to one dataframe: + prescribed_rsc = pd.concat([pupv,pwind,pcsp,pgeo],sort=False) + + #%%---------------------------------------------------------------------------- + ################################ + # -- SMR Existing Capacity -- # + ################################ + print('Gathering SMR Existing Capacity...') + # Grab the first year for smr because that is when new capacity can begin to be built (for + # smr, smr_ccs and electrolyzers) + firstyear = pd.read_csv( + os.path.join(inputs_case,'firstyear.csv'), + ).rename(columns={'*i':'i'}).set_index('i').squeeze(1) + h2_prod_first_year = firstyear['smr'] + # Get exogenous H2 demand + h2_exogenous_demand = ( + pd.read_csv(os.path.join(inputs_case,'h2_exogenous_demand.csv')) + .rename(columns={f'{sw.GSw_H2_Demand_Case}':'million_tons'},) + .drop(['*p'], axis=1).set_index('t').squeeze(1) + ) + ### Get BA share of national H2 demand + h2_ba_share = pd.read_csv( + os.path.join(inputs_case,'h2_ba_share.csv')) + # Filter to regions in function call + h2_ba_share = h2_ba_share[h2_ba_share['*r'].isin(regions)] + h2_ba_share = h2_ba_share.rename(columns={'*r':'r'}).pivot(index='t', columns='r', values='fraction') + ## h2_ba_share is only populated for 2021 and 2050, so need to fill the empty data + h2_ba_share = h2_ba_share.reindex(sorted(set(years+[2021,2050]))) + ## If a region has no data for 2021, it's zero (GAMS convention) + h2_ba_share.loc[2021] = h2_ba_share.loc[2021].fillna(0) + ## Backfill before 2021 + h2_ba_share.loc[:2021] = h2_ba_share.loc[:2021].fillna(method='bfill') + ## Interpolate between 2021-2050 + h2_ba_share.loc[2021:] = h2_ba_share.loc[2021:].interpolate('index') + ## Only keep the modeled years + h2_ba_share = h2_ba_share.loc[years].copy() + ## Reshape from wide to long format + h2_ba_share_out = h2_ba_share.reset_index().melt(id_vars='t', var_name='*r', value_name='fraction')[['*r','t','fraction']] + + # Calculating the consumption characteristics (has columns i, t, parameter, value) + consume_char0 = pd.read_csv( + os.path.join(inputs_case,'consume_char.csv')).rename(columns={'*i':'i'}) + consume_char0['i'] = consume_char0['i'].str.lower() + consume_char0 = consume_char0.set_index(['i','t','parameter']).value + + outage_forced_static = pd.read_csv(os.path.join(inputs_case,'outage_forced_static.csv'), + header=None, index_col=0, + ).squeeze(1) + + smr_init_ele_efficiency = consume_char0['smr',startyear,'ele_efficiency'] + smr_outage_forced = outage_forced_static['smr'] + h2_demand_initial = h2_exogenous_demand[h2_prod_first_year] + + # Now make some calculations to get the existing SMR capacity + # Hydrogen demand per r,t (million metric tons) * (10^9 kg/million metric ton) * (kWh/kg) + # / 8760 to convert kWh --> kW / (10^3 kW/MW) / outage rate + # * to make a tiny adjustment upwards to avoid infeasibilities + h2_existing_smr_cap = ( + h2_ba_share.stack('r').reorder_levels(['r','t']).rename('fraction').reset_index()) + # If this was multiplied by the H2 demand per year, then we would be forcing + # existing SMR to meet exogenous H2 demand forever and we don't want that. + # Only for it to meet 2023 demand + h2_existing_smr_cap['million_tons'] = h2_existing_smr_cap['fraction'] * h2_demand_initial + h2_existing_smr_cap['value'] = ( + h2_existing_smr_cap['million_tons'] * 1e9 * smr_init_ele_efficiency + / 8760 / 1000 / (1 - smr_outage_forced) * 1.0001) + # Make any value after h2_prod_first_year to be the same MW value as h2_prod_first_year + # (aka we will not force model to build more SMR capacity in 2030 once it has already + # met h2 demand in 2024). aka if model year is 2024, then from 2024-2050, the data + # will be the same df with columns t, r, fraction, million metric tons, + # value for 134 different BAs in h2_prod_first_year + # (but only do this if endyear > h2_prod_first_year, otherwise it will introduce NaNs) + if endyear > h2_prod_first_year: + h2_prod_first_year_df = h2_existing_smr_cap[ + h2_existing_smr_cap['t']==h2_prod_first_year + ].drop(['t'], axis=1) + # For any years after h2_prod_first_year + after_h2_prod_first_year_df = h2_existing_smr_cap[ + h2_existing_smr_cap['t'] > h2_prod_first_year + ].drop(['fraction','million_tons','value'], axis=1) + # New df from 2025 --> 2050 + after_h2_prod_first_year_df = pd.merge( + h2_prod_first_year_df, + after_h2_prod_first_year_df, + how='left', on=['r'], + ) + # Concat 2010-2024 df and 2025-->end of model + h2_existing_smr_cap = pd.concat([ + h2_existing_smr_cap[h2_existing_smr_cap['t']<=h2_prod_first_year], + after_h2_prod_first_year_df + ]) + # Filter down to modeled regions and years (otherwise b_inputs will throw an error) + h2_existing_smr_cap = (h2_existing_smr_cap + .rename(columns={'r':'*r'}) + .sort_values(by=['t','*r']) + ) + + + #%%---------------------------------------------------------------------------- + ################################ + # -- Retirements Data -- # + ################################ + print('Gathering Retirement Data...') + rets = gdb_use.loc[(gdb_use['tech'].isin(TECH['retirements'])) & + (gdb_use[retscen]>startyear) + ] + rets = rets[COLNAMES['retirements'][0]] + rets.columns = COLNAMES['retirements'][1] + rets.sort_values(by=COLNAMES['retirements'][1],inplace=True) + rets = rets.groupby(COLNAMES['retirements'][1][:-1]).sum().reset_index() + + rets_energy = gdb_use.loc[(gdb_use['tech'].isin(TECH['retirements_energy'])) & + (gdb_use[retscen]>startyear) + ] + rets_energy = rets_energy[COLNAMES['retirements_energy'][0]] + rets_energy.columns = COLNAMES['retirements_energy'][1] + rets_energy.sort_values(by=COLNAMES['retirements_energy'][1],inplace=True) + rets_energy = rets_energy.groupby(COLNAMES['retirements_energy'][1][:-1]).sum().reset_index() + + ################################ + # -- Wind Retirements -- # + ################################ + print('Gathering Wind Retirement Data...') + wind_rets = gdb_use.loc[(gdb_use['tech'].isin(TECH['windret'])) & + (gdb_use[Sw_onlineyearcol] <= startyear) & + (gdb_use['RetireYear'] > startyear) & + (gdb_use['RetireYear'] < startyear + 30) + ] + wind_rets = wind_rets[COLNAMES['windret'][0]] + wind_rets.columns = COLNAMES['windret'][1] + wind_rets['v'] = 'init-1' + wind_rets = wind_rets.groupby(['i','v','r','t']).sum().reset_index() + + wind_rets = (wind_rets.pivot_table(index = ['i','v','r'], columns = 't', values='value') + .reset_index() + .fillna(0) + ) + #================================ + # --- Geothermal Retirements --- + #================================ + print('Gathering Geothermal Retirement Data...') + geo_retirements = gdb_use.loc[(gdb_use['tech'].isin(TECH['georet'])) & + (gdb_use[Sw_onlineyearcol] <= startyear) & + (gdb_use['RetireYear'] > startyear) & + (gdb_use['RetireYear'] < startyear + 30) + ] + geo_retirements = geo_retirements[COLNAMES['georet'][0]] + geo_retirements.columns = COLNAMES['georet'][1] + geo_retirements['v'] = 'init-1' + geo_retirements = geo_retirements.groupby(['i','v','r','t']).sum().reset_index() + + geo_retirements = (geo_retirements + .pivot_table(index = ['i','v','r'], columns = 't', values='value') + .reset_index() + .fillna(0) + ) + + + #%%---------------------------------------------------------------------------- + ############################################################# + # -- Hydro Capacity Adjustment Factors: CC-Seasaon -- # + ############################################################# + + # Initialize with monthly hydropower capacity adjustment factor values + hydcapadj_ccszn = pd.read_csv(os.path.join(inputs_case,'hydcapadj.csv')) + #Filter to regions in function call + hydcapadj_ccszn = hydcapadj_ccszn[hydcapadj_ccszn['r'].isin(regions)] + # Map hot/cold values to ccseason months and filter for ccseason data + hydcapadj_ccszn['ccseason'] = hydcapadj_ccszn['month'].map(hotcold_months) + hydcapadj_ccszn = (hydcapadj_ccszn[hydcapadj_ccszn['ccseason'].isin(['cold','hot'])] + .drop(columns='month')) + # Average monthly data to get factor values by ccseason + hydcapadj_ccszn = hydcapadj_ccszn.groupby(['*i','r','ccseason']).mean().reset_index() + hydcapadj_ccszn['value'] = hydcapadj_ccszn['value'].round(5) + + + #%%---------------------------------------------------------------------------- + ######################################## + # -- Waterconstraint Indexing -- # + ######################################## + + rets['i'] = rets['i'].str.lower() + rets_energy['i'] = rets_energy['i'].str.lower() + prescribed_nonRSC['i'] = prescribed_nonRSC['i'].str.lower() + prescribed_nonRSC_energy['i'] = prescribed_nonRSC_energy['i'].str.lower() + + # When water constraints are enabled, retirements are also indexed by cooling technology + # and cooling water source. otherwise, they only have the indices of year, region, and tech + if GSw_WaterMain == 1: + ### Group by all cols except 'value' + rets = rets.groupby(COLNAMES['retirements'][1][:-1]).sum().reset_index() + rets.columns = COLNAMES['retirements'][1] + + capnonrsc = capnonrsc.groupby(COLNAMES['capnonrsc'][1][:-1]).sum().reset_index() + capnonrsc.columns = COLNAMES['capnonrsc'][1] + + prescribed_nonRSC = ( + prescribed_nonRSC + .groupby(COLNAMES['prescribed_nonRSC'][1][:-1]).sum().reset_index()) + prescribed_nonRSC.columns = COLNAMES['prescribed_nonRSC'][1] + + prescribed_nonRSC_energy = ( + prescribed_nonRSC_energy + .groupby(COLNAMES['prescribed_nonRSC_energy'][1][:-1]).sum().reset_index()) + + rets['i'] = rets['coolingwatertech'] + rets = rets.groupby(['t','r','i']).value.sum().reset_index() + rets.columns = ['t','r','i','value'] + + capnonrsc['i'] = capnonrsc['coolingwatertech'] + capnonrsc = capnonrsc.groupby(['i','r']).value.sum().reset_index() + capnonrsc.columns = ['i','r','value'] + + prescribed_nonRSC['i'] = prescribed_nonRSC['coolingwatertech'] + prescribed_nonRSC = prescribed_nonRSC.groupby(['t','r','i']).value.sum().reset_index() + prescribed_nonRSC.columns = ['t','r','i','value'] + + prescribed_nonRSC_energy['i'] = prescribed_nonRSC_energy['coolingwatertech'] + prescribed_nonRSC_energy = prescribed_nonRSC_energy.groupby(['t','r','i']).value.sum().reset_index() + prescribed_nonRSC_energy.columns = ['t','r','i','value'] + else: + # Group by [year, region, tech] + rets = rets.groupby(['t','r','i']).value.sum().reset_index() + rets.columns = ['t','r','i','value'] + + capnonrsc = capnonrsc.groupby(['i','r']).value.sum().reset_index() + capnonrsc.columns = ['i','r','value'] + + prescribed_nonRSC = prescribed_nonRSC.groupby(['t','r','i']).value.sum().reset_index() + prescribed_nonRSC.columns = ['t','r','i','value'] + + prescribed_nonRSC_energy = prescribed_nonRSC_energy.groupby(['t','r','i']).value.sum().reset_index() + prescribed_nonRSC_energy.columns = ['t','r','i','value'] + + # Final Groupby step for capacity groupings not affected by GSw_WaterMain: + caprsc = caprsc.groupby(['i','r']).value.sum().reset_index() + prescribed_rsc = prescribed_rsc.groupby(['t','i','r']).value.sum().reset_index() + + + #%%---------------------------------------------------------------------------- + ################################ + # -- Canadian Imports -- # + ################################ + + can_imports_year_mwh = pd.read_csv(os.path.join(inputs_case,'can_imports.csv'), + index_col='r').dropna() + # Filter to regions in function call + can_imports_year_mwh = can_imports_year_mwh[can_imports_year_mwh.index.isin(regions)] + can_imports_year_mwh.columns = can_imports_year_mwh.columns.astype(int) + can_imports_year_mwh = can_imports_year_mwh.reindex(years, axis=1).dropna(axis=1) + + ## Get hours per quarter + year = sw['GSw_HourlyWeatherYears'].split('_')[0] + timestamps = pd.Series(index=pd.date_range(f'{year}-01-01', periods=8760, freq='H')) + + month2quarter = pd.read_csv( + os.path.join(inputs_case, 'month2quarter.csv'), + index_col='month', + ).squeeze(1) + + quarterhours = timestamps.index.month.map(month2quarter).value_counts() + quarterhours.index = quarterhours.index.map(lambda x: quartershorten.get(x,x)).rename('szn') + + can_imports_quarter_frac = pd.read_csv(os.path.join(inputs_case,'can_imports_quarter_frac.csv'), + header=0, names=['szn','frac'], index_col='szn' + ).squeeze(1) + can_imports_capacity = ( + ## Start with annual imports in MWh + pd.concat({szn: can_imports_year_mwh for szn in quartershorten.values()}, axis=0, names=['szn','r']) + ## Multiply by season frac to get MWh per season + .multiply(can_imports_quarter_frac, axis=0, level='szn') + ## Divide by hours per season to get average MW by season + .divide(quarterhours, axis=0, level='szn') + ## Keep the max value across seasons + .groupby('r', axis=0).max() + ## Reshape for GAMS + .stack().rename_axis(['*r','t']).rename('MW').round(3) + ) + + + #%%---------------------------------------------------------------------------- + ############################## + # -- Data Write-Out -- # + ############################## + + #Round outputs before writing out + for df in [rets, rets_energy, capnonrsc, capnonrsc_energy, prescribed_nonRSC, prescribed_nonRSC_energy, + caprsc, prescribed_rsc, h2_existing_smr_cap]: + df['value'] = df['value'].round(6) + # Set all years to integer datatype + if 't' in df.columns: + df['t'] = df.t.astype(float).round().astype(int) + + #%% + # Return + files_out = {'capnonrsc' : capnonrsc[['i','r','value']], + 'capnonrsc_energy' : capnonrsc_energy[['i','r','value']], + 'rets' : rets[['t','r','i','value']], + 'rets_energy' : rets_energy[['t','r','i','value']], + 'prescribed_nonRSC' : prescribed_nonRSC[['t','i','r','value']], + 'prescribed_nonRSC_energy' : prescribed_nonRSC_energy[['t','i','r','value']], + 'caprsc' :caprsc[['i','r','value']], + 'prescribed_rsc' : prescribed_rsc[['t','i','r','value']], + 'wind_rets' : wind_rets, + 'h2_existing_smr_cap' : h2_existing_smr_cap[['*r','t','value']], + 'geo_retirements' : geo_retirements, + 'poi_cap_init' : poi_cap_init, + 'cap_cspns': cap_cspns, + 'rsc_wsc':rsc_wsc, + 'hydcapadj_ccszn' : hydcapadj_ccszn[['*i','ccseason','r','value']], + 'can_imports_capacity' : can_imports_capacity, + 'geoexist' : geoexist, + 'h2_ba_share': h2_ba_share_out + } + + return files_out + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if __name__ == '__main__': + ### Time the operation of this script + tic = datetime.datetime.now() + + ### Parse arguments + parser = argparse.ArgumentParser(description="""This file processes plant cost data by tech""") + parser.add_argument("reeds_path", help="ReEDS directory") + parser.add_argument("inputs_case", help="path to runs/{case}/inputs_case") + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + print('Starting writecapdat.py') + + + # Use agglevel_variables function to obtain spatial resolution variables + agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) + + # For mixed resolution runs the main function of writecapdat needs to be executed separately for each desired resolution + # Then the data from each resolution are combined and written to the inputs_case folder + if agglevel_variables['lvl'] == 'mult': + for resolution in agglevel_variables['agglevel']: + if resolution == 'aggreg': + aggreg_data = main(reeds_path, inputs_case, agglevel=resolution, + regions=agglevel_variables['ba_regions'] ) + if resolution == 'ba': + ba_data = main(reeds_path, inputs_case, agglevel=resolution, + regions=agglevel_variables['ba_regions']) + if resolution == 'county': + county_data = main(reeds_path, inputs_case, agglevel=resolution, + regions=agglevel_variables['county_regions'],) + + # Combine and write mixed resolution data + # ReEDS only supports county-BA, county-aggreg combinations + combined_data = {} + if 'ba' in agglevel_variables['agglevel']: + for key in ba_data.keys() : + if county_data[key].empty: + combined_data[key] = ba_data[key] + elif ba_data[key].empty: + combined_data[key] = county_data[key] + else: + combined_data[key] = pd.concat([ba_data[key], county_data[key]]) + + if 'aggreg' in agglevel_variables['agglevel']: + for key in aggreg_data.keys() : + if county_data[key].empty: + combined_data[key] = aggreg_data[key] + elif aggreg_data[key].empty: + combined_data[key] = county_data[key] + else: + combined_data[key] = pd.concat([aggreg_data[key], county_data[key]]) + + data = combined_data + + # Single Resolution Procedure + else: + agglevel = agglevel_variables['agglevel'] + regions = pd.read_csv(os.path.join(inputs_case,f'val_{agglevel}.csv'),header=None).squeeze(1).values + data = main(reeds_path, inputs_case,agglevel, regions) + + # Write it + print('Writing out capacity data') + outname = { + 'rets': 'retirements', + 'rets_energy': 'retirements_energy', + 'wind_rets': 'wind_retirements', + 'hydcapadj_ccszn': 'cap_hyd_ccseason_adj', + } + keep_index = { + 'poi_cap_init': True, + 'cap_cspns': True, + 'can_imports_capacity': True, + } + for key, df in data.items(): + df.to_csv( + os.path.join(inputs_case, f'{outname.get(key, key)}.csv'), + index=keep_index.get(key, False), + ) + + reeds.log.toc(tic=tic, year=0, process='inputs/writecapdat.py', + path=os.path.join(inputs_case,'..')) + + print('Finished writecapdat.py') diff --git a/reeds/input_processing/writedrshift.py b/reeds/input_processing/writedrshift.py new file mode 100644 index 00000000..92d7ab32 --- /dev/null +++ b/reeds/input_processing/writedrshift.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +""" +Code to process allowed shifting hours for demand response into +fraction of hours that can be shifted into each time slice +At some point, it may be nice to instead read in the actual DR +shifting potential and change to fraction of load that can be shifted +but haven't done that yet. + +Created on Feb 24 2021 +@author: bstoll +""" + +# %% =========================================================================== +### --- IMPORTS --- +### =========================================================================== +import os +import sys +import argparse +import pandas as pd +import datetime +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +# Time the operation of this script +tic = datetime.datetime.now() + +# %%################# +### FIXED INPUTS ### + +decimals = 4 + + +#%% =========================================================================== +### --- MAIN FUNCTION --- +### =========================================================================== + +if __name__ == "__main__": + ### Parse arguments + parser = argparse.ArgumentParser( + description="This file produces the DR shiftability inputs" + ) + parser.add_argument("reeds_path", help="ReEDS directory") + parser.add_argument("inputs_case", help="output directory") + + args = parser.parse_args() + inputs_case = args.inputs_case + reeds_path = args.reeds_path + + # Settings for testing + # reeds_path = os.getcwd() + # inputs_case = os.path.join(reeds_path,'runs','dr1_Pacific','inputs_case') + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + print('Starting writedrshift.py') + + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + + val_r = ( + pd.read_csv(os.path.join(inputs_case, "val_r.csv"), header=None) + .squeeze(1) + .tolist() + ) + + ### Create empty EVMC data files if GSw_EVMC == 0: + evmc_files = [ + "evmc_shape_profile_decrease", + "evmc_shape_profile_increase", + "evmc_storage_profile_decrease", + "evmc_storage_profile_increase", + "evmc_storage_energy", + ] + for file in evmc_files: + if int(sw["GSw_EVMC"]): + pass + else: + # Overwrite empty dataframes created in copy_files.py + df = pd.DataFrame(columns=["i", "hour", "year"] + val_r) + df.to_csv(os.path.join(inputs_case, file + ".csv"), index=False) + + reeds.log.toc(tic=tic, year=0, process='inputs/writedrshift.py', + path=os.path.join(inputs_case,'..')) + print('Finished writedrshift.py') diff --git a/reeds/input_processing/writesupplycurves.py b/reeds/input_processing/writesupplycurves.py new file mode 100644 index 00000000..11af4f6d --- /dev/null +++ b/reeds/input_processing/writesupplycurves.py @@ -0,0 +1,1139 @@ +""" +This script gathers supply curve data for on/offshore wind, upv, csp, hydro, and +psh into a single inputs_case file, rsc_combined.csv. + +This script contains additional procedures for gathering geothermal supply curve data +and demand response supply curve data, and spurline supply curve data +into various separate inputs_case files. +""" + +# %% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import argparse +import numpy as np +import os +import sys +import h5py +import datetime +import pandas as pd +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +reeds_path = reeds.io.reeds_path + +# %%################# +### FIXED INPUTS ### + +### Number of bins used for everything other than wind and PV +numbins_other = 5 +### Rounding precision +decimals = 7 +### spur_cutoff [$/MW]: Cutoff for spur line costs; clip cost for sites with larger costs +spur_cutoff = 1e7 + +# %% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== +def wm(df): + """Make a function to take the capacity-weighted average in a .groupby() call""" + def _wm(x): + weights = df.loc[x.index, 'capacity'] + if (weights < 0).any(): + raise ValueError( + "Negative capacity encountered during supply curve aggregation. " + "Check input supply curve data for invalid capacity values." + ) + # Return 0 if the group has zero total capacity. + if weights.sum() == 0: + return 0 + return np.average(x, weights=weights) + return _wm + + +def get_exog_cap(inputs_case, tech, dfsc): + """Get exogenous capacity by class, region, rscbin, and year""" + dfexog = ( + pd.read_csv(os.path.join(inputs_case, f'exog_cap_{tech}.csv')) + .merge( + dfsc.explode('sc_point_gid').reset_index()[['sc_point_gid','bin']], + on='sc_point_gid', + ) + .rename(columns={'capacity':'MW'}) + ) + dfexog['rscbin'] = dfexog['bin'].map('bin{}'.format) + dfexog = dfexog.groupby(['*tech', 'region', 'rscbin', 'year']).MW.sum() + return dfexog + + +def agg_supplycurve( + scpath, + inputs_case, + numbins_tech, + agglevel, + AggregateRegions, + bin_method='equal_cap_cut', + bin_col='supply_curve_cost_per_mw', + spur_cutoff=1e7, + agglevel_variables=None, + deflate=None, + sw=None, + write=False, +): + """ + """ + ### Get inputs + dfin = reeds.io.assemble_supplycurve( + scfile=scpath, + case=os.path.dirname(os.path.normpath(inputs_case)), + agg=AggregateRegions, + ## TEMPORARY 20260402 + **({'GSw_ZoneSet': 'z134'} if not AggregateRegions else {}), + ).reset_index().drop(columns=['FIPS','cf'], errors='ignore') + ## Convert dollar year and recalculate total cost + transcost_cols = [c for c in dfin if 'cost' in c] + dfin.loc[:, transcost_cols] *= deflate['interconnection'] + deflate_scen = os.path.splitext(os.path.basename(scpath))[0] + dfin['capital_adder_per_mw'] *= deflate[deflate_scen] + dfin['supply_curve_cost_per_mw'] = dfin[ + ['capital_adder_per_mw', 'cost_total_trans_usd_per_mw'] + ].sum(axis=1) + ### Define the aggregation settings + ## Cost and distance are weighted averages, with capacity as the weighting factor + aggs = {'capacity': 'sum', 'sc_point_gid': list} + index_cols = ['region', 'class', 'bin'] + aggs = { + col: aggs.get(col, wm(dfin)) for col in dfin + if col not in index_cols + } + + ### Assign bins + if dfin.empty: + dfin['bin'] = [] + else: + dfin = ( + dfin + .groupby(['region','class'], sort=False, group_keys=True) + .apply(reeds.parse.get_bin, numbins_tech, bin_method, bin_col) + .reset_index(drop=True) + .sort_values('sc_point_gid') + ) + ### Aggregate it + dfout = dfin.groupby(index_cols).agg(aggs) + ### Clip negative costs and costs above cutoff + dfout.supply_curve_cost_per_mw = dfout.supply_curve_cost_per_mw.clip(lower=0, upper=spur_cutoff) + + return dfin, dfout + + +# %% ============================================================================ +### --- MAIN FUNCTION --- +### ============================================================================ + + +def main( + reeds_path, inputs_case, AggregateRegions=1, rsc_wsc_dat=None, write=True, **kwargs +): + # #%% Settings for testing + # reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + # inputs_case = os.path.join(reeds_path,'runs','v20251209_scM0_Pacific','inputs_case') + # AggregateRegions = 1 + # rsc_wsc_dat = None + # write = True + # kwargs = {} + + #%% Inputs from switches + sw = reeds.io.get_switches(inputs_case) + ### Overwrite switches with keyword arguments + for kw, arg in kwargs.items(): + sw[kw] = arg + endyear = int(sw.endyear) + startyear = int(sw.startyear) + geohydrosupplycurve = sw.geohydrosupplycurve + egssupplycurve = sw.egssupplycurve + egsnearfieldsupplycurve = sw.egsnearfieldsupplycurve + pshsupplycurve = sw.pshsupplycurve + numbins = { + "upv": int(sw.numbins_upv), + "wind-ons": int(sw.numbins_windons), + "wind-ofs": int(sw.numbins_windofs), + "csp": int(sw.numbins_csp), + "geohydro": int(sw.numbins_geohydro_allkm), + "egs": int(sw.numbins_egs_allkm), + } + + # Use agglevel_variables function to obtain spatial resolution variables + agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) + agglevel = agglevel_variables['agglevel'] + + val_r_all = pd.read_csv( + os.path.join(inputs_case,'val_r_all.csv'), header=None).squeeze(1).tolist() + # Read in tech-subset-table.csv to determine number of csp configurations + tech_subset_table = pd.read_csv(os.path.join(inputs_case, "tech-subset-table.csv")) + csp_configs = tech_subset_table.loc[ + (tech_subset_table.CSP == "YES") & (tech_subset_table.STORAGE == "YES") + ].shape[0] + + # Read in dollar year conversions for RSC data + dollaryear = pd.read_csv( + os.path.join(inputs_case, 'dollaryear_sc.csv'), index_col='Scenario', + ).squeeze(1) + ## Interconnection cost dollar year is stored as metadata + fpath_interconnection = os.path.join( + reeds_path, 'inputs', 'supply_curve', 'interconnection_land.h5' + ) + with h5py.File(fpath_interconnection, 'r') as f: + dollaryear['interconnection'] = f['data'].attrs['dollaryear'] + deflator = pd.read_csv( + os.path.join(inputs_case, 'deflator.csv'), index_col='*Dollar.Year', + ).squeeze(1) + deflate = dollaryear.map(deflator).rename('Deflator') + + #%% Load the existing RSC capacity (PV plants, wind, and CSP) if not provided in main function call + if rsc_wsc_dat is None: + # writesupplycurves.py is being run as a main input processing script + rsc_wsc = pd.read_csv(os.path.join(inputs_case, "rsc_wsc.csv")) + else: + # writesupplycurves.py is being passed rsc_wsc data from an aggregate_regions.py call + rsc_wsc = rsc_wsc_dat.copy() + + # Group CSP tech + rsc_wsc.loc[rsc_wsc['i']=='csp-ws', 'i'] = 'csp' + rsc_wsc = rsc_wsc.groupby(["r", "i"]).sum().reset_index() + rsc_wsc.i = rsc_wsc.i.str.lower() + + if len(rsc_wsc.columns) < 3: + rsc_wsc["value"] = "" + rsc_wsc.columns = ["r", "tech", "exist"] + + else: + rsc_wsc.columns = ["r", "tech", "exist"] + + ### Change the units + rsc_wsc.exist /= 1000 + tout = rsc_wsc.copy() + + # %% Load supply curve files --------------------------------------------------------- + + alloutcap_list = [] + alloutcost_list = [] + spurout_list = [] + + # %%################# + # -- Wind -- # + #################### + + windin, wind = {}, {} + cost_components_wind_list = [] + wind_types = ["ons"] + if int(sw["GSw_OfsWind"]): + wind_types.append("ofs") + + for s in wind_types: + windin[s], wind[s] = agg_supplycurve( + scpath=os.path.join(inputs_case,f'supplycurve_wind-{s}.csv'), + inputs_case=inputs_case, + agglevel=agglevel, AggregateRegions=AggregateRegions, + numbins_tech=numbins[f'wind-{s}'], spur_cutoff=spur_cutoff, + agglevel_variables=agglevel_variables, deflate=deflate, + sw=sw, write=write + ) + + cost_components = ( + wind[s][["cost_total_trans_usd_per_mw", "capital_adder_per_mw"]] + .round(2) + .reset_index() + .rename( + columns={ + "region": "r", + "class": "*i", + "bin": "rscbin", + "cost_total_trans_usd_per_mw": "cost_trans", + "capital_adder_per_mw": "cost_cap", + } + ) + ) + + cost_components["*i"] = f"wind-{s}_" + cost_components[ + "*i" + ].astype(str) + cost_components["rscbin"] = "bin" + cost_components[ + "rscbin" + ].astype(str) + cost_components = pd.melt( + cost_components, + id_vars=["*i", "r", "rscbin"], + var_name="sc_cat", + value_name="value", + ) + cost_components_wind_list.append(cost_components) + + spurout_list.append( + wind[s] + .reset_index() + .assign(i=f"wind-{s}_" + wind[s].reset_index()["class"].astype(str)) + .assign(rscbin="bin" + wind[s].reset_index()["bin"].astype(str)) + .rename(columns={"region": "r"}) + ) + + cost_components_wind = pd.concat(cost_components_wind_list) + windall = ( + pd.concat(wind, axis=0) + .reset_index(level=0) + .rename(columns={"level_0": "tech"}) + .reset_index() + ) + ### Normalize formatting + windall["tech"] = "wind-" + windall["tech"] + windall.supply_curve_cost_per_mw = windall.supply_curve_cost_per_mw.round(2) + windall["class"] = "class" + windall["class"].astype(str) + windall["bin"] = "wsc" + windall["bin"].astype(str) + ### Pivot, with bins in long format + bins_wind = list(range(1, max(numbins["wind-ons"], numbins["wind-ofs"]) + 1)) + windcost = ( + windall.pivot( + index=["region", "class", "tech"], + columns="bin", + values="supply_curve_cost_per_mw", + ) + .fillna(0) + .reset_index() + ) + windcost.rename( + columns={"wsc{}".format(i): "bin{}".format(i) for i in bins_wind}, + inplace=True, + ) + alloutcost_list.append(windcost) + + windcap = ( + windall.pivot( + index=["region", "class", "tech"], columns="bin", values="capacity" + ) + .fillna(0) + .reset_index() + ) + windcap.rename( + columns={"wsc{}".format(i): "bin{}".format(i) for i in bins_wind}, + inplace=True, + ) + alloutcap_list.append(windcap) + + if write: + ## Exogenous wind capacity + dfwindexog = get_exog_cap(inputs_case, tech='wind-ons', dfsc=wind['ons']) + dfwindexog.round(3).to_csv(os.path.join(inputs_case, "exog_wind_ons_rsc.csv")) + + # %%############### + # -- PV -- # + ################## + + upvin, upv = agg_supplycurve( + scpath=os.path.join(inputs_case, 'supplycurve_upv.csv'), + inputs_case=inputs_case, + agglevel=agglevel, AggregateRegions=AggregateRegions, + numbins_tech=numbins['upv'], spur_cutoff=spur_cutoff, + agglevel_variables=agglevel_variables, deflate=deflate, + sw=sw, write=write + ) + + # Similar to wind, save the trans vs cap components and then concatenate them below just + # before outputting rsc_combined.csv + cost_components_upv = ( + upv[["cost_total_trans_usd_per_mw", "capital_adder_per_mw"]].round(2).reset_index() + ) + cost_components_upv = cost_components_upv.rename( + columns={ + "region": "r", + "class": "*i", + "bin": "rscbin", + "cost_total_trans_usd_per_mw": "cost_trans", + "capital_adder_per_mw": "cost_cap", + } + ) + cost_components_upv["*i"] = "upv_" + cost_components_upv["*i"].astype(str) + cost_components_upv["rscbin"] = "bin" + cost_components_upv["rscbin"].astype(str) + cost_components_upv = pd.melt( + cost_components_upv, + id_vars=["*i", "r", "rscbin"], + var_name="sc_cat", + value_name="value", + ) + + if write: + ## Exogenous UPV capacity + dfupvexog = get_exog_cap(inputs_case, tech='upv', dfsc=upv) + dfupvexog.round(3).to_csv(os.path.join(inputs_case, "exog_upv_rsc.csv")) + + ### Normalize formatting + upv = upv.reset_index() + upv["class"] = "class" + upv["class"].astype(str) + upv["bin"] = "upvsc" + upv["bin"].astype(str) + + spurout_list.append( + upv.assign(i="upv_" + upv["class"].astype(str).str.strip("class")) + .assign(rscbin="bin" + upv["bin"].str.strip("upvsc")) + .rename(columns={"region": "r"}) + ) + + ### Pivot, with bins in long format + bins_upv = list(range(1, numbins["upv"] + 1)) + upvcost = ( + upv.pivot( + columns="bin", values="supply_curve_cost_per_mw", index=["region", "class"] + ).fillna(0) + ### reV spur line and reinforcement costs are now in per MW-AC terms, so removing the + ### correction term that was applied. + .assign(tech="upv") + ).reset_index() + upvcost.rename( + columns={"upvsc{}".format(i): "bin{}".format(i) for i in bins_upv}, + inplace=True, + ) + alloutcost_list.append(upvcost) + + upvcap = ( + upv.pivot(columns="bin", values="capacity", index=["region", "class"]) + .fillna(0) + .reset_index() + .assign(tech="upv") + ) + upvcap.rename( + columns={"upvsc{}".format(i): "bin{}".format(i) for i in bins_upv}, + inplace=True, + ) + alloutcap_list.append(upvcap) + + # %%################ + # -- CSP -- # + ################### + + if int(sw["GSw_CSP"]): + cspin, csp = agg_supplycurve( + scpath=os.path.join(inputs_case, 'supplycurve_csp.csv'), + inputs_case=inputs_case, + agglevel=agglevel, AggregateRegions=AggregateRegions, + numbins_tech=numbins['csp'], spur_cutoff=spur_cutoff, + agglevel_variables=agglevel_variables, deflate=deflate, + sw=sw, write=False + ) + + ### Normalize formatting + csp = csp.reset_index() + csp["class"] = "class" + csp["class"].astype(str) + csp["bin"] = "cspsc" + csp["bin"].astype(str) + + spurout_list.append( + csp.assign(i="csp_" + csp["class"].astype(str).str.strip("class")) + .assign(rscbin="bin" + csp["bin"].str.strip("cspsc")) + .rename(columns={"region": "r"}) + ) + + ### Pivot, with bins in long format + cspcost = ( + csp.pivot( + columns="bin", values="supply_curve_cost_per_mw", index=["region", "class"] + ).fillna(0) + ).reset_index() + cspcap = ( + csp.pivot(columns="bin", values="capacity", index=["region", "class"]) + .fillna(0) + .reset_index() + ) + + ## Duplicate the CSP supply curve for each CSP configuration + bins_csp = list(range(1, numbins["csp"] + 1)) + cspcap = ( + pd.concat( + {"csp{}".format(i): cspcap for i in range(1, csp_configs + 1)}, axis=0 + ) + .reset_index(level=0) + .rename(columns={"level_0": "tech"}) + .reset_index(drop=True) + ) + cspcap.rename( + columns={"cspsc{}".format(i): "bin{}".format(i) for i in bins_csp}, + inplace=True, + ) + alloutcap_list.append(cspcap) + + cspcost = ( + pd.concat( + {"csp{}".format(i): cspcost for i in range(1, csp_configs + 1)}, axis=0 + ) + .reset_index(level=0) + .rename(columns={"level_0": "tech"}) + .reset_index(drop=True) + ) + cspcost.rename( + columns={"cspsc{}".format(i): "bin{}".format(i) for i in bins_csp}, + inplace=True, + ) + alloutcost_list.append(cspcost) + + # %% Geothermal + if int(sw["GSw_Geothermal"]): + use_geohydro_rev_sc = (geohydrosupplycurve == "reV") + use_egs_rev_sc = (egssupplycurve == "reV") + else: + use_geohydro_rev_sc = False + use_egs_rev_sc = False + + ## reV supply curves + if use_geohydro_rev_sc or use_egs_rev_sc: + geoin, geo = {}, {} + rev_geo_types = [] + if use_geohydro_rev_sc: + rev_geo_types.append("geohydro") + if use_egs_rev_sc: + rev_geo_types.append("egs") + for s in rev_geo_types: + geoin[s], geo[s] = agg_supplycurve( + scpath=os.path.join( + inputs_case, + f'supplycurve_{s}.csv'), + numbins_tech=numbins[s], inputs_case=inputs_case, + agglevel=agglevel, AggregateRegions=AggregateRegions, + spur_cutoff=spur_cutoff,agglevel_variables=agglevel_variables, deflate=deflate, + sw=sw, write=False + ) + spurout_list.append( + geo[s] + .reset_index() + .assign(i=f"{s}_allkm_" + geo[s].reset_index()["class"].astype(str)) + .assign(rscbin="bin" + geo[s].reset_index()["bin"].astype(str)) + .rename(columns={"region": "r"}) + ) + + geoall = ( + pd.concat(geo, axis=0) + .reset_index(level=0) + .rename(columns={"level_0": "type"}) + .reset_index() + ) + geoall["type"] = geoall["type"] + "_allkm" + geoall.supply_curve_cost_per_mw = geoall.supply_curve_cost_per_mw.round(2) + geoall["class"] = "class" + geoall["class"].astype(str) + geoall["bin"] = "geosc" + geoall["bin"].astype(str) + ### Pivot, with bins in long format + geocost = ( + geoall.pivot( + index=["region", "class", "type"], + columns="bin", + values="supply_curve_cost_per_mw", + ) + .fillna(0) + .reset_index() + ) + geocap = ( + geoall.pivot( + index=["region", "class", "type"], columns="bin", values="capacity" + ) + .fillna(0) + .reset_index() + ) + + ### Geothermal bins (flexible) + bins_geo = (range(1, max(numbins['geohydro']*use_geohydro_rev_sc, numbins['egs']*use_egs_rev_sc) + 1)) + geocap.rename( + columns={ + **{ + "type": "tech", + "Unnamed: 0": "region", + "Unnamed: 1": "class", + "Unnamed 2": "tech", + }, + **{"geosc{}".format(i): "bin{}".format(i) for i in bins_geo}, + }, + inplace=True, + ) + alloutcap_list.append(geocap) + + geocost.rename( + columns={ + **{ + "type": "tech", + "Unnamed: 0": "region", + "Unnamed: 1": "class", + "Unnamed 2": "tech", + }, + **{"geosc{}".format(i): "bin{}".format(i) for i in bins_geo}, + }, + inplace=True, + ) + alloutcost_list.append(geocost) + + if write: + ## Geothermal discovery rates + geo_disc_rate = pd.read_csv(os.path.join(inputs_case, "geo_discovery_rate.csv")) + geo_disc_rate.round(decimals).to_csv( + os.path.join(inputs_case, "geo_discovery_rate.csv"), index=False + ) + geo_discovery_factor = pd.read_csv( + os.path.join(inputs_case, "geo_discovery_factor.csv") + ) + geo_discovery_factor = geo_discovery_factor.loc[ + geo_discovery_factor.r.isin(val_r_all)].copy() + geo_discovery_factor.round(decimals).to_csv( + os.path.join(inputs_case, "geo_discovery_factor.csv"), index=False + ) + + if use_geohydro_rev_sc: + ## Exogenous geohydro capacity + dfgeohydroexog = get_exog_cap(inputs_case, tech='geohydro', dfsc=geo['geohydro']) + dfgeohydroexog.round(3).to_csv( + os.path.join(inputs_case, "exog_geohydro_allkm_rsc.csv") + ) + + # %% Get supply-curve data for postprocessing + spurcols = [ + 'i', + 'r', + 'rscbin', + 'capacity', + 'dist_spur_km', + 'dist_reinforcement_km', + 'supply_curve_cost_per_mw', + ] + spurout = pd.concat(spurout_list)[spurcols].round(2) + if write: + ## Spurline and reinforcement distances and costs + spurout.to_csv(os.path.join(inputs_case, "spur_parameters.csv"), index=False) + + ### Get spur-line and reinforcement distances if using in annual trans investment limit + poi_distance = spurout.copy() + ## Duplicate CSP entries for each CSP system design + poi_distance_csp = poi_distance.loc[poi_distance.i.str.startswith("csp")].copy() + poi_distance_csp_broadcasted = pd.concat( + [ + poi_distance_csp.assign( + i=poi_distance_csp.i.str.replace("csp_", f"csp{i}_") + ) + for i in range(1, csp_configs + 1) + ], + axis=0, + ) + poi_distance_out = ( + pd.concat( + [ + poi_distance.loc[~poi_distance.i.str.startswith("csp")], + poi_distance_csp_broadcasted, + ], + axis=0, + ) + ## Reformat to save for GAMS + .rename(columns={"i": "*i"}) + .set_index(["*i", "r", "rscbin"]) + ) + ## Convert to miles + distance_spur = (poi_distance_out.dist_spur_km.rename("miles") / 1.609).round(3) + if write: + distance_spur.to_csv(os.path.join(inputs_case, "distance_spur.csv")) + + distance_reinforcement = ( + poi_distance_out.dist_reinforcement_km.rename("miles") / 1.609 + ).round(3) + if write: + distance_reinforcement.to_csv( + os.path.join(inputs_case, "distance_reinforcement.csv") + ) + + # %%################################### + # -- Supply Curve Data -- # + ###################################### + # %% Combine the supply curves + alloutcap = ( + pd.concat(alloutcap_list) + .rename(columns={"region": "r"}) + .assign(var="cap") + ) + alloutcap["class"] = alloutcap["class"].map(lambda x: x.lstrip("cspclass")) + alloutcap["class"] = ( + "class" + alloutcap["class"].map(lambda x: x.lstrip("class")).astype(str) + ) + + t1 = alloutcap.pivot( + index=["r", "tech", "var"], + columns="class", + values=[c for c in alloutcap.columns if c.startswith("bin")], + ).reset_index() + ### Concat the multi-level column names to a single level + t1.columns = ["_".join(i).strip("_") for i in t1.columns.tolist()] + + t2 = t1.merge(tout, on=["r", "tech"], how="outer").fillna(0) + + ### Subset to single-tech curves + wndonst2 = t2.loc[t2.tech == "wind-ons"].copy() + wndofst2 = t2.loc[t2.tech == "wind-ofs"].copy() + cspt2 = t2.loc[t2.tech.isin(["csp{}".format(i) for i in range(1, csp_configs + 1)])] + upvt2 = t2.loc[t2.tech == "upv"].copy() + geohydrot2 = t2.loc[t2.tech == "geohydro_allkm"].copy() + egst2 = t2.loc[t2.tech == "egs_allkm"].copy() + + ### Get the combined outputs + outcap = pd.concat([wndonst2, wndofst2, upvt2, cspt2, geohydrot2, egst2]) + + moutcap = pd.melt(outcap, id_vars=["r", "tech", "var"]) + moutcap = moutcap.loc[~moutcap.variable.isin(["exist", "temp"])].copy() + + moutcap["bin"] = moutcap.variable.map(lambda x: x.split("_")[0]) + moutcap["class"] = moutcap.variable.map(lambda x: x.split("_")[1].lstrip("class")) + outcols = ["r", "tech", "var", "bin", "class", "value"] + moutcap = moutcap.loc[moutcap.value != 0, outcols].copy() + + outcapfin = ( + moutcap.pivot( + index=["r", "tech", "var", "class"], columns="bin", values="value" + ) + .fillna(0) + .reset_index() + ) + + alloutcost = ( + pd.concat(alloutcost_list) + .rename(columns={"region": "r"}) + .set_index(["r", "class", "tech"]) + .reset_index() + .assign(var="cost") + ) + alloutcost["class"] = alloutcost["class"].map(lambda x: x.lstrip("cspclass")) + alloutcost["class"] = alloutcost["class"].map(lambda x: x.lstrip("class")) + + allout = pd.concat([outcapfin, alloutcost]) + allout["tech"] = allout["tech"] + "_" + allout["class"].astype(str) + alloutm = pd.melt(allout, id_vars=["r", "tech", "var"]) + alloutm.rename(columns={"bin":"variable"}, inplace=True) + alloutm = alloutm.loc[alloutm.variable != "class"].copy() + allout_list = [alloutm] + + # %%---------------------------------------------------------------------------------- + ########################## + # -- Hydropower -- # + ########################## + """ + Adding hydro costs and capacity separate as it does not + require the calculations to reduce capacity by existing amounts. + + Goal here is to acquire a data frame that matches the format + of alloutm so that we can simply stack the two. + """ + hydcap = pd.read_csv(os.path.join(inputs_case, "hydcap.csv")) + hydcost = pd.read_csv(os.path.join(inputs_case, "hydcost.csv")) + + hydcap = ( + pd.melt(hydcap, id_vars=["tech", "class"]) + .set_index(["tech", "class", "variable"]) + .sort_index() + ) + hydcap = hydcap.reset_index() + hydcost = pd.melt(hydcost, id_vars=["tech", "class"]) + + # Convert dollar year + hydcost[hydcost.select_dtypes(include=["number"]).columns] *= deflate["hydcost"] + + hydcap["var"] = "cap" + hydcost["var"] = "cost" + + hyddat = pd.concat([hydcap, hydcost]) + hyddat["bin"] = hyddat["class"].map(lambda x: x.replace("hydclass", "bin")) + hyddat["class"] = hyddat["class"].map(lambda x: x.replace("hydclass", "")) + + hyddat.rename(columns={"variable": "r", "bin": "variable"}, inplace=True) + hyddat = hyddat[["tech", "r", "value", "var", "variable"]].fillna(0) + allout_list.append(hyddat) + + ######################################### + # -- Pumped Storage Hydropower -- # + ######################################### + + if int(sw["GSw_Storage"]): + # Input processing currently assumes that cost data in CSV file is in 2004$ + psh_cap = pd.read_csv(os.path.join(inputs_case, "psh_supply_curves_capacity.csv")) + psh_cost = pd.read_csv(os.path.join(inputs_case, "psh_supply_curves_cost.csv")) + psh_durs = pd.read_csv( + os.path.join(inputs_case, "psh_supply_curves_duration.csv"), header=0 + ) + + psh_cap = pd.melt(psh_cap, id_vars=["r"]) + psh_cost = pd.melt(psh_cost, id_vars=["r"]) + + # Convert dollar year + psh_cost[psh_cost.select_dtypes(include=["number"]).columns] *= deflate["PSHcostn"] + + psh_cap["var"] = "cap" + psh_cost["var"] = "cost" + + psh_out = pd.concat([psh_cap, psh_cost]).fillna(0) + psh_out["tech"] = "pumped-hydro" + psh_out["variable"] = psh_out.variable.map(lambda x: x.replace("pshclass", "bin")) + psh_out = psh_out[hyddat.columns].copy() + allout_list.append(psh_out) + + if write: + # Select storage duration correponding to the supply curve + psh_dur_out = psh_durs[psh_durs["pshsupplycurve"] == pshsupplycurve]["duration"] + psh_dur_out.to_csv( + os.path.join(inputs_case, "psh_sc_duration.csv"), index=False, header=False + ) + + write_storage_duration = int(sw["GSw_HydroPSHDurData"]) + write_storinmaxfrac = sw["GSw_HydroStorInMaxFrac"] == "data" + if write_storage_duration or write_storinmaxfrac: + cap_existing_psh = pd.read_csv( + os.path.join(inputs_case, 'cap_existing_psh.csv'), + index_col=['*i', 'v', 'r'] + ) + + if write_storage_duration: + # Calculate capacity-weighted storage duration in + # hours for each region with an existing psh fleet + existing_psh_duration_data = cap_existing_psh.copy() + existing_psh_duration_data['hours'] = ( + existing_psh_duration_data['max_energy_MWh'] / + existing_psh_duration_data['operational_capacity_MW'] + ) + existing_psh_duration_data[['hours']].round(1).to_csv( + os.path.join(inputs_case, 'storage_duration_pshdata.csv') + ) + + if write_storinmaxfrac: + # Calculate max storage_in as a fraction of psh + # capacity for each region with an existing psh fleet + existing_psh_stor_in_data = cap_existing_psh.copy() + existing_psh_stor_in_data['frac'] = ( + existing_psh_stor_in_data['pump_capacity_MW'] / + existing_psh_stor_in_data['operational_capacity_MW'] + ) + existing_psh_stor_in_data[['frac']].round(2).to_csv( + os.path.join(inputs_case, 'storinmaxfrac.csv') + ) + + + ####################################################### + # -- Demand Response -- # + ####################################################### + # Use capacity and cost to add DR Shed to rsc_combined + disagg_data = pd.read_csv(os.path.join(inputs_case,'disagg_state_lpf.csv')) + state2r = disagg_data.groupby('state')['r'].unique().apply(list).to_dict() + if int(sw["GSw_DRShed"]) and write: + # DR Shed input data at state-level, need to assign to model resolution + # State cost are uniformaly distributed across model regions within the state + # State capacity is distributed based on load + + # Calculate state-to-region aggregation/disaggregation factors + state_region_factors = ( + disagg_data.groupby(['state', 'r'], as_index=False) + ['state_frac'] + .sum() + .pivot(index='state', columns='r', values='state_frac') + .rename_axis(None, axis=1) + .fillna(0) + ) + dr_shed_cap_state = pd.read_csv(os.path.join(inputs_case,'dr_shed_cap.csv')) + dr_shed_cap_reg = dr_shed_cap_state.copy() + state_region_factors = state_region_factors.loc[state_region_factors.index.intersection(dr_shed_cap_reg.columns), :] + + # Multiply the hourly state load profiles by the state-to-region factors + dr_shed_cap_reg = ( + dr_shed_cap_reg[state_region_factors.index] + .dot(state_region_factors) + ) + dr_shed_cap_reg.insert(0, 'tech', dr_shed_cap_state['tech']) + + dr_shed_cost_state = pd.read_csv(os.path.join(inputs_case,'dr_shed_cost.csv')) + dr_shed_cost_reg = dr_shed_cost_state.copy() + for col in dr_shed_cost_state.columns[1:]: + # Copy state columns to each model region + for r in state2r[col]: + dr_shed_cost_reg[r] = dr_shed_cost_state[col] + dr_shed_cost_reg.drop(columns=col, inplace=True) + + # Define rsc class using tech + dr_shed_cap = dr_shed_cap_reg.copy() + dr_shed_cap['class'] = dr_shed_cap['tech'] + dr_shed_cost = dr_shed_cost_reg.copy() + dr_shed_cost['class'] = dr_shed_cost['tech'] + + dr_shed_cap = (pd.melt(dr_shed_cap, id_vars=['tech','class']) + .set_index(['tech','class','variable']) + .sort_index()) + dr_shed_cap = dr_shed_cap.reset_index() + dr_shed_cost = pd.melt(dr_shed_cost, id_vars=['tech','class']) + + # Convert dollar year + dr_shed_cost[dr_shed_cost.select_dtypes(include=['number']).columns] *= deflate['dr_shed'] + + # Assign rsc cat + dr_shed_cap['var'] = 'cap' + dr_shed_cost['var'] = 'cost' + + # Combined cost and capacity + dr_shed_dat = pd.concat([dr_shed_cap, dr_shed_cost]) + dr_shed_dat['bin'] = dr_shed_dat['class'].map(lambda x: x.replace('dr_shed_','bin')) + dr_shed_dat['class'] = dr_shed_dat['class'].map(lambda x: x.replace('dr_shed_','')) + + dr_shed_dat.rename(columns={'variable':'r','bin':'variable'}, inplace=True) + dr_shed_dat = dr_shed_dat[['tech','r','value','var','variable']].fillna(0) + allout_list.append(dr_shed_dat) + + if write: + # Update supply curve capacity multiplier from state-level to region-level + dr_shed_capacity_scalar_state = pd.read_csv(os.path.join(inputs_case,'dr_shed_capacity_scalar.csv')) + # Could be empty if regions included in run do not have DR data + if dr_shed_capacity_scalar_state.empty: + pass + else: + dr_shed_capacity_scalar_reg ={} + for st in dr_shed_capacity_scalar_state['r'].unique(): + scalar_df = {} + for r in state2r[st]: + scalar_ba= dr_shed_capacity_scalar_state[dr_shed_capacity_scalar_state['r'] == st].copy() + scalar_ba['r'] = r + scalar_df[r] = scalar_ba + dr_shed_capacity_scalar_reg[st] = pd.concat(scalar_df) + dr_shed_capacity_scalar_reg = pd.concat(dr_shed_capacity_scalar_reg.values(), ignore_index=True) + dr_shed_capacity_scalar_reg.to_csv(os.path.join(inputs_case,'dr_shed_capacity_scalar.csv'), index=False) + + # %%---------------------------------------------------------------------------------- + ################################## + # -- Combine everything -- # + ################################## + + ### Combine, then drop the (cap,cost) entries with nan cost + alloutm = ( + pd.concat(allout_list) + .pivot( + index=["r", "tech", "variable"], columns=["var"], values=["value"] + ) + .dropna()["value"] + .reset_index() + .melt(id_vars=["r", "tech", "variable"])[ + ["tech", "r", "var", "variable", "value"] + ] + ### Rename the first column so GAMS reads the header as a comment + .rename(columns={"tech": "*i", "var": "sc_cat", "variable": "rscbin"}) + .astype({"value": float}) + ### Drop 0 values + .replace({"value": {0.0: np.nan}}) + .dropna() + .round(5) + ) + + ## Merge geothermal non-reV supply curves if applicable + if int(sw["GSw_Geothermal"]): + if not use_geohydro_rev_sc: + geohydro_rsc = pd.read_csv(os.path.join(inputs_case, "geo_rsc.csv"), header=0) + geohydro_rsc = geohydro_rsc.loc[ + geohydro_rsc["*i"].str.startswith("geohydro_allkm") + ] + # Filter by valid regions + geohydro_rsc = geohydro_rsc.loc[geohydro_rsc["r"].isin(val_r_all)] + # Convert dollar year + geohydro_rsc.sc_cat = geohydro_rsc.sc_cat.str.lower() + geohydro_rsc.loc[geohydro_rsc.sc_cat == "cost", "value"] *= deflate[ + "geo_rsc_{}".format(geohydrosupplycurve) + ] + geohydro_rsc["rscbin"] = "bin1" + alloutm = pd.concat([alloutm, geohydro_rsc]) + + if not use_egs_rev_sc: + egs_rsc = pd.read_csv(os.path.join(inputs_case, "geo_rsc.csv"), header=0) + egs_rsc = egs_rsc.loc[egs_rsc["*i"].str.startswith("egs_allkm")] + # Filter by valid regions + egs_rsc = egs_rsc.loc[egs_rsc["r"].isin(val_r_all)] + # Convert dollar year + egs_rsc.sc_cat = egs_rsc.sc_cat.str.lower() + egs_rsc.loc[egs_rsc.sc_cat == "cost", "value"] *= deflate[ + "geo_rsc_{}".format(egssupplycurve) + ] + egs_rsc["rscbin"] = "bin1" + alloutm = pd.concat([alloutm, egs_rsc]) + + egsnearfield_rsc = pd.read_csv(os.path.join(inputs_case, "geo_rsc.csv"), header=0) + egsnearfield_rsc = egsnearfield_rsc.loc[ + egsnearfield_rsc["*i"].str.startswith("egs_nearfield") + ] + # Filter by valid regions + egsnearfield_rsc = egsnearfield_rsc.loc[egsnearfield_rsc["r"].isin(val_r_all)] + # Convert dollar year + egsnearfield_rsc.sc_cat = egsnearfield_rsc.sc_cat.str.lower() + egsnearfield_rsc.loc[egsnearfield_rsc.sc_cat == "cost", "value"] *= deflate[ + "geo_rsc_{}".format(egsnearfieldsupplycurve) + ] + egsnearfield_rsc["rscbin"] = "bin1" + alloutm = pd.concat([alloutm, egsnearfield_rsc]) + + ### Combine with cost components + alloutm = pd.concat([alloutm, cost_components_upv, cost_components_wind]) + if write: + alloutm.to_csv( + os.path.join(inputs_case, "rsc_combined.csv"), index=False, header=True + ) + + #%%---------------------------------------------------------------------------------- + ####################### + # -- Biomass -- # + ####################### + """ + Biomass is currently being handled directly in b_inputs.gms + """ + + # %%---------------------------------------------------------------------------------- + ########################################## + # -- Spur lines (disaggregated) -- # + ########################################## + ### Get interconnection cost for reV sites within modeled area + interconnection_cost = reeds.io.assemble_supplycurve() + sitemap = reeds.io.get_sitemap() + county2zone = reeds.io.get_county2zone(os.path.dirname(os.path.normpath(inputs_case))) + interconnection_cost['r'] = interconnection_cost.index.map(sitemap.FIPS).map(county2zone) + val_r = pd.read_csv( + os.path.join(inputs_case, 'val_r.csv'), + header=None, + ).squeeze(1).values + spursites = interconnection_cost.loc[interconnection_cost.r.isin(val_r)].copy() + spursites['x'] = 'i' + spursites.index.astype(str) + if write: + spursites[["x", "cost_total_trans_usd_per_mw"]].rename(columns={"x": "*x"}).to_csv( + os.path.join(inputs_case, "spurline_cost.csv"), index=False + ) + spursites["x"].to_csv( + os.path.join(inputs_case, "x.csv"), index=False, header=False + ) + spursites[["x", "r"]].rename( + columns={"x": "*x"} + ).drop_duplicates().to_csv(os.path.join(inputs_case, "x_r.csv"), index=False) + + ###### Site maps + ### UPV + sitemap_upv = ( + upvin.assign(i="upv_" + upvin["class"].astype(str)) + .assign(rscbin="bin" + upvin["bin"].astype(str)) + .assign(x="i" + upvin["sc_point_gid"].astype(str)) + ) + sitemap_upv = ( + sitemap_upv + ### Assign rb's based on the no-exclusions transmission table + .assign(r=sitemap_upv.x.map(spursites.set_index("x").r))[ + ["i", "r", "rscbin", "x"] + ].rename(columns={"i": "*i"}) + ) + ### wind-ons + sitemap_windons = ( + windin["ons"] + .assign(i="wind-ons_" + windin["ons"]["class"].astype(str)) + .assign(rscbin="bin" + windin["ons"]["bin"].astype(str)) + .assign(x="i" + windin["ons"]["sc_point_gid"].astype(str)) + ) + sitemap_windons = ( + sitemap_windons + ### Assign r's based on the no-exclusions transmission table + .assign(r=sitemap_windons.x.map(spursites.set_index("x").r))[ + ["i", "r", "rscbin", "x"] + ].rename(columns={"i": "*i"}) + ) + + ### Combine, then only keep sites that show up in both supply curve and spur-line cost tables + spurline_sitemap_list = [sitemap_upv, sitemap_windons] + ### geohydro_allkm + if use_geohydro_rev_sc: + sitemap_geohydro = ( + geoin["geohydro"] + .assign(i="geohydro_allkm_" + geoin["geohydro"]["class"].astype(str)) + .assign(rscbin="bin" + geoin["geohydro"]["bin"].astype(str)) + .assign(x="i" + geoin["geohydro"]["sc_point_gid"].astype(str)) + ) + sitemap_geohydro = ( + sitemap_geohydro + ### Assign rb's based on the no-exclusions transmission table + .assign(r=sitemap_geohydro.x.map(spursites.set_index("x").r))[ + ["i", "r", "rscbin", "x"] + ].rename(columns={"i": "*i"}) + ) + spurline_sitemap_list.append(sitemap_geohydro) + ### egs_allkm + if use_egs_rev_sc: + sitemap_egs = ( + geoin["egs"] + .assign(i="egs_allkm_" + geoin["egs"]["class"].astype(str)) + .assign(rscbin="bin" + geoin["egs"]["bin"].astype(str)) + .assign(x="i" + geoin["egs"]["sc_point_gid"].astype(str)) + ) + sitemap_egs = ( + sitemap_egs + ### Assign rb's based on the no-exclusions transmission table + .assign(r=sitemap_egs.x.map(spursites.set_index("x").r))[ + ["i", "r", "rscbin", "x"] + ].rename(columns={"i": "*i"}) + ) + spurline_sitemap_list.append(sitemap_egs) + + spurline_sitemap = pd.concat(spurline_sitemap_list, ignore_index=True) + spurline_sitemap = spurline_sitemap.loc[ + spurline_sitemap.x.isin(spursites.x.values) + ].copy() + if write: + spurline_sitemap.to_csv( + os.path.join(inputs_case, "spurline_sitemap.csv"), index=False + ) + + ### Add mapping from sc_point_gid to bin for reeds_to_rev.py + site_bin_map_list = [ + upvin.assign(tech="upv")[["tech", "sc_point_gid", "bin"]] + ] + for wind_type, dfin in windin.items(): + site_bin_map_list.append( + dfin.assign(tech=f"wind-{wind_type}")[ + ["tech", "sc_point_gid", "bin"] + ] + ) + if use_geohydro_rev_sc: + site_bin_map_list.append( + geoin["geohydro"].assign(tech="geohydro_allkm")[ + ["tech", "sc_point_gid", "bin"] + ] + ) + if use_egs_rev_sc: + site_bin_map_list.append( + geoin["egs"].assign(tech="egs_allkm")[["tech", "sc_point_gid", "bin"]] + ) + site_bin_map = pd.concat(site_bin_map_list, ignore_index=True) + if write: + site_bin_map.to_csv(os.path.join(inputs_case, "site_bin_map.csv"), index=False) + + return alloutm + + +# %% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +if __name__ == "__main__": + ### Time the operation of this script + tic = datetime.datetime.now() + + ### Parse arguments + parser = argparse.ArgumentParser(description="Format and supply curves") + parser.add_argument("reeds_path", help="path to ReEDS directory") + parser.add_argument("inputs_case", help="path to inputs_case directory") + + args = parser.parse_args() + reeds_path = args.reeds_path + inputs_case = args.inputs_case + + #%% Set up logger + log = reeds.log.makelog( + scriptname=__file__, + logpath=os.path.join(inputs_case,'..','gamslog.txt'), + ) + + # %% Run it + print("Starting writesupplycurves.py") + + main(reeds_path=reeds_path, inputs_case=inputs_case) + + reeds.log.toc( + tic=tic, year=0, process='inputs/writesupplycurves.py', + path=os.path.join(inputs_case,'..')) + + print('Finished writesupplycurves.py') diff --git a/reeds/io.py b/reeds/io.py index a3d9cd27..2ff40de6 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -650,7 +650,7 @@ def get_switches(case=None, **kwargs): try: fpath_asw = os.path.join( (case if case is not None else reeds_path), - 'ReEDS_Augur', 'augur_switches.csv', + 'reeds', 'resource_adequacy', 'augur_switches.csv', ) asw = pd.read_csv(fpath_asw, index_col='key') for i, row in asw.iterrows(): @@ -682,9 +682,9 @@ def get_switches(case=None, **kwargs): ### Get number of threads to use in PRAS opt_file = 'cplex.opt' if int(sw.GSw_gopt) == 1 else f'cplex.op{sw.GSw_gopt}' try: - threads = get_param_value(os.path.join(case, opt_file), "threads", dtype=int) + threads = get_param_value(os.path.join(case, 'reeds', 'solver', opt_file), "threads", dtype=int) except (FileNotFoundError, TypeError): - threads = get_param_value(os.path.join(reeds_path, opt_file), "threads", dtype=int) + threads = get_param_value(os.path.join(reeds_path, 'reeds', 'solver', opt_file), "threads", dtype=int) sw['threads'] = threads ### Determine whether run is on HPC sw['hpc'] = True if int(os.environ.get('REEDS_USE_SLURM', 0)) else False diff --git a/reeds/parse.py b/reeds/parse.py new file mode 100644 index 00000000..177c36e0 --- /dev/null +++ b/reeds/parse.py @@ -0,0 +1,840 @@ +### Imports +import os +import re +import sys +import yaml +import hashlib +import shapely +import numpy as np +import pandas as pd +import sklearn.cluster +import geopandas as gpd +from pathlib import Path +from warnings import warn +sys.path.append(str(Path(__file__).parent.parent)) +import reeds +from reeds.input_processing import mcs_sampler + +### Functions +def parse_regions(case_or_string, case=None): + """ + Inputs + ------ + case_or_string: path to a ReEDS case or a parseable string in the format of GSw_Region + case: path to a ReEDS case. Only used if case_or_string is not a ReEDS case. Should be + used if you want to select a subset of model zones from a ReEDS case that used + region aggregation. + + Returns + ------- + np.array of zone names + - If case_or_string is a case, return the regions modeled in the run + - If case_or_string is a parseable string in the format of GSw_Region, return + the regions that obey that string + + Examples + -------- + parse_regions('transreg/NYISO') -> ['p127', 'p128'] + parse_regions('st/PA') -> ['p115', 'p119', 'p120', 'p122'] + parse_regions('st/PA', 'path/to/case/using/region/aggregation') -> ['p115', 'p120', 'z122'] + """ + if os.path.exists(case_or_string): + sw = reeds.io.get_switches(case_or_string) + hierarchy = reeds.io.get_hierarchy(case_or_string) + GSw_Region = sw['GSw_Region'] + ## Provide case argument if using aggregated regions + elif os.path.exists(str(case)): + hierarchy = reeds.io.get_hierarchy(case) + GSw_Region = case_or_string + else: + hierarchy = reeds.io.get_hierarchy() + GSw_Region = case_or_string + + level, regions = GSw_Region.split('/') + regions = regions.split('.') + if level in ['r', 'ba']: + rs = [r for r in hierarchy.index if r in regions] + else: + rs = hierarchy.loc[hierarchy[level].isin(regions)].index + return rs + + +def parse_yearset(yearset:str) -> list: + """ + Parses a ReEDS-formatted yearset and returns a list of integer years. + + Args: + yearset (str): _-delimited list of individual years OR bash-formatted year ranges + + Returns: + list of integer years (sorted) + + Examples: + '2010' -> [2010] + '2010_2015_2020' -> [2010, 2015, 2020] + '2010..2020..5' -> [2010, 2015, 2020] + '2010_2015_2020..2050..3' -> [ + 2010, 2015, + 2020, 2023, 2026, 2029, 2032, 2035, 2038, 2041, 2044, 2047, 2050 + ] + '2010..2035..5_2040..2100..10' -> [ + 2010, 2015, 2020, 2025, 2030, 2035, + 2040, 2050, 2060, 2070, 2080, 2090, 2100 + ] + """ + pattern = r'^2\d{3}(\.\.2\d{3}(\.\.\d+)?)?(_2\d{3}(\.\.2\d{3}(\.\.\d+)?)?)*$' + helper = ( + "For formatting notes and examples, run the following commands:\n" + "$ python\n" + ">>> import reeds\n" + ">>> help(reeds.parse.parse_yearset)" + ) + if not re.match(pattern, yearset): + err = f"Invalid yearset ({yearset}); must match {pattern}. {helper}" + raise ValueError(err) + yearstrings = yearset.split('_') + years = [] + for y in yearstrings: + subyears = [int(i) for i in y.split('..')] + if len(subyears) == 1: + years.append(subyears[0]) + elif len(subyears) == 2: + years.extend(range(subyears[0], subyears[1]+1)) + elif len(subyears) == 3: + years.extend(range(subyears[0], subyears[1]+1, subyears[2])) + else: + err = f"Invalid subyears ({subyears}) in yearset {yearset}. {helper}" + raise ValueError(err) + out = sorted(set(years)) + return out + + +def add_intermediate_switches(dfcases:pd.DataFrame) -> pd.DataFrame: + """Determine some switch settings from other switches""" + ignore_columns = ['Choices', 'Description', 'Default Value'] + cases = [i for i in dfcases if i not in ignore_columns] + new_switches = {} + for case in cases: + sw = dfcases[case] + new_switches[case] = {} + ### TEMPORARY 20260402: The GSw_RegionResolution switch is deprecated; + ### for now, hardcode its value for the region resolutions that use it + match sw['GSw_ZoneSet']: + case 'z134': + GSw_RegionResolution = 'ba' + case 'z3109': + GSw_RegionResolution = 'county' + case 'PJMcounty' | 'UTcounty': + GSw_RegionResolution = 'mixed' + case _: + GSw_RegionResolution = 'aggreg' + new_switches[case]['GSw_RegionResolution'] = GSw_RegionResolution + ### TEMPORARY 20260402: Turn off itlgrp constraint until it's fixed + # new_switches[case]['GSw_itlgrpConstraint'] = str(int( + # sw['GSw_RegionResolution'] in ['county', 'mixed'] + # )) + new_switches[case]['GSw_itlgrpConstraint'] = '0' + ## 'meshed' offshore files are only used when offshore zones are turned on + new_switches[case]['GSw_OffshoreFiles'] = ( + 'meshed' if int(sw['GSw_OffshoreZones']) else 'radial' + ) + ## Load site region level (GSw_LoadSiteReg) is embedded in GSw_LoadSiteTrajectory + new_switches[case]['GSw_LoadSiteReg'] = sw['GSw_LoadSiteTrajectory'].split('_')[0] + ## Get numbins from the max of individual technology bins + new_switches[case]['numbins'] = str(max( + int(sw['numbins_windons']), + int(sw['numbins_windofs']), + int(sw['numbins_upv']), + 15, + )) + dfcases_out = pd.concat([dfcases, pd.DataFrame(new_switches)]) + return dfcases_out + + +def parse_cases( + cases_filename:str='cases_test.csv', + single:str='', + skip_checks:bool=False, +) -> pd.DataFrame: + """ + Read a ReEDS cases file, look up empty switch values from "Default Value" or cases.csv, + and return a dataframe of all switches and values. + + Args: + cases_filename (str): 'cases_{something}.csv' or 'cases.csv' + single (str): If not '', specifies a single column to keep from cases_filename + skip_checks (bool): Skip case validation (not recommended) + + Returns: + pd.DataFrame + """ + dfcases = pd.read_csv( + os.path.join(reeds.io.reeds_path, 'cases.csv'), dtype=object, index_col=0) + + # If we have a case suffix, use cases_[suffix].csv for cases. + if cases_filename != 'cases.csv': + dfcases = dfcases[['Choices', 'Default Value']] + dfcases_suf = pd.read_csv( + os.path.join(reeds.io.reeds_path, cases_filename), dtype=object, index_col=0) + # Replace periods and spaces in case names with _ + dfcases_suf.columns = [ + c.replace(' ','_').replace('.','_') if c != 'Default Value' else c + for c in dfcases_suf.columns] + + # Check to make sure user-specified cases file has up-to-date switches + missing_switches = [s for s in dfcases_suf.index if s not in dfcases.index] + if len(missing_switches): + error = ( + "The following switches are in {} but have changed names or are no longer " + "supported by ReEDS:\n\n{} \n\nPlease update your cases file; " + "for the full list of available switches see cases.csv. " + "Note that switch names are case-sensitive." + ).format(cases_filename, '\n'.join(missing_switches)) + raise ValueError(error) + + # First use 'Default Value' from cases_[suffix].csv to fill missing switches + # Later, we will also use 'Default Value' from cases.csv to fill any remaining holes. + if 'Default Value' in dfcases_suf.columns: + case_i = dfcases_suf.columns.get_loc('Default Value') + 1 + casenames = dfcases_suf.columns[case_i:].tolist() + for case in casenames: + dfcases_suf[case] = dfcases_suf[case].fillna(dfcases_suf['Default Value']) + dfcases_suf.drop(['Choices','Default Value'], axis='columns',inplace=True, errors='ignore') + dfcases = dfcases.join(dfcases_suf, how='outer') + + casenames = [c for c in dfcases.columns if c not in ['Description','Default Value','Choices']] + # Get the list of switch choices + choices = dfcases.Choices.copy() + + for case in casenames: + # Fill any missing switches with the defaults in cases.csv + dfcases[case] = dfcases[case].fillna(dfcases['Default Value']) + + # If --single/-s was passed, only keep those cases (regardless of ignore) + # otherwise, drop any case marked ignore + if single: + if case not in single.split(','): + continue + else: + if int(dfcases.loc['ignore', case]) == 1: + continue + + # Check to make sure the switch setting is valid + for i, val in dfcases[case].items(): + if skip_checks: + continue + # check that the switch isn't duplicated + if isinstance(choices[i], pd.Series) and len(choices[i]) > 1: + error = ( + f'Duplicate entries for "{i}", delete one and restart.' + ) + raise ValueError(error) + ### Split choices by either '; ' or ',' + if choices[i] in ['N/A',None,np.nan]: + pass + elif choices[i].lower() in ['int','integer']: + try: + int(val) + except ValueError: + error = ( + f'Invalid entry for "{i}" for case "{case}".\n' + f'Entered "{val}" but must be an integer.' + ) + raise ValueError(error) + elif choices[i].lower() in ['float','numeric','number','num']: + try: + float(val) + except ValueError: + error = ( + f'Invalid entry for "{i}" for case "{case}".\n' + f'Entered "{val}" but must be a float (number).' + ) + raise ValueError(error) + else: + i_choices = [ + str(j).strip() for j in + np.ravel([i.split(',') for i in choices[i].split(';')]).tolist() + ] + matches = [re.match(choice, str(val)) for choice in i_choices] + if not any(matches): + error = ( + f'Invalid entry for "{i}" for case "{case}".\n' + f'Entered "{val}" but must match one of the following:\n> ' + + '\n> '.join(i_choices) + + f'\nOr, if "{val}" is intended, it must be added to the ' + '"Choices" column in cases.csv.' + ) + raise ValueError(error) + + # Check GSw_Region switch and ask user to correct if commas are used instead of + # periods to list multiple regions + if ',' in (dfcases[case].loc['GSw_Region']) : + print("Please change the delimeter in the GSw_Region switch from ',' to '.'") + quit() + + # If doing a Monte Carlo run, modify dfcases by adding new columns + # for each scenario run. Also validate the distribution file. + warned_about_cluster_alg = False + if 'MCS_runs' in dfcases.index: + for c in dfcases.columns: + if ( + c not in ['Description','Default Value','Choices'] + and (int(dfcases.loc['MCS_runs',c]) > 0) + and (not int(dfcases.loc['ignore',c])) + ): + # Warn user if the hourly clustering algorithm is not fixed for Monte Carlo runs + if ( + not dfcases.at['GSw_HourlyClusterAlgorithm', c].startswith('user') + and not warned_about_cluster_alg + ): + print(f"\n[Warning] Case Column: '{c}'") + print( + "You are attempting to run a Monte Carlo simulation with " + "`GSw_HourlyClusterAlgorithm` set to a value other than 'user'.\n" + "This may result in inconsistent representative days across MCS runs.\n\n" + "To ensure consistency, we strongly recommend setting " + "`GSw_HourlyClusterAlgorithm = user` in your switch configuration.\n" + "Do you want to proceed with the current setup?" + ) + user_input = input("Type 'yes' to proceed, or 'no' to exit: ").strip().lower() + if user_input not in ['yes', 'y']: + print("\nPlease update the `GSw_HourlyClusterAlgorithm` switch and restart.") + quit() + warned_about_cluster_alg = True + print() + + # Validate the distribution file + sw = dfcases[c].fillna(dfcases['Default Value']) + mcs_dist_path = os.path.join( + reeds.io.reeds_path, 'inputs', 'userinput', + 'mcs_distributions_{}.yaml'.format(sw.MCS_dist) + ) + mcs_sampler.general_mcs_dist_validation(reeds.io.reeds_path, mcs_dist_path, sw) + + # c (column) is a case with monte carlo runs. + # replicate this column N (NumMonteCarloRuns) times + NumMonteCarloRuns = int(dfcases.loc['MCS_runs',c]) + NewColumnNames = [ + f"{c}_MC{i:0>4}" + for i in range(1, NumMonteCarloRuns + 1) + ] + + # Each new column is a copy of the original column with name c_{MC1,MC2,...} + dfcases_MC = pd.DataFrame( + data=np.array([dfcases[c].values]*NumMonteCarloRuns).T, + index=dfcases.index, + columns=NewColumnNames, + ) + dfcases = pd.concat([dfcases, dfcases_MC], axis=1) + # drop the original column + dfcases.drop(c, axis=1, inplace=True) + + ## Add switches determined from other switches and remove unnecessary columns + dfcases_out = ( + add_intermediate_switches(dfcases) + .drop(columns=['Choices', 'Description', 'Default Value'], errors='ignore') + ) + + return dfcases_out + + +def solvestring_sequential( + batch_case, caseSwitches, + cur_year, next_year, prev_year, restartfile, + toLogGamsString=' logOption=4 logFile=gamslog.txt appendLog=1 ', + hpc=0, iteration=0, stress_year=None, + temporal_inputs='rep', + ): + """ + Typical inputs: + * restartfile: batch_case if first solve year else {batch_case}_{prev_year} + * caseSwitches: loaded from {batch_case}/inputs_case/switches.csv + """ + savefile = f"{batch_case}_{cur_year}i{iteration}" + _stress_year = f"{cur_year}i0" if stress_year is None else stress_year + out = ( + f"gams {Path('reeds','core','solve','3_solve_oneyear.gms')}" + + (" license=gamslice.txt" if hpc else '') + + " o=" + os.path.join("lstfiles", f"{savefile}.lst") + + " r=" + os.path.join("g00files", restartfile) + + " gdxcompress=1" + + " xs=" + os.path.join("g00files", savefile) + + toLogGamsString + + f" --case={batch_case}" + + f" --cur_year={cur_year}" + + f" --next_year={next_year}" + + f" --prev_year={prev_year}" + + f" --stress_year={_stress_year}" + + f" --temporal_inputs={temporal_inputs}" + + ''.join([f" --{s}={caseSwitches[s]}" for s in [ + 'GSw_Canada', + 'GSw_ClimateHydro', + 'GSw_ClimateWater', + 'GSw_gopt', + 'GSw_HourlyChunkLengthRep', + 'GSw_HourlyChunkLengthStress', + 'GSw_HourlyType', + 'GSw_HourlyWrapLevel', + 'GSw_MGA_CostDelta', + 'GSw_MGA_Direction', + 'GSw_PVB_Dur', + 'GSw_SkipAugurYear', + 'GSw_StateCO2ImportLevel', + 'GSw_StartMarkets', + 'GSw_ValStr', + 'solver', + 'debug', + 'startyear', + 'diagnose', + 'diagnose_year' + ]]) + + '\n' + ) + + return out + + +def get_bin( + df_in, + bin_num, + bin_method='equal_cap_cut', + bin_col='capacity_factor_ac', + bin_out_col='bin', + weight_col='capacity', +): + """ + bin supply curve points based on a specified bin column. Used in hourlize to create 'bins' + for the resource classes (typically using capacity factor) and then used by + writesupplycurves.py to create bins based on supply curve cost. + """ + df = df_in.copy() + ser = df[bin_col] + # If we have less than or equal unique points than bin_num, + # we simply group the points with the same values. + if ser.unique().size <= bin_num: + bin_ser = ser.rank(method='dense') + df[bin_out_col] = bin_ser.values + elif bin_method == 'kmeans': + nparr = ser.to_numpy().reshape(-1,1) + weights = df[weight_col].to_numpy() + kmeans = ( + sklearn.cluster.KMeans(n_clusters=bin_num, random_state=0, n_init=10) + .fit(nparr, sample_weight=weights) + ) + bin_ser = pd.Series(kmeans.labels_) + # but kmeans doesn't necessarily label in order of increasing value because it is 2D, + # so we replace labels with cluster centers, then rank + kmeans_map = pd.Series(kmeans.cluster_centers_.flatten()) + bin_ser = bin_ser.map(kmeans_map).rank(method='dense') + df[bin_out_col] = bin_ser.values + elif bin_method == 'equal_cap_man': + # using a manual method instead of pd.cut because i want the first bin to contain the + # first sc point regardless, even if its weight_col value is more than the capacity + # of the bin, and likewise for other bins, so i don't skip any bins. + orig_index = df.index + df.sort_values(by=[bin_col], inplace=True) + cumcaps = df[weight_col].cumsum().tolist() + totcap = df[weight_col].sum() + vals = df[bin_col].tolist() + bins = [] + curbin = 1 + for i, _v in enumerate(vals): + bins.append(curbin) + if cumcaps[i] >= totcap*curbin/bin_num: + curbin += 1 + df[bin_out_col] = bins + # we need the same index ordering for apply to work + df = df.reindex(index=orig_index) + elif bin_method == 'equal_cap_cut': + # Use pandas.cut with cumulative capacity in each class. This will assume equal capacity bins + # to bin the data. + orig_index = df.index + df.sort_values(by=[bin_col], inplace=True) + df['cum_cap'] = df[weight_col].cumsum() + bin_ser = pd.cut(df['cum_cap'], bin_num, labels=False) + bin_ser = bin_ser.rank(method='dense') + df[bin_out_col] = bin_ser.values + # we need the same index ordering for apply to work + df = df.reindex(index=orig_index) + df[bin_out_col] = df[bin_out_col].astype(int) + return df + + +def hash_string(string:str, hashfunc='md5') -> str: + """Return the hash of a string""" + _hashfunc = getattr(hashlib, hashfunc) + return _hashfunc(string.encode()).hexdigest() + + +def hash_counties(countylist, delim_county=',', hashfunc='md5') -> list: + """ + Takes a list of 5-digit county FIPS codes, sorts them, concatenates them into a string + delimited by `delim_county`, and returns a hash using the hashlib function + specified by `hashfunc`. + """ + ## Validate the inputs + invalid = [i for i in countylist if not re.match(r'^\d{5}$', i)] + if len(invalid): + err = ( + "The following entries in countylist do not look like 5-digit FIPS codes:\n" + ','.join(invalid) + ) + raise ValueError(err) + delim_string = delim_county.join(sorted(countylist)) + return hash_string(delim_string, hashfunc=hashfunc) + + +def get_itl_config() -> dict: + configpath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'itl_config.yaml') + with open(configpath, 'r') as f: + config = yaml.safe_load(f) + return config + + +def get_itl(r, rr, case=None, errors='raise', **kwargs) -> dict: + """ + Get the ITL for a single interface from zone `r` to `rr`. + The resolution can be provided by: + - Providing a path to a ReEDS run via `case` + - Providing `GSw_ZoneSet` as a keyword argument + """ + sw = reeds.io.get_switches(case, **kwargs) + config = get_itl_config() + hashfunc = config['hashfunc'] + county2zone = reeds.io.get_county2zone(case, **kwargs) + rs = county2zone.unique() + for _r, rlabel in [(r, 'r'), (rr, 'rr')]: + if _r not in rs: + err = f"{rlabel} = {_r} is not defined for GSw_ZoneSet = {sw.GSw_ZoneSet}" + raise KeyError(err) + ## Get the ITLs for all interfaces + itlspath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'itl_NARIS.csv') + itls = pd.read_csv(itlspath, index_col=[f'{hashfunc}_from', f'{hashfunc}_to']) + ## Look up the desired interface + rhash = hash_counties(county2zone.loc[county2zone==r].index.tolist()) + rrhash = hash_counties(county2zone.loc[county2zone==rr].index.tolist()) + try: + itl = itls.loc[rhash, rrhash].to_dict() + except KeyError: + try: + ## Check for the other direction. If it exists, reverse the definition of + ## 'forward' and 'reverse' to match the user-provided 'r' and 'rr'. + _itl = itls.loc[rrhash, rhash].to_dict() + itl = {'MW_forward': _itl['MW_reverse'], 'MW_reverse': _itl['MW_forward']} + except KeyError: + ## The requested interface is not in the table + itl = {'MW_forward':0, 'MW_reverse':0} + interfacepath = Path(reeds.io.reeds_path, 'inputs', 'zones', sw.GSw_ZoneSet) + err = ( + f"The interface defined by r = {r} and rr = {rr} with " + f"GSw_ZoneSet = {sw.GSw_ZoneSet} does not have an ITL in {itlspath}. " + "It either has not been calculated or the provided zones are not " + f"connected. Check {interfacepath} to see if a value is expected for " + "this interface." + ) + if errors == 'raise': + raise KeyError(err) + elif errors == 'warn': + warn(err) + return itl + + +def get_itls(case=None, level:str='r', errors='raise', **kwargs) -> pd.DataFrame: + """ + Get all the ITLs for the specified resolution. The resolution can be specified by: + - Providing a path to a ReEDS run via `case` + - Providing `GSw_ZoneSet` as a keyword argument + If neither `case` nor `GSw_ZoneSet` are provided, the default resolution from + `cases.csv` is used. + + Args: + level (str): 'r' or 'transgrp' + + Inputs for testing: + case = None + level = 'r' + kwargs = {} + errors = 'raise' + """ + sw = reeds.io.get_switches(case, **kwargs) + inputs = Path(reeds.io.reeds_path, 'inputs') + ## Get the ITL config settings + config = get_itl_config() + hashfunc = config['hashfunc'] + ## Get the ITLs for all interfaces + fpath = Path(inputs, 'transmission', 'itl_NARIS.csv') + itls = ( + pd.read_csv(fpath) + .rename(columns={ + f'{hashfunc}_from':f'{hashfunc}_r', + f'{hashfunc}_to':f'{hashfunc}_rr', + }) + ) + if itls.index.duplicated().sum(): + raise ValueError('Duplicate entries in ITL database') + ### Get the zone hashes + if level == 'r': + ## We save the zonehash for level == 'r' directly for peace of mind + zonehash = pd.read_csv( + Path(inputs, 'zones', sw.GSw_ZoneSet, 'zonehash.csv'), + index_col='r', + )[hashfunc] + else: + ## For other levels, we calculate the zonehash from the hierarchy + hierarchy = reeds.io.assemble_hierarchy(case, **kwargs).set_index('r') + county2zone = reeds.io.get_county2zone(case=None, **kwargs) + county2level = county2zone.map(hierarchy[level]).rename(level) + if county2level.isnull().sum(): + print(county2level.loc[county2level.isnull()]) + err = ( + "Model zones in county2zone.csv and hierarchy.csv " + f"for GSw_ZoneSet={sw.GSw_ZoneSet} do not match" + ) + raise ValueError(err) + zonehash = county2level.reset_index().groupby(level).FIPS.agg(hash_counties) + ### Get the ITLs + interfacepath = Path(inputs, 'zones', sw.GSw_ZoneSet, f'interfaces_{level}.csv') + dfout = pd.read_csv(interfacepath) + for i, r in enumerate(['r', 'rr']): + dfout[r] = dfout.interface.str.split(config['idelim']).str[i] + dfout[f'{hashfunc}_{r}'] = dfout[r].map(zonehash) + dfout = dfout.merge(itls, on=[f'{hashfunc}_r', f'{hashfunc}_rr'], how='left') + ### Make sure it worked + missing = dfout.loc[dfout.MW_forward.isnull() | dfout.MW_reverse.isnull()] + if len(missing): + print(missing) + err = f'Missing ITL for {len(missing)} interfaces' + if len(missing) <= 10: + err += ': ' + (' '.join(missing.interface)) + if errors == 'raise': + raise KeyError(err) + elif errors == 'warn': + warn(err) + return dfout.dropna() + + +def get_zones(case=None, crs='ESRI:102008', **kwargs) -> gpd.GeoDataFrame: + """ + Args: + case (str, Path, or None): Path to a ReEDS case. + If None, uses the default GSw_ZoneSet from cases.csv. + crs (str): Coordinate reference system + **kwargs: ReEDS switch:value pairs (overrides case argument) + """ + dfcounty = reeds.spatial.get_map('county', source='tiger', crs=crs) + dfstates = reeds.spatial.get_map('states', source='census', crs=crs) + country = dfstates.dissolve().geometry[0] + county2zone = reeds.io.get_county2zone(case, **kwargs) + + dfcounty['r'] = county2zone + + dfzones = dfcounty.dissolve('r') + dfzones.geometry = dfzones.intersection(country).buffer(0) + + return dfzones[['geometry']] + + +def _make_line(row): + return shapely.LineString([[row.from_lon, row.from_lat], [row.to_lon, row.to_lat]]) + + +def get_hvdc_lines(): + """Load data for individual HVDC lines""" + datapath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'hvdc_lines.csv') + dfdc = pd.read_csv(datapath) + dfdc['geometry'] = dfdc.apply(_make_line, axis=1) + dfdc = gpd.GeoDataFrame(dfdc, crs='EPSG:4326') + for i, side in enumerate(['from', 'to']): + dfdc[f'{side}_latlon'] = dfdc.geometry.map(lambda x: shapely.geometry.Point(x.coords[i])) + return dfdc + + +def map_hvdc_lines_to_interfaces(case=None, **kwargs) -> pd.DataFrame: + """ + Assign HVDC line capacity to interfaces by mapping start/end points to zones + + Inputs for testing: + case = None + kwargs = {'GSw_ZoneSet': 'z90'} + """ + dfzones = get_zones(case, **kwargs) + dfdc = get_hvdc_lines().to_crs(dfzones.crs) + for i, side in enumerate(['from', 'to']): + dfdc[f'zone_{side}'] = gpd.sjoin( + dfdc.set_geometry(f'{side}_latlon').set_crs('EPSG:4326').to_crs(dfzones.crs), + dfzones.reset_index(), + how='left', + )['r'] + + dfcap = ( + dfdc.loc[dfdc.zone_from != dfdc.zone_to].dropna() + .rename(columns={'zone_from':'r', 'zone_to':'rr'}) + ).copy() + ## Normalize from/to order and sum capacity for each interface + for index, row in dfcap.iterrows(): + for side, r in enumerate(['r', 'rr']): + dfcap.loc[index, r] = sorted(row[['r','rr']])[side] + dfout = dfcap.groupby(['r','rr'])[['name','MW']].agg({'MW':sum, 'name':list}) + return dfout + + +def get_b2b(case=None, **kwargs) -> pd.DataFrame: + """ + Get back-to-back (B2B) converter capacity for specified zone resolution. + Check it against the sum of known individual converter capacities. + + Inputs for testing: + case = None + kwargs = {} + """ + sw = reeds.io.get_switches(case, **kwargs) + b2bpath = Path(reeds.io.reeds_path, 'inputs', 'zones', sw.GSw_ZoneSet, 'b2b.csv') + b2b = pd.read_csv(b2bpath).drop(columns=['name', 'notes'], errors='ignore') + + ## Take the sum by interconnection for validation + hierarchy = reeds.io.assemble_hierarchy(case, **kwargs).set_index('r') + _b2b = b2b.copy() + for i, (r, side) in enumerate([('r', 'from'), ('rr', 'to')]): + _b2b[f'interconnect_{side}'] = _b2b[r].map(hierarchy.interconnect) + _b2b['interface'] = _b2b.apply( + lambda row: '~~'.join(sorted([row.interconnect_from, row.interconnect_to])), + axis=1 + ) + b2b_interconnect = _b2b.groupby('interface').MW.sum() + + ## Get data for individual converters + vpath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'b2b_converters.csv') + converters = pd.read_csv(vpath) + converters['interface'] = converters.apply( + lambda row: '~~'.join(sorted([row.interconnection_from, row.interconnection_to])), + axis=1 + ) + converters_interconnect = converters.groupby('interface').MW.sum() + + ## Interface capacity should match sum of individual converters + if (b2b_interconnect != converters_interconnect).any(): + err = ( + f"The B2B interface capacity in {b2bpath} does not match the sum of " + f"individual B2B converter capacity in {vpath}" + ) + raise ValueError(err) + + return b2b + + +def check_aggreg_unique(hierarchy): + """ + Make sure each aggreg is only assigned to a single transreg / transgrp / st / etc. + """ + testcols = [i for i in hierarchy.columns if i != 'aggreg'] + aggreg_errors = {} + for col in testcols: + unique_aggregs = ( + hierarchy[[col,'aggreg']] + .drop_duplicates() + .groupby('aggreg')[col].count() + ) + duplicated = unique_aggregs.loc[unique_aggregs>1] + if len(duplicated): + aggreg_errors[col] = hierarchy.loc[ + hierarchy.aggreg.isin(duplicated.index), + [col,'aggreg'] + ] + return aggreg_errors + + +def validate_zoneset(GSw_ZoneSet): + """ + Make sure all the required inputs are supplied for GSw_ZoneSet + + Test all options: + GSw_ZoneSets = [ + 'z48', + 'z54', + 'z69', + 'z90', + 'z132', + 'z134', + # 'z153', + # 'z1259', + # 'z2972', + 'z3109', + 'UTcounty', + 'PJMcounty', + ] + for GSw_ZoneSet in GSw_ZoneSets: + print(GSw_ZoneSet) + validate_zoneset(GSw_ZoneSet) + """ + zonepath = Path(reeds.io.reeds_path, 'inputs', 'zones', GSw_ZoneSet) + ## Do all the files exist? + required_files = [ + 'b2b.csv', + 'county2zone.csv', + 'hierarchy.csv', + 'interfaces_r.csv', + 'interfaces_transgrp.csv', + 'zonehash.csv', + ] + missing = [f for f in required_files if not Path(zonepath, f).is_file()] + if len(missing): + err = f'Missing these files from {zonepath}: ' + ' '.join(missing) + raise FileNotFoundError(err) + ## Are all/only the right counties included? + fpath_county2zone = Path(zonepath, 'county2zone.csv') + fpath_countystate = Path(reeds.io.reeds_path, 'inputs', 'zones', 'county_state.csv') + county2zone = pd.read_csv(fpath_county2zone, dtype=str) + county_state = pd.read_csv(fpath_countystate, dtype=str) + extra_fips = county2zone.loc[~county2zone.FIPS.isin(county_state.FIPS), 'FIPS'].values + missing_fips = county_state.loc[~county_state.FIPS.isin(county2zone.FIPS), 'FIPS'].values + if len(extra_fips): + raise ValueError( + f"{len(extra_fips)} counties should NOT be in {fpath_county2zone}: " + f"{', '.join(extra_fips)}" + ) + if len(missing_fips): + raise ValueError ( + f"{len(missing_fips)} counties are missing from {fpath_county2zone}: " + f"{', '.join(missing_fips)}" + ) + ## Do the zone definitions in county2zone.csv match zonehash.csv? + config = get_itl_config() + hashfunc = config['hashfunc'] + zonehash = pd.read_csv(Path(zonepath, 'zonehash.csv'), index_col='r')[hashfunc] + checkhash = county2zone.groupby('r').FIPS.agg(hash_counties) + if (zonehash != checkhash).any(): + _df = pd.concat({'zonehash.csv':zonehash, 'county2zone.csv':checkhash}, axis=1) + wrong = _df.loc[_df['zonehash.csv'] != _df['county2zone.csv']] + print(wrong) + raise ValueError( + f"zonehash.csv and county2zone.csv in inputs/zones/{GSw_ZoneSet} do not " + f"match for {len(wrong)} zones: {', '.join(wrong.index)}" + ) + ## Do all the zone interfaces have ITLs? + get_itls(GSw_ZoneSet=GSw_ZoneSet, errors='raise') + ## Do all the transgrp interfaces have ITLs? + get_itls(GSw_ZoneSet=GSw_ZoneSet, level='transgrp', errors='raise') + ## Do the hierarchy files have all the required columns? + required_levels = ['st', 'transreg', 'transgrp', 'nercr', 'interconnect'] + hierarchy = reeds.io.assemble_hierarchy(GSw_ZoneSet=GSw_ZoneSet).set_index('r') + missing = [i for i in required_levels if i not in hierarchy] + if len(missing): + hierarchypath = Path(zonepath, 'hierarchy.csv') + err = f'The following columns are missing from {hierarchypath}: ' + ' '.join(missing) + raise KeyError(err) + ## TEMPORARY 20260402: Is each aggreg only assigned to a single hierarchy level? + fpath_134 = Path(zonepath, 'hierarchy_from134.csv') + if fpath_134.is_file(): + hierarchy_134 = pd.read_csv(fpath_134, index_col='ba') + errors = check_aggreg_unique(hierarchy_134) + if len(errors): + for v in errors.values(): + print(v) + print() + err = ( + "There are aggreg values spanning multiple hierarchy levels for:\n > " + + '\n > '.join(errors.keys()) + + f"\nPlease modify {fpath_134}\n" + "to ensure each aggreg is only assigned to a single hierarchy level." + ) + raise ValueError(err) diff --git a/reeds/prasplots.py b/reeds/prasplots.py index 9722ff6e..2794c489 100644 --- a/reeds/prasplots.py +++ b/reeds/prasplots.py @@ -339,7 +339,7 @@ def plot_pras_samples( reeds.io.get_last_iteration(case, t) if iteration in [None, 'last'] else iteration ) - rs = reeds.inputs.parse_regions(region, case) + rs = reeds.parse.parse_regions(region, case) bokehcolors, plotorder = reeds.reedsplots.get_tech_colors_order(order='fuel_storage_vre') diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index da1013b1..7501c6ac 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -4992,8 +4992,7 @@ def plot_seed_stressperiods( ): """ """ - sys.path.append(os.path.join(reeds.io.reeds_path, 'input_processing')) - import hourly_repperiods + from reeds.input_processing import hourly_repperiods sw = reeds.io.get_switches(case) hierarchy = reeds.io.get_hierarchy(case) @@ -6212,8 +6211,7 @@ def map_stressors( 'load': os.path.join(case, 'ReEDS_Augur', 'augur_data', f'pras_load_{t}.h5'), } if any([not os.path.exists(fpath) for fpath in augur_files.values()]): - import ReEDS_Augur.prep_data as prep_data - prep_data.main(t, case, iteration) + reeds.resource_adequacy.prep_data.main(t, case, iteration) vre_gen = reeds.io.read_file(augur_files['vre_gen'], parse_timestamps=True) vre_gen.columns = pd.MultiIndex.from_tuples( diff --git a/reeds/resource_adequacy/Augur.py b/reeds/resource_adequacy/Augur.py new file mode 100644 index 00000000..17ad0026 --- /dev/null +++ b/reeds/resource_adequacy/Augur.py @@ -0,0 +1,224 @@ +#%% Imports +import argparse +import os +import sys +import subprocess +import datetime +import pandas as pd +import gdxpds +import reeds + + +#%% Functions +def run_pras( + casedir, + t, + iteration=0, + recordtime=True, + repo=False, + overwrite=True, + include_samples=False, + write_flow=False, + write_surplus=False, + write_energy=False, + write_shortfall_samples=False, + write_availability_samples=False, + **kwargs, + ): + """ + If additional keyword arguments are provided, they override values in the switches + dictionary generated using `reeds.io.get_switches(casedir)`. + Only keys in the switches dictionary are allowed, but we do not check types or + self-consistency for the provided values, so review the allowed choices in cases.csv + and use these additional arguments at your own risk. + """ + ### Get the PRAS settings for this solve year + print('Running ReEDS2PRAS and PRAS') + sw = reeds.io.get_switches(casedir) + ## If additional kwargs are provided, use them to override the switch values + for key, val in kwargs.items(): + if key in sw: + sw[key] = val + else: + raise ValueError(f'Provided {key}={val} but {key} is not in switches.csv') + scriptpath = (sw['reeds_path'] if repo else casedir) + start_year = min(sw['resource_adequacy_years_list']) + ## ReEDS2PRAS runs at hourly resolution + timesteps = sw['num_resource_adequacy_years'] * 8760 + command = ' '.join([ + "julia", + f"--project={sw['reeds_path']}", + ### As of 20231113 there seems to be a problem with multithreading in julia on + ### mac M1 machines that causes multithreaded processes to hang + ### without resolution. So disable multithreading on those systems. + ( + '--threads=1' if (sys.platform == 'darwin') or int(sw.get('pras_singlethread', 0)) + else f"--threads={sw['threads'] if sw['threads'] > 0 else 'auto'}" + ), + f"{os.path.join(scriptpath, 'ReEDS_Augur','run_pras.jl')}", + f"--reeds_path={sw['reeds_path']}", + f"--reedscase={casedir}", + f"--solve_year={t}", + f"--weather_year={start_year}", + f"--timesteps={timesteps}", + f"--hydro_energylim={sw['pras_hydro_energylim']}", + f"--write_flow={int(write_flow)}", + f"--write_surplus={int(write_surplus)}", + f"--write_energy={int(write_energy)}", + f"--write_shortfall_samples={int(write_shortfall_samples)}", + f"--write_availability_samples={int(write_availability_samples)}", + f"--iteration={iteration}", + f"--samples={sw['pras_samples']}", + f"--overwrite={int(overwrite)}", + f"--include_samples={int(include_samples)}", + f"--scheduled_outage={sw['pras_scheduled_outage']}", + f"--pras_agg_ogs_lfillgas={int(sw['pras_agg_ogs_lfillgas'])}", + f"--pras_existing_unit_size={int(sw['pras_existing_unit_size'])}", + f"--pras_max_unitsize_prm={int(sw.get('pras_max_unitsize_prm',1))}", + f"--pras_seed={int(sw['pras_seed'])}", + ]) + print(command) + print(f'vvvvvvvvvvvvvvv run_pras.jl {t}i{iteration} vvvvvvvvvvvvvvv') + log = open(os.path.join(casedir, 'gamslog.txt'), 'a') + result = subprocess.run(command, stdout=log, stderr=log, text=True, shell=True) + log.close() + print(f'^^^^^^^^^^^^^^^ run_pras.jl {t}i{iteration} ^^^^^^^^^^^^^^^') + + if recordtime: + try: + reeds.log.write_last_pras_runtime(year=t) + except Exception as err: + print(err) + + return result + + +#%% Main function +def main(t, tnext, casedir, iteration=0): + + # #%% To debug, uncomment these lines and update the run path + # t = 2026 + # tnext = 2029 + # reeds_path = reeds.io.reeds_path + # casedir = os.path.join( + # reeds_path,'runs','v20250521_prasM0_Pacific') + # iteration = 0 + # assert tnext >= t + # os.chdir(casedir) + # ## Copy reeds2pras from repo to run folder + # import shutil + # shutil.rmtree(os.path.join(casedir, 'reeds2pras')) + # shutil.copytree( + # os.path.join(reeds_path, 'reeds2pras'), + # os.path.join(casedir, 'reeds2pras'), + # ignore=shutil.ignore_patterns('test'), + # ) + + #%% Get run settings + sw = reeds.io.get_switches(casedir) + sw['t'] = t + + #%% Prep data for resource adequacy + print('Preparing data for resource adequacy calculations') + tic = datetime.datetime.now() + augur_csv, augur_h5 = reeds.resource_adequacy.prep_data.main(t, casedir, iteration) + reeds.log.toc(tic=tic, year=t, process='ra/prep_data.py') + + #%% Calculate capacity credit if necessary; otherwise bypass + print('calculating capacity credit...') + tic = datetime.datetime.now() + + if int(sw['GSw_PRM_CapCredit']): + cc_results = reeds.resource_adequacy.capacity_credit.reeds_cc(t, tnext, casedir) + else: + cc_results = { + 'cc_mar': pd.DataFrame(columns=['i','r','ccreg','szn','t','Value']), + 'cc_old': pd.DataFrame(columns=['i','r','ccreg','szn','t','Value']), + 'cc_evmc': pd.DataFrame(columns=['i','r','szn','t','Value']), + 'sdbin_size': pd.DataFrame(columns=['ccreg','szn','bin','t','Value']), + } + + reeds.log.toc(tic=tic, year=t, process='ra/capacity_credit.py') + + #%% Run PRAS if necessary + solveyears = pd.read_csv( + os.path.join(casedir,'inputs_case','modeledyears.csv') + ).columns.astype(int) + pras_this_solve_year = { + 0: False, + 1: True if t == max(solveyears) else False, + 2: True, + }[int(sw['pras'])] + if pras_this_solve_year or int(sw.GSw_PRM_StressIterateMax): + result = run_pras( + casedir, t, + iteration=iteration, + write_flow=(True if t == max(solveyears) else False), + write_energy=True, + write_shortfall_samples=(True if int(sw.GSw_PRM_UpdateMethod) > 1 else False), + ) + if result.returncode: + raise Exception( + f"run_pras.jl returned code {result.returncode}. Check gamslog.txt for error trace." + ) + + #%% Identify stress periods + print('identifying new stress periods...') + tic = datetime.datetime.now() + if ( + ('user' not in sw['GSw_PRM_StressModel'].lower()) + or ((int(sw.GSw_PRM_StressIterateMax)) and int(sw['GSw_PRM_CapCredit'])) + ): + reeds.resource_adequacy.stress_periods.main(sw=sw, t=t, iteration=iteration) + reeds.log.toc(tic=tic, year=t, process='ra/stress_periods.py') + + #%% Write gdx file explicitly to ensure that all entries + ### (even empty dataframes) are written as parameters, not sets + with gdxpds.gdx.GdxFile() as gdx: + for key in cc_results: + gdx.append( + gdxpds.gdx.GdxSymbol( + key, gdxpds.gdx.GamsDataType.Parameter, + dims=cc_results[key].columns[:-1].tolist(), + ) + ) + gdx[-1].dataframe = cc_results[key] + gdx.write( + os.path.join('ReEDS_Augur', 'augur_data', f'ReEDS_Augur_{t}.gdx') + ) + + # #%% Uncomment to run diagnostic_plots + # ### (typically run from call_{}.sh script for parallelization) + # try: + # import ReEDS_Augur.diagnostic_plots as diagnostic_plots + # diagnostic_plots.main(sw) + # except Exception as err: + # print('diagnostic_plots.py failed with the following exception:') + # print(err) + + +#%% Procedure +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description="""Running ReEDS Augur""") + + parser.add_argument("tnext", help="Next ReEDS solve year", type=int) + parser.add_argument("t", help="Previous ReEDS solve year", type=int) + parser.add_argument("casedir", help="Path to ReEDS run") + parser.add_argument('--iteration', '-i', default=0, type=int, + help='iteration number on this solve year') + + args = parser.parse_args() + + tnext = args.tnext + t = args.t + casedir = args.casedir + iteration = args.iteration + + #%% Set up logger + reeds.log.makelog( + scriptname=f'{__file__} {t}-{tnext}', + logpath=os.path.join(casedir,'gamslog.txt'), + ) + + main(t=t, tnext=tnext, casedir=casedir, iteration=iteration) diff --git a/reeds/resource_adequacy/__init__.py b/reeds/resource_adequacy/__init__.py new file mode 100644 index 00000000..92f9aebb --- /dev/null +++ b/reeds/resource_adequacy/__init__.py @@ -0,0 +1,5 @@ +from . import Augur as Augur +from . import capacity_credit as capacity_credit +from . import prep_data as prep_data +from . import ra as ra +from . import stress_periods as stress_periods diff --git a/reeds/resource_adequacy/augur_switches.csv b/reeds/resource_adequacy/augur_switches.csv new file mode 100644 index 00000000..bff65dbe --- /dev/null +++ b/reeds/resource_adequacy/augur_switches.csv @@ -0,0 +1,16 @@ +key,value,dtype,description +cc_all_resources,FALSE,boolean,indicate whether to calculate capacity credit between all pairs of resources and regions (TRUE) or just for resources within region (FALSE) +cc_ann_hours,20,int,number of top hours considered in annual cc calculations +cc_calc_annual,FALSE,boolean,when true: annual cc values are calculated +cc_calc_seasonal,TRUE,boolean,when true: seasonal cc values are calculated +cc_default_rte,0.85,float,default efficiency value to use for assessing peaking storage potential +cc_marg_evmc_mw,100,int,step size used for marginal DR cc calculations +cc_max_stor_pen,0.9,float,max fraction of peak load considered for storage peaking capacity assessment +cc_safety_bin_size,100000,int,default value (in MW) for the safety bin size in ReEDS +cc_stor_buffer,60,int,additional duration (in minutes) that is required of storage to receive full capacity credit +cc_stor_stepsize,100,int,step size (in MW) used when determining the peaking capacity potential of storage +decimals,3,int,number of decimals to round results to for ReEDS +flex_consume_techs,"dac,electrolyzer",list,list of consume techs that are flexible +keepfiles,"dropped_load,cf",list,list of temporary files to keep +marg_vre_steps,2,int,Number of previous solve years to consider when evaluating the marginal VRE step size (default: 2). Must be at least 1; a value of 2 can help reduce oscillations. Augur will automatically drop from consideration solves that are more than 5 years from the previous solve. +storcap_cutoff,1,float,[MW and MWh] Minimum storage capacity to send to ReEDS2PRAS (applies to both power and energy capacity) diff --git a/reeds/resource_adequacy/capacity_credit.py b/reeds/resource_adequacy/capacity_credit.py new file mode 100644 index 00000000..d778d6da --- /dev/null +++ b/reeds/resource_adequacy/capacity_credit.py @@ -0,0 +1,862 @@ +#%% IMPORTS +import os +import numpy as np +import pandas as pd +import gdxpds +import reeds + + +#%% Functions +def get_relative_step_sizes(t, yearset, target_step): + ''' + Checking the relative ReEDS temporal step sizes for this solve year and + any previous solve year, specified by 'target_step' + ''' + i = yearset.index(t) + tnext = yearset[i+1] + + j = yearset.index(target_step) + targ_prev = yearset[j-1] + + relative_step_sizes = (tnext - t) / (target_step - targ_prev) + return relative_step_sizes + + +def set_marg_vre_step_size(t, sw, gdx, hierarchy): + ''' + Marginal vre step size has a default floor value of 1000 MW but + here we check to see if it needs to be higher. The function looks + back by the number of steps specified by 'marg_vre_steps' and computes + the max of the average new vre investment in those previous steps. + We take the max of that value and 1000 MW to set the set size. + The fuction also accounts for potentially varying step sizes in ReEDS. + + Inputs + * marg_vre_steps [int]: Number of previous solve years to consider when + evaluating the marginal VRE step size (default: 2). Must be at least 1; + a value of 2 can help reduce oscillations. Augur will automatically drop + from consideration solves that are more than 5 years from the previous solve. + ''' + # load yearset for getting various previous steps + yearset = gdx['tmodel_new'].allt.astype(int).tolist() + + # collect list of previous years and their relative step sizes + prev_year_list = [] + step_sizes = [] + for step in range(int(sw['marg_vre_steps'])): + + # try-except to handle cases where there aren't multiple + # steps to go back to (e.g. running Augur after 1st solve) + try: + target_last_step = yearset[yearset.index(t)-step] + + # only look at steps beyond the previous year if the step sizes + # are less than 5 years + if (t - target_last_step) < 5: + step_sizes.append(get_relative_step_sizes(t, yearset, target_last_step)) + prev_year_list.append(target_last_step) + except Exception: + print('First Augur year so no previous steps') + + relative_step_sizes = pd.DataFrame(list(zip(prev_year_list, step_sizes)), + columns=['t', 'step']) + + # load investment data for all techs + techs = gdx['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') + inv = gdx['inv_ivrt'].astype({'t':int}) + + # get investment from any previous steps under consideration + inv_last_years = inv[inv['t'].isin(prev_year_list)] + inv_vre = inv_last_years[inv_last_years['i'].isin(techs['VRE'].dropna().index)] + + # map inv_vre to ccregions + r_ccreg = hierarchy[['r','ccreg']].drop_duplicates() + inv_vre = inv_vre.merge(r_ccreg, on = 'r') + + # aggregate by tech and then and compute average across the appropriate + # geographic resolution - r for curtailment, ccreg for capacity credit + level = 'ccreg' + df = ( + inv_vre.groupby([level, 't'], as_index=False).Value.sum() + .groupby(['t'], as_index=False).Value.mean() + ) + # adjust each previous step by its relative step size + df = df.merge(relative_step_sizes, on='t') + df['Value'] *= df['step'] + + # now get max across all previous steps and set as marg_vre_mw + marg_vre_mw = round(df['Value'].max(), 0) + + marg_vre_mw_cc = int(max(int(sw['marg_vre_mw']), marg_vre_mw)) + print(f'marg_vre_mw_cc set to {marg_vre_mw_cc}') + + return marg_vre_mw_cc + +def load_evmc_data(csv_path,inputs_case,h_dt_szn, + set_h_szn_cols=['h','ccseason','hour'], + set_idx_cols=['h','hour', 'year','ccseason']): + df = pd.read_csv(os.path.join(inputs_case,csv_path)) + df = pd.merge(df,h_dt_szn[set_h_szn_cols],on='hour',how='left') + return df.set_index(set_idx_cols) + + +#%% Main function +def reeds_cc(t, tnext, casedir): + ''' + This function directs all of the capacity credit calculations for ReEDS + It writes out a gdx file which is then read back in to ReEDS during the + next iteration. + ''' + #%% Get the switches + sw = reeds.io.get_switches(casedir) + + #%% Set up log + log = reeds.log.makelog( + 'capacity_credit.py', + os.path.join(sw['casedir'], 'gamslog.txt'), + ) + + #%% Load some inputs + inputs_case = os.path.join(casedir, 'inputs_case') + hierarchy = reeds.io.get_hierarchy(casedir).reset_index() + resources = pd.read_csv(os.path.join(inputs_case, 'resources.csv')) + + augur_data = os.path.join(casedir,'ReEDS_Augur','augur_data') + cap = pd.read_csv(os.path.join(augur_data, f'max_cap_{t}.csv')) + + gdx = gdxpds.to_dataframes(os.path.join(augur_data,f'reeds_data_{t}.gdx')) + techs = gdx['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') + techs.columns = techs.columns.str.lower() + r = gdx['rfeas'] + + cap_stor = cap.loc[cap['i'].isin(gdx['storage_standalone'].i) & + ~cap['i'].isin(gdx['i_subsets'][gdx['i_subsets']['i_subtech'] == 'CONTINUOUS_BATTERY'].i)] \ + .rename(columns={'Value':'MW'}) + cap_stor['duration'] = cap_stor.i.map(gdx['storage_duration'].set_index('i').Value) + cap_stor['MWh'] = cap_stor['MW'] * cap_stor['duration'] + #Adding a check if there is no storage - populate with 0 MW and 0 MWh in each r + if cap_stor.empty: + stor_techs = gdx['storage_standalone'].i.tolist() + r_values = r['r'].tolist() + for tech_name in stor_techs: + for r_val in r_values: + cap_stor.loc[len(cap_stor)] = [tech_name,'', r_val, 0, 0, 0] + cap_stor['duration'] = cap_stor.i.map(gdx['storage_duration'].set_index('i').Value) + + cap_stor_agg = cap_stor.merge(hierarchy[['r','ccreg']], on='r') + cap_stor_agg = cap_stor_agg.groupby('ccreg', as_index=False)[['MW','MWh']].sum() + sdb = gdx['sdbin'].rename(columns={'*':'bin'})[['bin']] + + ### Get the marginal step size + marg_vre_mw_cc = set_marg_vre_step_size(t, sw, gdx, hierarchy) + + ### Get the non-duplicated profiles + resource_profiles = resources.drop_duplicates('resource') + + # Remove the "8760" safety valve bin + safety_bin = max(sdb['bin'].values) + sdb = sdb[sdb['bin'] != max(sdb['bin'])] + sdb = [int(x) for x in sdb['bin']] + + # Temporal definitions + h_dt_szn = pd.read_csv(os.path.join('inputs_case', 'rep', 'h_dt_szn.csv')) + + ccseasons = [] + if sw['cc_calc_annual']: + ccseasons += ['year'] + if sw['cc_calc_seasonal']: + ccseasons += h_dt_szn['ccseason'].drop_duplicates().tolist() + + ### Prepare the seasonal profiles + ## vre_gen needs to have tech_class_r columns + ## last version has (ccseason,year,h,hour) index + vre_gen = pd.read_hdf(os.path.join(augur_data,f'vre_gen_exist_{t}.h5')) + ## vre_cf_marg has same columns and index as vre_gen + vre_cf_marg = pd.read_hdf(os.path.join(augur_data,f'vre_cf_marg_{t}.h5')) + + if int(sw['GSw_PRM_CapCreditMulti']) == 0: + # Restrict capacity credit evaluation to use 2012 only (rather than multi-year) + vre_gen = vre_gen[vre_gen.index.get_level_values('year') == 2012].copy() + vre_cf_marg = vre_cf_marg[vre_cf_marg.index.get_level_values('year') == 2012].copy() + + vregen_ccseason = {} + vregen_marginal_ccseason = {} + for ccseason in ccseasons: + if ccseason == 'year': + vregen_ccseason[ccseason] = vre_gen + vregen_marginal_ccseason[ccseason] = vre_cf_marg * marg_vre_mw_cc + else: + vregen_ccseason[ccseason] = vre_gen.loc[ccseason] + vregen_marginal_ccseason[ccseason] = (vre_cf_marg * marg_vre_mw_cc).loc[ccseason] + + load_profiles = ( + # HOURLY_PROFILES['load'].profiles + pd.read_hdf(os.path.join(augur_data,f'load_{t}.h5')) + ### Map BA regions to ccreg's and sum over them + .rename(columns=hierarchy.set_index('r').ccreg) + .groupby(axis=1, level=0).sum() + ) + + if int(sw['GSw_PRM_CapCreditMulti']) == 0: + # Restrict capacity credit evaluation to use 2012 only (rather than multi-year) + load_profiles = load_profiles[load_profiles.index.get_level_values('year') == 2012].copy() + + # Get EVMC data if necessary + if int(sw['GSw_EVMC']): + # Get EVMC props + evmccf_shape_increase = load_evmc_data('evmc_shape_profile_increase.csv',inputs_case,h_dt_szn) + evmccf_shape_decrease = load_evmc_data('evmc_shape_profile_decrease.csv',inputs_case,h_dt_szn) + + # Initialize dataframes to store results + dict_cc_old = {} + dict_cc_mar = {} + dict_sdbin_size = {} + dict_cc_evmc = {} + dict_net_load = {} + dict_net_load_2012 = {} + + #%% Loop over CCREGs + for ccreg in hierarchy['ccreg'].drop_duplicates(): + #% CCREG DATA + # ccreg = 'cc6' # Uncomment for debugging + # ------- Get load profile, RECF profiles, VG capacity, storage + # capacity, and storage RTE for this CCREG ------- + + log.info('Calculating capacity credit for {}'.format(ccreg)) + + # Resources to be used + resources_ccreg = resource_profiles[resource_profiles['ccreg'] == ccreg] + resourcelist = ( + slice(None) if sw['cc_all_resources'] + else resources_ccreg.resource.tolist() + ) + + # Hourly profiles + load_profile_ccreg = load_profiles[ccreg] + + # EVMC profile + if int(sw['GSw_EVMC']): + evmc_shape_reg = [r for r in resources_ccreg.r.drop_duplicates() + if r in evmccf_shape_increase.columns] + evmccf_shape_increase_ccreg = evmccf_shape_increase[['i'] + evmc_shape_reg] + evmc_shape_reg = [r for r in resources_ccreg.r.drop_duplicates() + if r in evmccf_shape_decrease.columns] + evmccf_shape_decrease_ccreg = evmccf_shape_decrease[['i'] + evmc_shape_reg] + + # Storage information + # Note that we only calculate storage capacity credit for storage in the same ccreg + cap_stor_agg_ccreg = cap_stor_agg[ + cap_stor_agg['ccreg'] == ccreg].reset_index(drop=True) + + cap_stor_ccreg = cap_stor[ + cap_stor['r'].isin(hierarchy[hierarchy['ccreg'] == ccreg]['r']) + ].reset_index(drop=True) + # df = cap_stor.assign(ccreg=cap_stor.r.map(hierarchy.set_index('r').ccreg)) + # df.groupby('ccreg').MW.max() + # df.groupby('ccreg').MWh.max() + + try: + eff_charge = cap_stor_agg_ccreg['rte'].values[0] + except Exception: + eff_charge = float(sw['cc_default_rte']) + + max_demand = load_profile_ccreg.max() / (1/float(sw['cc_max_stor_pen'])) + reductions_considered = int(max_demand // float(sw['cc_stor_stepsize'])) + peak_reductions = np.linspace(0, max_demand, reductions_considered) + #Skip CC calculation if number of reductions_considered is only 1. Avoids error in interpolation within cc_storage function. + if reductions_considered == 1: + continue + + # log.debug(f'max_demand = {max_demand}') + # log.debug(f'reductions_considered = {reductions_considered}') + # log.debug(f'peak_reductions diff = {peak_reductions[1] - peak_reductions[0]}') + + # ---------------------------- CALL FUNCTIONS ------------------------- + #%% Loop over ccseasons + for ccseason in ccseasons: + #%% + # ccseason = 'winter' # Uncomment for debugging + # Get the load and CF profiles for this ccseason + if ccseason == 'year': + load_profile_ccseason = load_profile_ccreg.copy() + hours_considered = int(sw['cc_ann_hours']) + if int(sw['GSw_EVMC']): + evmc_shape_load_ccseason = evmccf_shape_increase_ccreg.copy() + evmc_shape_gen_ccseason = evmccf_shape_decrease_ccreg.copy() + + else: + load_profile_ccseason = load_profile_ccreg.xs( + ccseason, axis=0, level='ccseason').reset_index() + hours_considered = int(sw['GSw_PRM_CapCreditHours']) + + if int(sw['GSw_EVMC']): + evmc_shape_load_ccseason = evmccf_shape_increase_ccreg.xs( + ccseason, axis=0, level='ccseason').reset_index() + evmc_shape_gen_ccseason = evmccf_shape_decrease_ccreg.xs( + ccseason, axis=0, level='ccseason').reset_index() + + # log.debug(ccseason, int(len(load_profile_ccseason) / 7)) + ###### Calculate the capacity credit for each resource + cc_vg_results = cc_vg( + vg_power=vregen_ccseason[ccseason][resourcelist].values, + load=load_profile_ccseason[ccreg].values, + vg_marg_power=vregen_marginal_ccseason[ccseason][resourcelist].values, + top_hours_n=hours_considered, cap_marg=marg_vre_mw_cc) + + ###### Store the existing and marginal capacity credit results + dict_cc_old[ccreg, ccseason] = pd.DataFrame({ + 'resource': resource_profiles.set_index('resource').loc[resourcelist].index, + 'MW': cc_vg_results['cap_useful_MW'][:,0], + }) + + dict_cc_mar[ccreg, ccseason] = ( + resource_profiles.loc[resource_profiles.resource.isin(resourcelist)] + .drop('ccreg', axis=1) + .assign(CC=cc_vg_results['cc_marg']) + ) + + net_load_ccreg_ccseason = ( + load_profile_ccseason.drop(columns=ccreg) + .assign(MW=cc_vg_results['load_net']) + .sort_values(['MW'], ascending=False) + ) + #Save top n hrs of net load for ccreg and ccseason across all years, and for 2012 alone + net_load_out_numhrs = 500 + dict_net_load[ccreg, ccseason] = net_load_ccreg_ccseason.head(net_load_out_numhrs) + dict_net_load_2012[ccreg, ccseason] = ( + net_load_ccreg_ccseason[net_load_ccreg_ccseason['year']==2012].head(net_load_out_numhrs) + ) + + ###### Calculate the storage capacity credit + # The call to this function gives the MWh required for each + # peak reduction capacity. For each data year, loop through + # and get get the MWh needed for each peak reduction capacity. + # Get a "ccseason_required_MWhs" for each year. + # Get the maximum value for each position in the array. + # Make a new "ccseason_required_MWhs" array to send to the + # cc_storage function. + # Call storage cc functions for existing and marginal + # conventional storage. + net_load_profile_timestamp = pd.DataFrame( + cc_vg_results['load_net'], load_profile_ccseason.year) + years = list(net_load_profile_timestamp.index.unique()) + + for y in years: + net_load_profile_temp = net_load_profile_timestamp.iloc[ + :, 0][net_load_profile_timestamp.index == y].to_numpy() + required_MWhs_temp, batt_powers = calc_required_mwh( + load_profile=net_load_profile_temp.copy(), + peak_reductions=peak_reductions.copy(), + eff_charge=eff_charge, stor_buffer_minutes=float(sw['cc_stor_buffer'])) + + if years.index(y) == 0: + required_MWhs = required_MWhs_temp.copy() + else: + required_MWhs = np.maximum(required_MWhs, required_MWhs_temp) + + # Get the peaking storage potential by duration + peaking_stor = cc_storage( + pr=peak_reductions.copy(), + re=required_MWhs.copy(), + sdb=sdb.copy(), log=log) + + # Store the storage capacity credit along with the energy capacity. + # For the safety bin, compute MWh as safety_bin * cc_safety_bin_size. + dict_sdbin_size[ccreg, ccseason] = pd.concat([ + peaking_stor[['duration', 'MW']], + pd.DataFrame({ + 'duration': [safety_bin], + 'MW': float(sw['cc_safety_bin_size']) + }), + ], ignore_index=True) + + def pivot_melt_data(df): + return pd.pivot_table( + pd.melt(df, + id_vars=['h','year','hour','i'], + var_name='r'), + index=['year','hour','h'], + columns=['i','r'], values='value') + + if int(sw['GSw_EVMC']): + evmc_shape_inc_timestamp = pivot_melt_data(evmc_shape_load_ccseason) + evmc_shape_dec_timestamp = pivot_melt_data(evmc_shape_gen_ccseason) + evmc_years = evmc_shape_dec_timestamp.index.get_level_values('year').unique() + #do as evmc cap credit instead? + if len(evmc_years)==1: + gen_array = evmc_shape_dec_timestamp.values - evmc_shape_inc_timestamp.values + evmc_shape_marg_power = np.tile(gen_array,(7,1)) + elif len(evmc_years)==7: + evmc_shape_marg_power = evmc_shape_dec_timestamp.values - evmc_shape_inc_timestamp.values + else: + log.info("no weather year data on EVMC for any relevant regions; skipping") + continue + + ###### Calculate the capacity credit for each evmc_shape resource + results_evmc_shape = cc_evmc_shape(load=cc_vg_results['load'], + load_net=cc_vg_results['load_net'], + top_hours_net=cc_vg_results['top_hours_net'], + top_hours_n=hours_considered, + evmc_shape_marg_power=evmc_shape_marg_power*float(sw['marg_evmc_mw']), + cap_marg=float(sw['marg_evmc_mw'])) + + evmc_cc_i = pd.melt(pd.DataFrame(data=[np.round(results_evmc_shape, decimals=5), ], + columns=evmc_shape_dec_timestamp.columns)) + + if (ccreg, ccseason) in dict_cc_evmc.keys(): + dict_cc_evmc[ccreg, ccseason] = pd.concat( + [dict_cc_evmc[ccreg, ccseason], + evmc_cc_i[['r', 'i', 'value']]]) + else: + dict_cc_evmc[ccreg, ccseason] = evmc_cc_i[['r', 'i', 'value']] + + # ------ AGGREGATE OUTPUTS ------ + cc_old = ( + pd.concat(dict_cc_old, axis=0) + ### Drop the ccreg and numbered indices + .reset_index().drop(['level_2'], axis=1) + .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'MW':'value'}) + .assign(t=str(tnext)) + .merge(resources.drop('ccreg',axis=1), on='resource', how='left') + ) + ### Reorder to match ReEDS convention + cc_old = cc_old.reindex(['i','r','ccreg','ccseason','t','value'], axis=1) + + sdbin_size = ( + pd.concat(dict_sdbin_size, axis=0) + ### Keep the ccreg and ccseason indices but drop the numbered index + .reset_index().drop('level_2', axis=1) + .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'duration':'bin'}) + .astype({'bin':str}) + .assign(t=str(tnext)) + .reindex(['ccreg','ccseason','bin','t','MW'], axis=1) + ) + + cc_mar = ( + pd.concat(dict_cc_mar, axis=0) + .reset_index().drop('level_2', axis=1) + .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'CC':'value'}) + .assign(t=str(tnext)) + ) + ### Reorder to match ReEDS convention + cc_mar = cc_mar.reindex(['i','r','ccreg','ccseason','t','value'], axis=1) + + net_load = ( + pd.concat(dict_net_load, axis=0) + .reset_index().drop(['level_2'], axis=1) + .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'MW':'value'}) + ### Rename seasons to match ReEDS convention and add year index + .replace({'winter':'wint', 'spring':'spri', 'summer':'summ'}) + .assign(t=str(tnext)) + .sort_values(['ccreg','hour']) + ) + ### Reorder to match ReEDS convention + net_load = net_load.reindex(['ccreg','ccseason','year','h','hour','t','value'], axis=1) + + net_load_2012 = ( + pd.concat(dict_net_load_2012, axis=0) + .reset_index().drop(['level_2'], axis=1) + .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'MW':'value'}) + ### Rename seasons to match ReEDS convention and add year index + .replace({'winter':'wint', 'spring':'spri', 'summer':'summ'}) + .assign(t=str(tnext)) + .sort_values(['ccreg','hour']) + ) + ### Reorder to match ReEDS convention + net_load_2012 = net_load_2012.reindex(['ccreg','ccseason','year','h','hour','t','value'], axis=1) + + if int(sw['GSw_EVMC']): + cc_evmc = ( + pd.concat(dict_cc_evmc, axis=0) + .reset_index().drop(['level_2', 'level_0'], axis=1) + .rename(columns={'level_1':'ccseason'}) + .assign(t=str(tnext)) + .reindex(['i','r','ccseason','t','value'], axis=1) + ) + else: + cc_evmc = pd.DataFrame(columns=['i', 'r', 'ccseason', 't', 'value']) + + # ---------------- RETURN A DICTIONARY WITH THE OUTPUTS FOR REEDS -------- + + cc_results = { + 'cc_mar': cc_mar, + 'cc_old': cc_old, + 'cc_evmc': cc_evmc, + 'sdbin_size': sdbin_size, + 'net_load': net_load, + 'net_load_2012': net_load_2012, + } + + return cc_results + +#%% Additional functions +# ------------------ CALC CC OF EXISTING VG RESOURCES ------------------------- +# @numba.jit(cache=True) +def cc_vg(vg_power, load, vg_marg_power, top_hours_n, cap_marg): + ''' + Calculate the capacity credit of existing and marginal variable generation + capacity using a top hour approximation. More details on the methodology + used in this approximation can be found here: + //nrelnas01/ReEDS/8760_Method_Inputs/8760 Method Documentation + Args: + vg_power: numpy matrix containing power output profiles for all + hours_n for each variable generating resource + load: numpy array containing time-synchronous load profile for all + hours_n. Units: MW + cf_marg: numpy array containing capacity factor profiles for marginal + builds of each variable generating resource + top_hours_n: number of top hours to consider for the calculation + cap_marg: marginal capacity used to calculate marginal capacity credit + Returns: + cc_marg: marginal capacity credit for each variable generating resource + load_net: net load profile. Units: MW + top_hours_net: arguments for the highest net load hours in load_net, + length top_hours_n + top_hours: argumnets for the highest load hours in load, length + top_hours_n + Notes: + Currently only built for hourly profiles. Generalize to any duration + timestep. + ''' + + # number of hours in the load and CF profiles + hours_n = len(load) + + # get the net load that must be met with conventional generation + load_net = load - np.sum(vg_power, axis=1) + + # get the indices of the top hours of net load + top_hours_net = load_net.argsort()[np.arange(hours_n-1, (hours_n-top_hours_n)-1, -1)] + + # get the indices of the top hours of load + top_hours = load.argsort()[np.arange(hours_n-1, (hours_n-top_hours_n)-1, -1)] + + # get the differences and reductions in load as well as the ratio between the two + + # load_ratio is the effective reduction in load from wind and PV for each top load + # hour, and is used to scale the contributions of wind and PV respectively + # see slide 5 of "\\nrelnas01\ReEDS\8760_Method_Inputs\8760 Method Documentation\ + # VG Capacity credit allocation documentation.pptx" for additional details + load_dif = load[top_hours] - load_net[top_hours] + load_reduct = load[top_hours] - load_net[top_hours_net] + load_ratio = np.tile( + np.divide( + load_reduct, load_dif, + out=np.zeros_like(load_reduct), + where=load_dif != 0, + ).reshape(top_hours_n, 1), + (1, vg_power.shape[1]) + ) + # get the existing cc for each resource + gen_tech = ( + vg_power[top_hours_net, :] + + np.where( + load_ratio < 1, + vg_power[top_hours, :]*load_ratio, + vg_power[top_hours, :])) + + gen_sum = np.tile( + np.sum(gen_tech, axis=1).reshape(top_hours_n, 1), + (1, vg_power.shape[1])) + + gen_frac = np.divide( + gen_tech, gen_sum, + out=np.zeros_like(gen_tech), where=gen_sum != 0) + + cap_useful_MW = ( + np.sum( + gen_frac + * np.tile(load_reduct.reshape(top_hours_n, 1), (1, vg_power.shape[1])), + axis=0) + / top_hours_n + ).reshape(vg_power.shape[1], 1) + + # get the marg net load for each VG resource [hours x resources] + load_marg = ( + np.tile(load_net.reshape(hours_n, 1), (1, vg_marg_power.shape[1])) + - vg_marg_power) + + ### Get the peak net load hours [top_hours_n x resources] + peak_net_load = np.transpose(np.array( + ### np.partition returns the max top_hours_n values, unsorted; then np.sort sorts. + ### So we only sort top_hours_n values instead of the whole array, saving time. + [np.sort( + np.partition(load_marg[:,n], -top_hours_n)[-top_hours_n:] + )[::-1] + for n in range(load_marg.shape[1])] + )) + + # get the reductions in load for each resource + load_reduct_marg = np.tile( + load_net[top_hours_net].reshape(top_hours_n, 1), + (1, vg_marg_power.shape[1]) + ) - peak_net_load + + # get the marginal CCs for each resource + cc_marg = np.sum(load_reduct_marg, axis=0) / top_hours_n / cap_marg + + # setting the lower bound for marginal CC to be 0.01 + cc_marg[cc_marg < 0.01] = 0.0 + + # round the outputs + load_net = np.around(load_net, decimals=3) + cc_marg = np.around(cc_marg, decimals=5) + cap_useful_MW = np.around(cap_useful_MW, decimals=5) + + results = { + 'load': load, + 'load_net': load_net, + 'cc_marg': cc_marg, + 'cap_useful_MW': cap_useful_MW, + 'top_hours_net': top_hours_net, + 'peak_net_load': peak_net_load + } + + return results + +def cc_evmc_shape(load,load_net,top_hours_net,top_hours_n,evmc_shape_marg_power,cap_marg): + ''' + Calculate the capacity credit of marginal evmc_shape resources + using a top hour approximation. + Args: + load: numpy array containing time-synchronous load profile for all + hours_n. Units: MW + load_net: net load profile. Units: MW + top_hours_net: arguments for the highest net load hours in load_net, + calculated in cc_vg(). Is of length top_hours_n + top_hours_n: number of top hours to consider for the calculation + evmc_shape_marg: numpy array containing capacity factor profiles for marginal + builds of each evmc_shape resource bin + cap_marg: marginal capacity used to calculate marginal capacity credit + Returns: + cc_marg: marginal capacity credit for each evmc_shape resource + Notes: + Currently only built for hourly profiles. Generalize to any duration + timestep. + ''' + hours_n = len(load) + + # get the marg net load for each evmc_shape resource [hours x resources] + load_marg = ( + np.tile(load_net.reshape(hours_n, 1), (1, evmc_shape_marg_power.shape[1])) + - evmc_shape_marg_power) + + ### Get the peak net load hours [top_hours_n x evmc_shape resources] + peak_net_load = np.transpose(np.array( + ### np.partition returns the max top_hours_n values, unsorted; then np.sort sorts. + ### So we only sort top_hours_n values instead of the whole array, saving time. + [np.sort( + np.partition(load_marg[:,n], -top_hours_n)[-top_hours_n:] + )[::-1] + for n in range(load_marg.shape[1])] + )) + + load_reduct_marg = np.tile( + load_net[top_hours_net].reshape(top_hours_n, 1), + (1, evmc_shape_marg_power.shape[1]) + ) - peak_net_load + # get the marginal CCs for each resource + cc_marg = np.sum(load_reduct_marg, axis=0) / top_hours_n / cap_marg + + # setting the lower bound for marginal CC to be 0.01 + cc_marg[cc_marg < 0.01] = 0.0 + return cc_marg + +# -------------------------CALC REQUIRED MWHS---------------------------------- +# @numba.jit(nopython=True, cache=True) +def calc_required_mwh(load_profile, peak_reductions, eff_charge, stor_buffer_minutes): + ''' + Determine the energy storage capacity required to acheive a certain peak + load reduction for a given load profile + Args: + load_profile: time-synchronous load profile + peak_reductions: set of peak reductions (in MW) to be tested + eff_charge: RTE of charging + Returns: + required_MWhs: set of energy storage capacities required for each peak + reduction size + batt_powers: corresponding peak reduction sizes for required_MWhs + ''' + + hours_n = len(load_profile) + + inc = len(peak_reductions) + max_demands = np.tile( + (np.max(load_profile) - peak_reductions).reshape(inc, 1), (1, hours_n)) + batt_powers = np.tile(peak_reductions.reshape(inc, 1), (1, hours_n)) + + poss_charges = np.minimum(batt_powers * eff_charge, + (max_demands - load_profile) * eff_charge) + necessary_discharges = (max_demands - load_profile) + + poss_batt_changes = np.where(necessary_discharges <= 0, + necessary_discharges, poss_charges) + + batt_e_level = np.zeros([inc, hours_n]) + batt_e_level[:, 0] = np.minimum(poss_batt_changes[:, 0], 0) + for n in np.arange(1, hours_n): + batt_e_level[:, n] = batt_e_level[:, n-1] + poss_batt_changes[:, n] + batt_e_level[:, n] = np.clip(batt_e_level[:, n], a_min=None, + a_max=0.0, out=batt_e_level[:, n]) + + required_MWhs = -np.min(batt_e_level, axis=1) + + # 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[:, 0] * stor_buffer_hrs) + + return required_MWhs, batt_powers + + +# --------------------- CALC CC OF MARGINAL STORAGE --------------------------- +def cc_storage(pr, re, sdb, log=None): + """ + Determine the theoretical peaking capacity (MW) for incrementally increasing + storage durations, based on the MW–MWh curve (pr, re). + + Args: + pr (array-like): + Array of storage power capacities (MW) analyzed. + re (array-like): + Array of corresponding energy capacities (MWh). + sdb (list or array-like): + Storage duration bins (in hours) to evaluate (e.g., [2,4,6,8,...]). + + Returns: + pd.DataFrame with columns: + - MW: The theoretical peaking potential for each bin. + """ + # Initializing terms + ds = sdb.copy() + min_bin = min(ds) + ds.remove(min_bin) + peak_stor = pd.DataFrame(columns=['peaking potential', 'existing power']) + + # Get the step size and make a smaller step size for interpolation + p_step = (pr[1] - pr[0]) + rel_step = 100 + p_step_small = p_step / rel_step + + # Get duration and marginal duration as a function of storage penetration + dur = np.zeros(len(pr)) + dur[1:] = re[1:] / pr[1:] + dur = dur.round(3) + dur_marg = np.zeros(len(pr)) + for i in range(1, len(pr)): + dur_marg[i] = ((re[i] - re[i-1]) / (pr[i] - pr[i-1])) + dur_marg = dur_marg.round(3) + + # ---------------------------------------------------------------- + # 1) Determine peaking potential for the smallest duration bin + # ---------------------------------------------------------------- + + # Find the limit of 2-hour storage capacity + dur_temp = dur[dur <= min_bin].copy() + dur_marg_temp = dur_marg[dur_marg < float(min(ds))].copy() + + + # Case 1: Storage potential bleeds into the next bin's marginal addition + if len(dur_marg_temp) < len(dur_temp): + peak_stor.loc[min_bin, 'peaking potential'] = pr[len(dur_marg_temp)-1] + + # Case 2: There's storage potential for the smallest bin + elif len(dur_temp) > 1: + # If the marginal duration is acceptable at the crossover point, find + # the crossover point. + lower_bound_p = pr[len(dur_temp) - 1] + upper_bound_p = pr[len(dur_temp)] + lower_bound_e = re[len(dur_temp) - 1] + upper_bound_e = re[len(dur_temp)] + min_p = np.linspace(lower_bound_p, upper_bound_p, (rel_step**2) + 1) + min_e = np.linspace(lower_bound_e, upper_bound_e, (rel_step**2) + 1) + min_dur = min_e / min_p + min_dur_temp = min_dur[min_dur <= min_bin].copy() + # If the duration is already the min duration, don't interpolate + if len(min_dur_temp) == 0: + peak_stor.loc[min_bin,'peaking potential'] = lower_bound_p + else: + # Find the max addition that could be made without exceeding the + # marginal duration limit. + dur_marg_test = min(min(ds), dur_marg[len(dur_temp)]) + max_interp = ((p_step * (min(ds) - dur_marg_test)) + / (min(ds) - min_bin)) + lower_bound_p + # Set the peaking potential for the lowest bin to be the minimum + # between the crossover point and the maximum allowed interpolated + # value (limited by the marg duration and p_step size). + peak_stor.loc[min_bin, 'peaking potential'] = min( + max_interp, min_p[len(min_dur_temp) - 1]) + + # Case 3: No storage potential for the smallest bin + elif len(dur_temp) == 1: + peak_stor.loc[min_bin, 'peaking potential'] = 0 + + # ---------------------------------------------------------------- + # 2) Determine peaking potential for the remaining duration bins + # ---------------------------------------------------------------- + # Iterate through the rest of the storage duration bins to find the + # peaking potential. + for i in range(0, len(ds)): + d = ds[i] + try: + d1 = ds[i+1] + except Exception: + d1 = d * 2 + e_base = 0 + p_base = 0 + for key in peak_stor.index: + e_base += peak_stor.loc[key, 'peaking potential'] * key + p_base += peak_stor.loc[key, 'peaking potential'] + # First check to see if this bin size will be limited by marginal + # duration. + dur_marg_temp = dur_marg[dur_marg < float(d1)].copy() + p_temp = pr[len(dur_marg_temp) - 1] - p_base + e_temp = e_base + (p_temp * d) + e_test = re[len(dur_marg_temp) - 1] + if e_test <= e_temp: + peak_stor.loc[d, 'peaking potential'] = p_temp + else: + # Now add small incremental capacity until we reach the crossover + # point + error = 0 + p = p_base + condition = True + while condition: + p_test = p + p_step_small + e_test = e_base + ((p_test - p_base) * d) + if np.interp(p_test, pr, re) >= e_test: + condition = False + else: + p += p_step_small + error += 1 + if error > 1e7: + log.info(d) + condition = False + log.info('**** Runaway while loop in capacity_credit.py') + # Find the max addition that could be made without exceeding the + # marginal duration limit + pr_temp = pr[pr <= p] + dur_marg_test = dur_marg[len(pr_temp) - 1] + max_interp = ((p_step * (d1 - dur_marg_test)) + / (d1 - d)) + pr_temp[-1] + # Set the peaking potential to be the minimum of the crossover + # point and maximum interpolation value (limited by the marg + # duration and p_step size). + peak_stor.loc[d, 'peaking potential'] = min(max_interp, p) - p_base + + peak_stor['existing power'] = 0 + + # ---------------------------------------------------------------- + # 3) Prepare final output: duration and MW + # ---------------------------------------------------------------- + peak_stor['peaking potential'] = pd.to_numeric(peak_stor['peaking potential']) + result = ( + peak_stor[['peaking potential']] + .round(decimals=2) + .reset_index() + .rename(columns={'index': 'duration', 'peaking potential': 'MW'}) + ) + + return result diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py new file mode 100644 index 00000000..61523955 --- /dev/null +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -0,0 +1,1359 @@ +#%%### Imports +import os +import sys +import pandas as pd +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib import patheffects as pe +from glob import glob +import traceback +import gdxpds +import cmocean +### Local imports +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) +import reeds +from reeds import plots + +plots.plotparams() + + +#%%### Fixed inputs +dpi = None +interactive = False +savefig = True + + +#%%### Functions +def delete_temporary_files(sw): + """ + Delete temporary csv, pkl, and h5 files + """ + dropfiles = ( + glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.pkl")) + + glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.h5")) + + glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.csv")) + + glob(os.path.join(sw['casedir'],'ReEDS_Augur','PRAS',f"PRAS_{sw['t']}*.pras")) + ) + + for keyword in sw['keepfiles']: + dropfiles = [f for f in dropfiles if not os.path.basename(f).startswith(keyword)] + + for f in dropfiles: + os.remove(f) + +#%% Input-loading function +def get_inputs(sw): + ### Make savepath + sw['savepath'] = os.path.join(sw['casedir'], 'outputs', 'Augur_plots') + os.makedirs(sw['savepath'], exist_ok=True) + + ##### Load shared parameters + fulltimeindex = reeds.timeseries.get_timeindex(sw.resource_adequacy_years) + + h_dt_szn = ( + pd.read_csv(os.path.join(sw['casedir'], 'inputs_case', 'rep', 'h_dt_szn.csv')) + .assign(hr=(['hr{:>03}'.format(i+1) for i in range(sw['hoursperperiod'])] + * sw['periodsperyear'] * len(sw.resource_adequacy_years_list))) + .assign(datetime=fulltimeindex) + ) + h_dt_szn['d'] = h_dt_szn.datetime.dt.strftime('sy%Yd%j') + + gdxreeds = gdxpds.to_dataframes( + os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'reeds_data_{sw["t"]}.gdx')) + + techs = gdxreeds['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') + h2dac = techs['CONSUME'].dropna().index + + tech_map = pd.read_csv( + os.path.join(sw['reeds_path'],'postprocessing','bokehpivot','in','reeds2','tech_map.csv')) + + tech_map.raw = reeds.reedsplots.simplify_techs(tech_map.raw, display_level = 'diagnostics') + tech_map = tech_map.drop_duplicates().set_index('raw').display + + tech_style = pd.read_csv( + os.path.join(sw['reeds_path'],'postprocessing','bokehpivot','in','reeds2','tech_style.csv'), + index_col='order', + ).squeeze(1) + + hierarchy = reeds.io.get_hierarchy(sw.casedir) + + resources = pd.read_csv( + os.path.join(sw['casedir'],'inputs_case','resources.csv') + ).set_index('resource') + resources['tech'] = reeds.reedsplots.simplify_techs(resources.i, display_level = 'diagnostics') + resources['rb'] = resources.r + + ##### Hourly dispatch by month + ### Load and aggregate the VRE generation profiles by tech group + try: + vre_gen = reeds.io.read_file( + os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'pras_vre_gen_{sw.t}.h5'), + parse_timestamps=True, + ) + except FileNotFoundError: + vre_gen = None + + ### Get vre_gen by tech (only resource_adequacy_years) + vre_gen_usa = ( + vre_gen + .rename(columns=dict(zip(vre_gen.columns, vre_gen.columns.map(lambda x: x.split('|')[0])))) + .groupby(axis=1, level=0).sum() + .set_index(fulltimeindex) + ) + vre_gen_usa.columns = reeds.reedsplots.simplify_techs(vre_gen_usa.columns, display_level = 'diagnostics') + + if len(sw['resource_adequacy_years_list']) == 1: + vre_gen_usa = vre_gen_usa.xs(int(sw['resource_adequacy_years_list'][0]), level='year', axis=0) + + ### Get vre_gen summed over tech by BA (full 7 years) + vre_gen_r = ( + vre_gen + .rename(columns=dict(zip(vre_gen.columns, vre_gen.columns.map(lambda x: x.split('|')[1])))) + .groupby(axis=1, level=0).sum() + ) + + ### Load hourly demand + try: + load_r = pd.read_hdf( + os.path.join( + sw['casedir'],'ReEDS_Augur','augur_data',f'load_{sw.t}.h5') + ) + load_r.index = fulltimeindex + except FileNotFoundError: + load_r = None + + ### Load PRAS load + try: + pras_load = reeds.io.read_file( + os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'pras_load_{sw.t}.h5'), + parse_timestamps=True, + ) + except FileNotFoundError: + pras_load = None + pras_load.index = fulltimeindex + try: + pras_h2dac_load = reeds.io.read_file( + os.path.join( + sw['casedir'],'ReEDS_Augur','augur_data', + f"pras_h2dac_load_{sw['t']}.h5"), + parse_timestamps=True, + ) + except FileNotFoundError: + pras_h2dac_load = pd.DataFrame(columns=pras_load.columns) + pras_h2dac_load.index = fulltimeindex + + ### Load input capacity to PRAS + try: + max_cap = pd.read_csv( + os.path.join( + sw['casedir'],'ReEDS_Augur','augur_data',f"max_cap_{sw['t']}.csv")) + max_cap.i = reeds.reedsplots.simplify_techs(max_cap.i, display_level = 'diagnostics') + except FileNotFoundError: + max_cap = pd.DataFrame(columns=['i','v','r','MW']) + + ### Load LOLE/EUE/NEUE from PRAS + try: + pras = reeds.io.read_pras_results( + os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', + f"PRAS_{sw.t}i{sw.iteration}.h5") + ) + pras.index = fulltimeindex + except FileNotFoundError as err: + print(f"Failed to load PRAS outputs: {err}") + pras = pd.DataFrame({'USA_EUE':9999}, index=fulltimeindex) + + ### Load the PRAS system + try: + pras_system = reeds.io.get_pras_system( + case=sw.casedir, year=sw.t, iteration=sw.iteration) + for key in pras_system: + pras_system[key].index = fulltimeindex + for key in ['gencap', 'storcap', 'genstorcap', 'genfailrate', 'genrepairrate']: + pras_system[key].columns = pras_system[key].columns.droplevel(['unit','name']) + if 'i' in pras_system[key].columns.names: + col_vals = pras_system[key].columns.get_level_values('i').unique().tolist() + mapping = dict(zip(col_vals, reeds.reedsplots.simplify_techs(col_vals)), display_level = 'diagnostics') + pras_system[key].rename(columns = mapping, level = 'i', inplace = True) + + except FileNotFoundError as err: + print(f"Failed to load .pras system: {err}") + pras_system = dict() + + ###### Get net load profiles + ### Get net load by BA + net_load_r = load_r - vre_gen_r + ### Get net load by ccreg + net_load_ccreg = net_load_r.rename(columns=hierarchy.ccreg).groupby(axis=1, level=0).sum() + ### Get net load for the USA + net_load_usa = net_load_r.set_index(fulltimeindex).sum(axis=1) + + ###### Make combined dataframes for plotting + ### Get top load hours by ccseason + datetime2ccseason = h_dt_szn.set_index('datetime').ccseason + ### Use seasons appropriate to resolution + ccseasons = h_dt_szn.ccseason.unique() + + ccregs = sorted(net_load_ccreg.columns) + peakhours = {} + for ccseason in ccseasons: + for ccreg in ccregs: + peakhours[ccseason,ccreg] = ( + net_load_ccreg.loc[net_load_ccreg.index.map(datetime2ccseason)==ccseason][ccreg] + .nlargest(int(sw['GSw_PRM_CapCreditHours'])) + .index + ) + + dfpeak = ( + pd.DataFrame(peakhours) + .stack(level=0) + .reorder_levels([1,0], axis=0) + .stack() + .rename('datetime').to_frame() + .assign(peak=1) + .reset_index(level=2).rename(columns={'level_2':'ccreg'}) + .pivot(index='datetime', columns='ccreg', values='peak') + .reindex(fulltimeindex) + .fillna(0).astype(int) + ) + + peakcolors = plots.rainbowmapper(ccregs, plt.cm.tab20) + + ###### Store them for later + dfs = {} + dfs['dfpeak'] = dfpeak + dfs['fulltimeindex'] = fulltimeindex + dfs['gdxreeds'] = gdxreeds + dfs['h_dt_szn'] = h_dt_szn + dfs['h2dac'] = h2dac + dfs['hierarchy'] = hierarchy + dfs['load_r'] = load_r + dfs['max_cap'] = max_cap + dfs['net_load_r'] = net_load_r + dfs['net_load_usa'] = net_load_usa + dfs['peakcolors'] = peakcolors + dfs['pras_h2dac_load'] = pras_h2dac_load + dfs['pras_load'] = pras_load + dfs['pras_system'] = pras_system + dfs['pras'] = pras + dfs['resources'] = resources + dfs['tech_map'] = tech_map + dfs['tech_style'] = tech_style + dfs['vre_gen_usa'] = vre_gen_usa + dfs['vre_gen'] = vre_gen + + return dfs + + +#%%### Plotting functions +def plot_netload_profile(sw, dfs): + """ + Net load profile + """ + for y in sw['resource_adequacy_years_list']: + savename = f"A-netload-profile-w{y}-{sw['t']}.png" + + dfpos = dfs['net_load_usa'].loc[str(y)].clip(lower=0) + dfneg = dfs['net_load_usa'].loc[str(y)].clip(upper=0) + plt.close() + f,ax = plots.plotyearbymonth( + dfpos, colors=['C3'], dpi=dpi, + ) + plots.plotyearbymonth( + dfneg, colors=['C0'], + f=f, ax=ax, + ) + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_dropped_load_timeseries_full(sw, dfs): + """ + Dropped load timeseries + """ + dropped = dfs['pras']['USA_EUE'].copy() + timeindex_y = pd.date_range( + f"{sw['t']}-01-01", f"{sw['t']+1}-01-01", inclusive='left', freq='H', + tz='Etc/GMT+6')[:8760] + savename = f"dropped_load-timeseries-wfull-{sw['t']}.png" + weatheryears = sw.resource_adequacy_years_list + plt.close() + f,ax = plt.subplots(len(weatheryears), 1, sharex=True, sharey=True, figsize=(13.33,5)) + for row, y in enumerate(weatheryears): + ax[row].fill_between( + timeindex_y, dropped.loc[str(y)].values/1e3, + lw=0.2, color='C3') + ### Formatting + ax[row].annotate( + y, (0.01,1), xycoords='axes fraction', + fontsize=14, weight='bold', va='top') + ### Formatting + ax[0].set_xlim( + pd.Timestamp(f"{sw['t']}-01-01 00:00-05:00"), + pd.Timestamp(f"{sw['t']}-12-31 23:59-05:00")) + ax[0].set_ylim(0) + ax[len(weatheryears)-1].set_ylabel('Dropped load [GW]', y=0, ha='left') + plots.despine(ax) + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_h2dac_load_timeseries(sw, dfs): + """ + H2 and DAC load timeseries + """ + + dfplot = dfs['pras_h2dac_load'].sum(axis=1) + if not dfplot.sum(): + return + + for y in sw['resource_adequacy_years_list']: + savename = f"h2dac_load-timeseries-w{y}-{sw['t']}.png" + + plt.close() + ## DAC and H2 demand + f,ax = plots.plotyearbymonth( + dfplot.loc[str(y)].rename('H2/DAC\ndemand').abs().to_frame(), + colors=['C9'], dpi=dpi, + ) + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_dropped_load_duration(sw, dfs): + """ + Dropped load duration + """ + dropped = dfs['pras']['USA_EUE'].copy() + + savename = f"dropped_load-duration-{sw['t']}.png" + plt.close() + f,ax = plt.subplots(dpi=dpi) + ## Dropped load + ax.fill_between( + range(len(dropped)), + dropped.rename('Dropped').sort_values(ascending=False).values/1e3, + color='C3', lw=0, label='Dropped', + ) + ## Mark the extrema + ax.plot( + [0], [dropped.max()/1000], + lw=0, marker=1, ms=5, c='C3',) + ax.plot( + [(dropped>0).sum()], [0], + lw=0, marker=2, ms=5, c='C3',) + ## Formatting + ax.set_ylabel('Demand [GW]') + ax.set_xlabel( + f"Hours of dispatch period ({min(sw['resource_adequacy_years_list'])}–" + f"{max(sw['resource_adequacy_years_list'])}) [h]") + ax.set_xlim(0, max((dropped>0).sum(), 1)) + ax.set_ylim(0) + ax.legend( + # loc='upper left', bbox_to_anchor=(1,1.25), + fontsize=11, frameon=True, ncol=1, + columnspacing=0.5, handletextpad=0.3, handlelength=0.7, + ) + plots.despine(ax) + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def map_dropped_load(sw, dfs, level='r'): + """ + Annual EUE and NEUE by ReEDS zone + """ + ### Get the inputs + dfmap = reeds.io.get_dfmap(sw['casedir']) + dfba = dfmap['r'] + dropped = dfs['pras'][ + [c for c in dfs['pras'] if c.endswith('_EUE') and (not c.startswith('USA'))] + ].copy() + dropped.columns = dropped.columns.map(lambda x: x.split('_')[0]) + units = { + ('EUE','max'): ('MW',1), ('EUE','mean'): ('MW',1), ('EUE','sum'): ('GWh',1e-3), + ('NEUE','max'): ('%',1e2), ('NEUE','sum'): ('ppm',1e6), + } + load = dfs['pras_load'] + + ### Plot it + for metric in ['EUE','NEUE']: + ## Aggregate if necessary + if level not in ['r','rb','ba']: + dropped = ( + dropped.rename(columns=dfs['hierarchy'][level]) + .groupby(level=0, axis=1).sum().copy() + ) + load = ( + load.rename(columns=dfs['hierarchy'][level]) + .groupby(level=0, axis=1).sum().copy() + ) + for agg in ['max','sum','mean']: + if (metric,agg) not in units: + continue + savename = f"dropped_load-map-{metric}_{agg}-{level}-{sw['t']}.png" + + dfplot = dfba.copy() + if level not in ['r','rb','ba']: + dfplot[level] = dfs['hierarchy'][level] + dfplot = dfplot.dissolve(level) + dfplot['centroid_x'] = dfplot.geometry.centroid.x + dfplot['centroid_y'] = dfplot.geometry.centroid.y + if metric == 'EUE': + dfplot['val'] = dropped.agg(agg) + elif (metric == 'NEUE') and (agg == 'max'): + dfplot['val'] = (dropped / load).agg(agg) + elif (metric == 'NEUE'): + dfplot['val'] = dropped.agg(agg) / load.agg(agg) + dfplot.val = (dfplot.val * units[metric,agg][1]).replace(0, np.nan) + + plt.close() + f,ax = plt.subplots(figsize=(8,8/1.45), dpi=150) + ### Background + dfba.plot(ax=ax, facecolor='none', edgecolor='k', lw=0.2) + ### Data + dfplot.plot(ax=ax, column='val', cmap=cmocean.cm.rain) + for r, row in dfplot.iterrows(): + if row.val > 0: + ax.annotate( + f'{row.val:,.0f} {units[metric,agg][0]}', + (row.centroid_x, row.centroid_y), + color='r', ha='center', va='top', fontsize=6, weight='bold') + ### Formatting + if level in ['r','rb','ba']: + for r, row in dfba.iterrows(): + ax.annotate(r, (row.centroid_x, row.centroid_y), + ha='center', va='bottom', fontsize=6, color='C7') + ax.axis('off') + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_pras_ICAP(sw, dfs): + """ + Plot the available capacity used in PRAS without any outages + """ + if not len(dfs['pras_system']): + print('PRAS system was not loaded') + return + ### Collect the PRAS system capacities + cap = pd.concat([ + dfs['pras_system']['gencap'].groupby(axis=1, level=0).sum(), + dfs['pras_system']['storcap'].groupby(axis=1, level=0).sum(), + dfs['pras_system']['genstorcap'].groupby(axis=1, level=0).sum(), + ], axis=1) + ## Drop any empties + cap = cap.replace(0,np.nan).dropna(axis=1, how='all').fillna(0).astype(int) + ## Get the colors + tech_style = dfs['tech_style']['color'].squeeze() + ## Aggregate by type + cap = cap.groupby(axis=1, level=0).sum() + order = [c for c in tech_style.index if c in cap] + cap = cap[order] + if cap.shape[1] != len(order): + raise Exception(f"missing colors: {cap.columns}, {tech_style}") + ## Cumulate for plot + cumcap = cap.cumsum(axis=1)[order[::-1]] + load = dfs['pras_system']['load'].sum(axis=1) + + ### Plot it + for y in sw.resource_adequacy_years_list: + savename = f"PRAS-ICAP-w{y}-{sw['t']}.png" + plt.close() + f,ax = plots.plotyearbymonth( + load.loc[str(y)], + colors=['k'], style='line', lwforline=1, + ) + plots.plotyearbymonth( + cumcap.loc[str(y)], colors=[tech_style[c] for c in cumcap], + f=f, ax=ax, + ) + ## Legend + leg = ax[0].legend( + loc='upper left', bbox_to_anchor=(1,1.25), + fontsize=11, frameon=False, ncol=1, + columnspacing=0.5, handletextpad=0.3, handlelength=0.7, + ) + leg.set_title(f'ICAP {y}', prop={'size':'large'}) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_augur_pras_capacity(sw, dfs): + """ + Plot the nameplate capacity from Augur and PRAS to check consistency + """ + if not len(dfs['pras_system']): + print('PRAS system was not loaded') + return + savename = f"PRAS-Augur-capacity-{sw['t']}.png" + ### Get the colors + tech_style = dfs['tech_style']['color'].squeeze() + ### Collect the PRAS system capacities + cap = {} + cap['pras'] = pd.concat([ + dfs['pras_system']['gencap'], + dfs['pras_system']['storcap'], + dfs['pras_system']['genstorcap'], + ], axis=1) / 1e3 + ## Drop any empties + cap['pras'] = cap['pras'].replace(0,np.nan).dropna(axis=1, how='all').fillna(0) + ## Aggregate by type + cap['pras'] = (cap['pras'] + .groupby(axis=1, level=[1,0]).sum().max().rename('MW') + ) + + ### Collect the Augur capacities + cap['augur'] = dfs['max_cap'].groupby(['i','r'], as_index=False).MW.sum() + ## Convert from s to p regions + cap['augur'].r = cap['augur'].r + ## Aggregate by type + cap['augur'] = (cap['augur'] + .replace({'i':{'Hydropower Existing':'Hydropower', 'Hydropower New':'Hydropower'}}) + .groupby(['r','i']).MW.sum() / 1e3 + ) + + ### Drop VRE since its capacity is handled differently + for datum in cap: + cap[datum] = ( + cap[datum].loc[ + ~cap[datum].index.get_level_values('i').isin( + dfs['vre_gen_usa'].columns.tolist()) + ] + ) + + ### Get coordinates + zones = dfs['hierarchy'].index + ncols = int(np.around(np.sqrt(len(zones)) * 1.618, 0)) + nrows = len(zones) // ncols + int(bool(len(zones) % ncols)) + coords = dict(zip(zones, [(row,col) for row in range(nrows) for col in range(ncols)])) + + ### Plot the capacities + plt.close() + f,ax = plt.subplots( + nrows, ncols, figsize=(1.2*ncols, 1.2*nrows), sharex=True, + gridspec_kw={'wspace':0.6, 'hspace':0.5} + ) + alltechs = set() + for r in zones: + df = pd.concat({'A':cap['augur'].get(r,pd.Series()), 'P':cap['pras'].get(r,pd.Series())}, axis=1).T + order = [c for c in tech_style.index if c in df] + missing = [c for c in df if c not in order] + if len(missing): + print(f'WARNING: Missing colors for these techs: {missing}') + df = df[order].copy() + alltechs.update(df.columns.tolist()) + plots.stackbar(df=df, ax=ax[coords[r]], colors=tech_style, net=False, width=0.8) + ### Formatting + ax[coords[r]].set_title(r) + ax[coords[r]].set_xticks([0,1]) + ax[coords[r]].set_xticklabels(['A','P']) + ### Legend + if r == zones[-1]: + handles = [ + mpl.patches.Patch(facecolor=tech_style[i], edgecolor='none', label=i) + for i in [j for j in tech_style.index if j in alltechs] + ][::-1] + ax[coords[r]].legend( + handles=handles, loc='center left', bbox_to_anchor=(1,0.5), + frameon=False, columnspacing=0.5, handletextpad=0.3, handlelength=0.7, + ncol=len(alltechs)//3, + ) + ### Formatting + plots.trim_subplots(ax=ax, nrows=nrows, ncols=ncols, nsubplots=len(zones)) + ax[-1, 0].set_ylabel('Nameplate capacity [GW]', y=0, ha='left') + plots.despine(ax) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_pras_ICAP_regional(sw, dfs, numdays=5): + """ + Plot the available capacity used in PRAS without any outages + """ + if not len(dfs['pras_system']): + print('PRAS system was not loaded') + return + ### Get the peak dropped-load periods + dropped = dfs['pras']['USA_EUE'].copy() + dropped = dropped.groupby( + [dropped.index.year, dropped.index.month, dropped.index.day] + ).sum().nlargest(numdays).replace(0,np.nan).dropna() + + ### Collect the PRAS system capacities + load = dfs['pras_system']['load'] / 1e3 + cap = pd.concat([ + dfs['pras_system']['gencap'], + dfs['pras_system']['storcap'], + dfs['pras_system']['genstorcap'], + ], axis=1) / 1e3 + ## Drop any empties + cap = cap.replace(0,np.nan).dropna(axis=1, how='all').fillna(0) + ## Get the colors + tech_style = dfs['tech_style']['color'].squeeze() + ## Aggregate by type + cap = cap.groupby(axis=1, level=[1,0]).sum() + + ### Get coordinates + zones = dfs['hierarchy'].index + ncols = int(np.around(np.sqrt(len(zones)) * 1.618, 0)) + nrows = len(zones) // ncols + int(bool(len(zones) % ncols)) + coords = dict(zip(zones, [(row,col) for row in range(nrows) for col in range(ncols)])) + + ### Plot the highest-EUE days + for day in range(len(dropped)): + date = '{}-{:>02}-{:>02}'.format(*dropped.index[day]) + savename = f"PRAS-ICAP-{sw['t']}-{date.replace('-','')}.png" + plt.close() + f,ax = plt.subplots( + nrows, ncols, figsize=(1.2*ncols, 1.2*nrows), sharex=True, + gridspec_kw={'wspace':0.6, 'hspace':0.5} + ) + for r in zones: + df = cap.loc[date][r].copy() + ### Cumulate for plot + order = [c for c in tech_style.index if c in df] + df = df[order] + ### Plot it + plots.stackbar( + df=df, ax=ax[coords[r]], colors=tech_style, net=False, align='edge', + width=pd.Timedelta('1H'), + ) + ax[coords[r]].plot( + load.loc[date].index, load.loc[date][r].values, c='k', lw=1, + path_effects=[pe.withStroke(linewidth=1.7, foreground='w', alpha=0.8)], + ) + ### Formatting + ax[coords[r]].set_title(r) + ### Formatting + plots.trim_subplots(ax=ax, nrows=nrows, ncols=ncols, nsubplots=len(zones)) + ax[coords[zones[0]]].set_xlim(df.index[0], df.index[-1] + pd.Timedelta('1H')) + ax[coords[zones[0]]].set_xticks([]) + ax[-1, 0].set_xlabel(date, x=0, ha='left', labelpad=10) + ax[-1, 0].set_ylabel('ICAP [GW]', y=0, ha='left') + plots.despine(ax) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_pras_unitsize_distribution(sw, dfs): + """ + Distribution of PRAS unit sizes by tech + """ + if not len(dfs['pras_system']): + print('PRAS system was not loaded') + return + savename = f"PRAS-unitcap-{sw['t']}.png" + cap = ( + pd.concat([ + dfs['pras_system']['gencap'], + dfs['pras_system']['storcap'], + dfs['pras_system']['genstorcap'], + ], axis=1) + .max().rename('MW').reset_index() + ) + ## Get the colors + tech_style = dfs['tech_style']['color'].squeeze() + ## Aggregate by type + techs = cap.i.unique() + nondisaggtechs = ( + dfs['vre_gen_usa'].columns.tolist() + + ['Hydropower Existing'] + + ['Battery'] + + ['Canadian Imports'] + ) + order = [i for i in tech_style.index if i in techs] + others = [i for i in techs if ((i not in order) and (i not in nondisaggtechs))] + # for i in others: + # tech_style[i] = 'k' + + ylabel = {0: {'scale':1, 'units':'MW'}, 1: {'scale':1e-3, 'units':'GW'}} + plt.close() + f,ax = plt.subplots(1, 2, figsize=(7,3.75), gridspec_kw={'wspace':0.4}) + for i in (order+others)[::-1]: + df = cap.loc[cap.i==i].copy() + col = 1 if i in nondisaggtechs else 0 + df.MW = df.MW * ylabel[col]['scale'] + ax[col].plot( + range(len(df)), df.MW.sort_values().values, + c=tech_style.get(i,'k'), label=i) + ax[col].annotate( + f' {i}', (len(df)-1, df.MW.max()), + fontsize=10, color=tech_style.get(i,'k'), + path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.7)], + ) + for col in range(2): + ax[col].set_ylabel(f"Unit size [{ylabel[col]['units']}]") + ax[col].set_xlabel('Number of units') + ax[col].set_xlim(0) + ax[col].set_ylim(0) + ax[col].legend( + ncol=1, columnspacing=0.5, frameon=False, + handletextpad=0.3, handlelength=0.7, + loc='upper center', bbox_to_anchor=(0.5,-0.2)) + ax[0].set_title('Disaggregated techs') + ax[1].set_title('Aggregated techs (PRAS FOR=0)') + plots.despine(ax) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_pras_unitnumber(sw, dfs, level='country'): + """Number of PRAS units by technology, grouped by region level""" + if not len(dfs['pras_system']): + print('PRAS system was not loaded') + return + savename = f"PRAS-unitnumber-{level}-{sw['t']}.png" + cap = ( + pd.concat([ + dfs['pras_system']['gencap'], + dfs['pras_system']['storcap'], + dfs['pras_system']['genstorcap'], + ], axis=1) + .max().rename('MW').reset_index() + ) + ## Get the colors + tech_style = dfs['tech_style']['color'].squeeze() + ## Aggregate by type + order = cap.i.value_counts().index + colors = order.map(lambda x: tech_style.get(x, mpl.colors.to_hex('k'))) + + ## Group by hierarchy level + regions = sorted(( + dfs['hierarchy'].index if level == 'r' else dfs['hierarchy'][level] + ).unique()) + nrows, ncols, coords = plots.get_coordinates(regions) + + plt.close() + f,ax = plt.subplots( + nrows, ncols, figsize=(max(ncols*2, 5), max(nrows*2, 3.75)), + sharex=True, sharey=True, + ) + for region in regions: + _ax = (ax if nrows == ncols == 1 else ax[coords[region]]) + rs = ( + [region] if level == 'r' + else dfs['hierarchy'].loc[dfs['hierarchy'][level]==region].index + ) + df = cap.loc[cap.r.isin(rs)].i.value_counts().reindex(order).fillna(0) + _ax.bar( + df.index, df.values, color=colors, + ) + _ax.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) + _ax.set_xticks(range(len(order))) + _ax.set_xticklabels(order, rotation=90, fontsize=9) + _ax.set_xlim(-0.5, len(order)-0.5) + _ax.annotate( + region.replace('_','\n') + f'\n{df.sum():.0f}', + (0.95,0.95), xycoords='axes fraction', ha='right', va='top', + fontsize='large', weight='bold', + ) + labelax = ( + ax if (nrows == ncols == 1) + else ax[0] if ((nrows == 1) or (ncols == 1)) + else ax[-1,0] + ) + labelax.set_ylabel('Number of units', y=0, ha='left') + plots.despine(ax) + plots.trim_subplots(ax, nrows, ncols, len(regions)) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_pras_load_units(sw, dfs): + """Histogram of net load and sorted unit sizes by model zone""" + savename = f"PRAS-load_hist-units-{sw['t']}.png" + ### Get inputs + ## Capacity + cap = ( + pd.concat([ + dfs['pras_system']['gencap'], + dfs['pras_system']['storcap'], + dfs['pras_system']['genstorcap'], + ], axis=1) + .max().rename('MW').reset_index() + ) + ## Net demand + vre_gen = dfs['vre_gen'].copy() + vre_gen.columns = pd.MultiIndex.from_tuples( + vre_gen.columns.map(lambda x: tuple(x.split('|'))), + names=['i','r'], + ) + net_demand = (dfs['pras_system']['load'] - vre_gen.groupby('r', axis=1).sum()) / 1e3 + ## Remaining unit capacity + units = cap.loc[ + ~cap.i.isin( + reeds.reedsplots.simplify_techs(vre_gen.columns.get_level_values('i').unique(), display_level = 'diagnostics')) + & (cap.MW > 0) + ] + ## Get the colors + tech_style = dfs['tech_style']['color'].squeeze() + ## Only keep the 10 regions with highest neue + regions = ( + dfs['pras'][[c for c in dfs['pras'] if c.endswith('_EUE') and not c.startswith('USA')]] + .sum() + .nlargest(10) + ).index.map(lambda x: x[:-len('_EUE')]) + + ## Maps + dfmap = reeds.io.get_dfmap(sw['casedir']) + + ### Plot it + ncols = len(regions) + nrows = 3 + plt.close() + f,ax = plt.subplots( + nrows, ncols, figsize=(ncols*1.25, 6), + gridspec_kw={'height_ratios':[1,1,3], 'hspace':0.3}, + ) + for col, r in enumerate(regions): + ## Net load + ax[1,col].hist(net_demand[r], bins=101, color='C3') + ## Units + df = units.loc[units.r==r].sort_values('MW') + df['GW'] = df.MW / 1e3 + df['GW_cumsum'] = df.GW.cumsum() + df['left'] = df['GW_cumsum'].shift(1).fillna(0) + ## Do it twice to get darker lines around the edges + ax[-1,col].bar( + x=df['left'], + height=df['MW'], + width=df['GW'], + align='edge', + color=df.i.map(lambda x: tech_style.get(x,'#000000')), + alpha=0.7, + ) + ax[-1,col].bar( + x=df['left'], + height=df['MW'], + width=df['GW'], + align='edge', + color='none', + edgecolor='k', lw=0.15, + ) + ## Peak + peak = net_demand[r].max() + for row in [1, 2]: + ax[row,col].axvline(peak, c='C3', lw=0.75, ls=':') + ## Formatting + reeds.plots.despine(ax[1,col], left=False) + ax[1,col].set_yticks([]) + ax[1,col].set_xticklabels([]) + ## Ignore battery and hydro for the y limit since they're not disaggregated + ax[-1,col].set_ylim(0, units.loc[~units.i.str.startswith(('Battery','Hydro')), 'MW'].max()) + if col > 0: + ax[-1,col].set_yticklabels([]) + ## Share x axis for histograms + xmax = max(ax[1,col].get_xlim()[1], ax[2,col].get_xlim()[1]) + for row in [1, 2]: + ax[row,col].set_xlim(0, xmax) + ## Maps + dfmap['r'].loc[[r]].plot(ax=ax[0,col], facecolor='k', edgecolor='none', zorder=3) + bounds = dfmap['r'].loc[[r]].bounds.squeeze() + ax[0,col].set_xlim(bounds.minx-50e3, bounds.maxx+50e3) + ax[0,col].set_ylim(bounds.miny-50e3, bounds.maxy+50e3) + dfmap['st'].plot(ax=ax[0,col], facecolor='none', edgecolor='k', lw=0.5, zorder=2) + dfmap['r'].plot(ax=ax[0,col], facecolor='0.9', edgecolor='w', lw=0.5, zorder=1) + ax[0,col].axis('off') + ax[1,col].set_title(r) + ## Formatting + ax[-1,0].set_xlabel('GW') + ax[1,0].set_xlabel('Net load [GW]', x=0, ha='left', color='C3') + ax[-1,0].set_ylabel('Unit size [MW]') + ax[-1,0].set_xlabel('Installed capacity [GW]', x=0, ha='left') + reeds.plots.despine(ax) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_pras_augur_load(sw, dfs): + """PRAS load against Augur load""" + dfpras = dfs['pras_system']['load'].sum(axis=1).rename('PRAS') + dfaugur = dfs['load_r'].set_axis(dfpras.index).sum(axis=1).rename('Augur') + years = dfpras.index.year.unique() + linecolors = {'Augur':'C0', 'PRAS':'C3'} + for year in years: + savename = f"demand_USA-Augur-PRAS-w{year}-{sw['t']}.png" + plt.close() + f,ax = plots.plotyearbymonth( + dfaugur.loc[str(year)], style='line', colors=linecolors['Augur']) + plots.plotyearbymonth( + dfpras.loc[str(year)], style='line', colors=linecolors['PRAS'], f=f, ax=ax) + ## Legend + handles = [ + mpl.lines.Line2D([], [], color=linecolors[i], label=i, lw=2) + for i in linecolors + ] + ax[0].legend( + handles=handles, ncol=2, frameon=False, + loc='lower right', bbox_to_anchor=(1,0.5), + handlelength=1.0, handletextpad=0.3, labelspacing=0.5, columnspacing=0.5, + ) + ax[0].annotate(year, (0.85, 1), xycoords='axes fraction', ha='right') + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_pras_load(sw, dfs): + """PRAS load over all weather years""" + dfpras = dfs['pras_system']['load'].sum(axis=1).rename('PRAS') + years = dfpras.index.year.unique() + linecolors = plots.rainbowmapper(years, categorical=True) + savename = f"demand_USA-PRAS-{sw['t']}.png" + plt.close() + f,ax = plots.plotyearbymonth( + dfpras.loc[str(years[0])], style='line', colors=linecolors[years[0]]) + for year in years[1:]: + plots.plotyearbymonth( + dfpras.loc[str(year)], style='line', colors=linecolors[year], f=f, ax=ax) + ## Legend + handles = [ + mpl.lines.Line2D([], [], color=linecolors[y], label=y, lw=2) + for y in years + ] + ax[0].legend( + handles=handles, ncol=len(years), frameon=False, + loc='lower right', bbox_to_anchor=(1,0.5), + handlelength=1.0, handletextpad=0.3, labelspacing=0.5, columnspacing=0.5, + ) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def map_pras_failure_rate(sw, dfs, aggfunc='mean', repair=False): + """ + Failure rates from PRAS, indicating the probability that an online unit will fail + (which is different from the outage rate, which gives the average fraction of units + that are on outage over time, including mean time to repair).""" + dfmap = reeds.io.get_dfmap(sw['casedir']) + dfzones = dfmap['r'] + + failrate = ( + dfs['pras_system']['genfailrate'] + ## Only keep one copy of each (i,r) + .T.reset_index().drop_duplicates().set_index(['i','r']).T + * 100 + ) + failrate.index = dfs['pras_system']['genfailrate'].index + + failsum = failrate.sum() + plottechs = failsum.loc[failsum != 0].index.get_level_values('i').unique() + + for tech in plottechs: + savename = f"hourly_failure_rate-year,month-{aggfunc}-{tech.replace('-','')}-{sw['t']}" + plt.close() + f, ax = plots.map_years_months( + dfzones=dfzones, dfdata=failrate[tech], + title=f"Monthly {aggfunc}\nhourly failure rate,\n{tech} [%]", + ) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + if repair: + repairrate = ( + dfs['pras_system']['genrepairrate'] + .T.reset_index().drop_duplicates().set_index(['i','r']).T + * 100 + ) + repairrate.index = dfs['pras_system']['genrepairrate'].index + for tech in plottechs: + savename = f"hourly_repair_rate-year,month-{aggfunc}-{tech.replace('-','')}-{sw['t']}" + plt.close() + f, ax = plots.map_years_months( + dfzones=dfzones, dfdata=repairrate[tech], + title=f"Monthly {aggfunc}\nhourly repair rate,\n{tech} [%]", + ) + ## Save it + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_cc_mar(sw, dfs): + """ + Marginal capacity credit + """ + param = 'cc_mar' + savename = f"{param}-{sw['t']}.png" + + if not int(sw['GSw_PRM_CapCredit']): + raise KeyError('No capacity credit values to plot') + cc_results = gdxpds.to_dataframes(os.path.join( + sw['casedir'],'ReEDS_Augur','augur_data', + 'ReEDS_Augur_{}.gdx'.format(sw['t']) + )) + + dfplot = cc_results[param].drop('t',axis=1).copy() + dfplot['tech'] = dfplot.i.map(lambda x: x.split('_')[0]) + techs = sorted(dfplot.tech.unique()) + numcols = len(techs) + bootstrap = 5 + squeeze = 0.7 + ### Use seasons appropriate to resolution + ccseasons = ['cold', 'hot'] + histcolor = ['C0', 'C1'] + xticklabels = ccseasons + + plt.close() + f,ax = plt.subplots( + 1, numcols, figsize=(len(techs)*1.2, 3.75), sharex=True, sharey=True) + for row, tech in enumerate(techs): + df = dfplot.loc[(dfplot.tech==tech)].copy() + ### Each observation in the histogram is a (i,r) pair + df['i_r'] = df.i + '_' + df.r + df = df.pivot(columns='ccseason',values='Value',index='i_r')[ccseasons] + + plots.plotquarthist( + ax[row], df, histcolor=histcolor, + bootstrap=bootstrap, density=True, squeeze=squeeze, + pad=0.03, + ) + + ### Formatting + ax[row].set_title(tech) + ax[row].yaxis.set_minor_locator(mpl.ticker.MultipleLocator(0.1)) + ax[row].yaxis.set_major_locator(mpl.ticker.MultipleLocator(0.2)) + ax[row].set_ylim(0,1) + ax[row].set_xticklabels(xticklabels, rotation=90) + + ax[0].set_ylabel( + f'{sw.t} {param} [fraction]', weight='bold', fontsize='x-large') + + plots.despine(ax) + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_netloadhours_timeseries(sw, dfs): + """ + Peak net load hours by ccreg + """ + savename = f"netloadhours-timeseries-{sw['t']}.png" + + ### Plot it + years = sw.resource_adequacy_years_list + ccregs = sorted(dfs['dfpeak'].columns) + plt.close() + f,ax = plt.subplots(len(years),1,sharex=False,sharey=True,figsize=(12,6)) + for row, year in enumerate(years): + df = dfs['dfpeak'].loc[str(year)].cumsum(axis=1)[ccregs[::-1]] + df.plot.area( + ax=ax[row], color=dfs['peakcolors'], stacked=False, alpha=1, + legend=False, + ) + ax[row].annotate( + year,(0.005,0.95),xycoords='axes fraction',ha='left',va='top', + weight='bold',fontsize='large') + if row < (len(years) - 1): + ax[row].set_xticklabels([]) + handles, labels = ax[0].get_legend_handles_labels() + ax[0].legend( + handles[::-1], labels[::-1], + columnspacing=0.5, handletextpad=0.3, handlelength=0.7, + loc='upper left', bbox_to_anchor=(1,1), frameon=False, title='ccreg', + ) + ax[3].set_ylabel('Net load peak instances [#]') + # ax[-1].xaxis.set_major_locator(mpl.dates.MonthLocator()) + # ax[-1].xaxis.set_major_formatter(mpl.dates.DateFormatter('%b')) + plots.despine(ax) + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_netloadhours_histogram(sw, dfs): + """ + histograms of peak net load occurrence + """ + savename = f"netloadhours-histogram-{sw['t']}.png" + + ### Plot it + years = sw.resource_adequacy_years_list + ccregs = sorted(dfs['dfpeak'].columns) + plt.close() + f,ax = plt.subplots(1,3,figsize=(12,3.75)) + ### hour + dfs['dfpeak'].groupby(dfs['dfpeak'].index.hour).sum()[ccregs[::-1]].plot.bar( + ax=ax[0], color=dfs['peakcolors'], stacked=True, alpha=1, + width=0.95, legend=False, + ) + ax[0].set_xlabel('Hour [CST]') + ax[0].xaxis.set_major_locator(mpl.ticker.MultipleLocator(4)) + ax[0].xaxis.set_minor_locator(mpl.ticker.MultipleLocator(1)) + ax[0].tick_params(labelrotation=0) + ### Month + dfs['dfpeak'].groupby(dfs['dfpeak'].index.month).sum()[ccregs[::-1]].plot.bar( + ax=ax[1], color=dfs['peakcolors'], stacked=True, alpha=1, + width=0.95, legend=False, + ) + ax[1].set_xlabel('Month') + ax[1].tick_params(labelrotation=0) + ### Year + dfs['dfpeak'].groupby(dfs['dfpeak'].index.year).sum()[ccregs[::-1]].plot.bar( + ax=ax[2], color=dfs['peakcolors'], stacked=True, alpha=1, + width=0.95, legend=False, + ) + ax[2].set_xlabel('Year') + ax[2].set_xticks(range(len(years))) + ax[2].set_xticklabels( + years, + rotation=35, ha='right', rotation_mode='anchor') + # ax[2].tick_params(labelrotation=45) + ### Formatting + handles, labels = ax[2].get_legend_handles_labels() + ax[2].legend( + handles[::-1], labels[::-1], + loc='center left', bbox_to_anchor=(1,0.5), frameon=False, + title=sw['capcredit_hierarchy_level'], + ncol=1, columnspacing=0.5, handletextpad=0.3, handlelength=0.7, + ) + ax[0].set_ylabel('Net peak load instances [#]') + plots.despine(ax) + if savefig: + plt.savefig(os.path.join(sw['savepath'],savename)) + if interactive: + plt.show() + plt.close() + + +def plot_stressors(sw, dfs): + """ + Map demand/CF/FOR (organized differently to allow use outside of Augur) + """ + for iteration in range(sw['iteration']): + plot_generator = reeds.reedsplots.map_stressors( + case=sw['casedir'], t=sw['t'], iteration=iteration, + seed=(True if t == int(sw['endyear']) else False), + ) + while True: + try: + f, ax, df, plotlabel = next(plot_generator) + savename = ( + f"stress{t}i{iteration}-" + + plotlabel.split(':')[0].replace('-','') + ) + if savefig: + plt.savefig(os.path.join(sw.casedir, 'outputs', 'figures', f'{savename}.png')) + if interactive: + plt.show() + except StopIteration: + break + + +#%%### Main function +def main(sw, debug=False): + """ + debug: Make more plots for debugging if set to True + """ + #%% Get the inputs + dfs = get_inputs(sw) + + #%% Make the plots + if (not int(sw.GSw_PRM_CapCredit)) or (int(sw.pras == 2)): + try: + plot_pras_ICAP_regional(sw, dfs) + except Exception: + print('plot_pras_ICAP_regional() failed:', traceback.format_exc()) + + try: + plot_pras_load_units(sw, dfs) + except Exception: + print('plot_pras_load_units() failed:', traceback.format_exc()) + + try: + plot_pras_unitsize_distribution(sw, dfs) + except Exception: + print('plot_pras_unitsize_distribution() failed:', traceback.format_exc()) + + try: + for level in ['country', 'transgrp', 'r']: + plot_pras_unitnumber(sw, dfs, level) + except Exception: + print('plot_pras_unitnumber() failed:', traceback.format_exc()) + + try: + plot_augur_pras_capacity(sw, dfs) + except Exception: + print('plot_augur_pras_capacity() failed:', traceback.format_exc()) + + try: + plot_pras_load(sw, dfs) + except Exception: + print('plot_pras_load() failed:', traceback.format_exc()) + + try: + plot_stressors(sw, dfs) + except Exception: + print('plot_stressors() failed:', traceback.format_exc()) + + try: + plot_dropped_load_timeseries_full(sw, dfs) + except Exception: + print('plot_dropped_load_timeseries_full() failed:', traceback.format_exc()) + + try: + plot_dropped_load_duration(sw, dfs) + except Exception: + print('plot_dropped_load_duration() failed:', traceback.format_exc()) + + try: + for level in ['r', 'transgrp']: + map_dropped_load(sw, dfs, level=level) + except Exception: + print('map_dropped_load() failed:', traceback.format_exc()) + + if int(sw['GSw_PRM_CapCredit']): + try: + plot_cc_mar(sw, dfs) + except Exception: + print('plot_cc_mar() failed:', traceback.format_exc()) + + try: + plot_netloadhours_timeseries(sw, dfs) + except Exception: + print('plot_netloadhours_timeseries() failed:', traceback.format_exc()) + + try: + plot_netloadhours_histogram(sw, dfs) + except Exception: + print('plot_netloadhours_histogram() failed:', traceback.format_exc()) + + if debug: + try: + plot_pras_augur_load(sw, dfs) + except Exception: + print('plot_pras_augur_load() failed:', traceback.format_exc()) + + try: + plot_pras_ICAP(sw, dfs) + except Exception: + print('plot_pras_ICAP() failed:', traceback.format_exc()) + + try: + plot_netload_profile(sw, dfs) + except Exception: + print('plot_netload_profile() failed:', traceback.format_exc()) + + try: + map_pras_failure_rate(sw, dfs) + except Exception: + print('map_pras_failure_rate() failed:', traceback.format_exc()) + + +#%%### PROCEDURE +if __name__ == '__main__': + #%%### ARGUMENT INPUTS + import argparse + parser = argparse.ArgumentParser( + description='Create the necessary 8760 and capacity factor data for hourly resolution') + parser.add_argument('--reeds_path', help='ReEDS-2.0 directory') + parser.add_argument('--casedir', help='ReEDS-2.0/runs/{case} directory') + parser.add_argument('--t', type=int, default=2050, help='solve year to plot') + parser.add_argument('--iteration', '-i', type=int, default=-1, + help='iteration to plot (default of -1 means latest iteration)') + parser.add_argument('--debug', '-d', action='store_true', help='Make more plots') + + args = parser.parse_args() + reeds_path = args.reeds_path + casedir = args.casedir + t = args.t + iteration = args.iteration + debug = args.debug + + # #%%### Inputs for debugging + # reeds_path = reeds.io.reeds_path + # casedir = os.path.join(reeds_path, 'runs', 'v20251111_15M0_Pacific') + # t = 2026 + # interactive = True + # iteration = 0 + # debug = True + + #%%### INPUTS + ### Switches + sw = reeds.io.get_switches(casedir) + sw['t'] = t + ## Debugging + # sw['reeds_path'] = reeds_path + # sw['casedir'] = casedir + + ### Run for the latest iteration + if iteration < 0: + sw['iteration'] = int( + os.path.splitext( + sorted(glob(os.path.join(sw.casedir,'lstfiles',f'*{sw.t}i*.lst')))[-1] + )[0] + .split(f'{sw.t}i')[-1] + ) + else: + sw['iteration'] = iteration + + ### Make the plots + print('plotting intermediate Augur results...') + try: + main(sw, debug) + except Exception as _err: + print('diagnostic_plots.py failed with the following exception:') + print(traceback.format_exc()) + + ### Remove intermediate csv files to save drive space + if (not int(sw['keep_augur_files'])) and (not int(sw['debug'])): + delete_temporary_files(sw) diff --git a/reeds/resource_adequacy/prep_data.py b/reeds/resource_adequacy/prep_data.py new file mode 100644 index 00000000..8f4febfc --- /dev/null +++ b/reeds/resource_adequacy/prep_data.py @@ -0,0 +1,588 @@ +#%% Notes +""" +This script writes inputs for resource adequacy calculations: +* capacity_credit.py (existing and marginal capacity credit) +* run_pras.jl -> ReEDS2PRAS.jl -> PRAS.jl (probabilistic resource adequacy) + +The files used by PRAS are: +* In {case}/ReEDS_Augur/augur_data: + * cap_converter_{year}.csv + * energy_cap_{year}.csv + * max_cap_{year}.csv + * pras_load_{year}.h5 + * pras_vre_gen_{year}.h5 + * tran_cap_{year}.csv +* In {case}/inputs_case: + * hydcf.csv + * outage_forced_hourly.h5 + * outage_forced_static.csv + * resources.csv + * tech-subset-table.csv + * unitdata.csv + * unitsize.csv +""" + +#%% General imports +import os +import re +import pandas as pd +import numpy as np +import gdxpds +### Local imports +import reeds + + +### Functions +def errorcheck_reeds2pras(casedir, csvout, h5out): + ### In ReEDS2PRAS, two classes of technologies are handled separately: + ### - Technologies in pras_vre_gen: + ### - Capacity is defined as the maximum of the hourly generation profile + ### - Capacity is not disaggregated into units and no outages are applied + ### - Technologies in max_cap: + ### - Capacity is taken from max_cap and disaggregated into units + ### - Unit outages are applied + ### - (except for batteries and dispatchable hydro, which are not disaggregated and + ### for which outages are not applied) + ### So here, to avoid double-counting, make sure the techs in pras_vre_gen and max_cap + ### do not overlap + profile_techs = h5out['pras_vre_gen'].columns.map(lambda x: x.split('|')[0]).unique() + max_cap_techs = csvout['max_cap'].index.get_level_values('i').unique() + for check in [ + [i for i in profile_techs if i in max_cap_techs], + [i for i in max_cap_techs if i in profile_techs], + ]: + if len(check): + raise ValueError(f'{check} overlap between pras_vre_gen and max_cap') + + ### ReEDS2PRAS takes the region list from the load data, so make sure all regions + ### with generation/transmission capacity show up in the load data + load_regions = h5out['pras_load'].columns + profile_regions = h5out['pras_vre_gen'].columns.map(lambda x: x.split('|')[1]).unique() + max_cap_regions = csvout['max_cap'].index.get_level_values('r').unique() + tran_regions = ( + list(csvout['tran_cap'].index.get_level_values('r').unique()) + + list(csvout['tran_cap'].index.get_level_values('rr').unique()) + ) + energy_regions = csvout['energy_cap'].index.get_level_values('r').unique() + converter_regions = csvout['cap_converter'].index + for check in [ + [r for r in profile_regions if r not in load_regions], + [r for r in max_cap_regions if r not in load_regions], + [r for r in tran_regions if r not in load_regions], + [r for r in energy_regions if r not in load_regions], + [r for r in converter_regions if r not in load_regions], + ]: + if len(check): + raise ValueError(f'{check} are not in pras_load') + + ### Make sure there are no missing values in data sent to ReEDS2PRAS + for key in ['pras_load', 'pras_vre_gen']: + if h5out[key].isnull().sum().sum() > 0: + missing_data_cols = [i for i in h5out[key] if h5out[key].isnull().sum(axis=0) > 0] + print(missing_data_cols) + raise ValueError(f'{key} has NaN values in {len(missing_data_cols)} columns') + + ### Make sure disaggregated techs have unit sizes, forced outage rates, and MTTRs + unitsize = pd.read_csv( + os.path.join(casedir, 'inputs_case', 'unitsize.csv'), + index_col='tech', + )['MW'] + mttr = pd.read_csv(os.path.join(casedir, 'inputs_case', 'mttr.csv'), index_col='tech') + outage_forced = reeds.io.get_outage_hourly(casedir, 'forced') + outage_techs = outage_forced.columns.get_level_values('i').unique() + ## ReEDS2PRAS does not disaggregate batteries + battery_techs = reeds.techs.get_tech_subset_table(casedir).loc[['BATTERY']].tolist() + for check, infile in [ + ([i for i in max_cap_techs if i not in unitsize.index.tolist() + battery_techs], 'unitsize.csv'), + ([i for i in max_cap_techs if i not in mttr.index], 'mttr.csv'), + ([i for i in max_cap_techs if i not in outage_techs], 'outage_forced_hourly.h5'), + ]: + if len(check): + raise ValueError(f'{check} are missing from {infile}') + + +#%%### Procedure +def main(t, casedir, iteration=0): + #%%### DEBUGGING: Inputs + # t = 2020 + # reeds_path = os.path.expanduser('~/github2/ReEDS-2.0') + # casedir = os.path.join(reeds_path,'runs','v20230214_PRMaugurM0_Pacific_d7fIrh4_CC_y2012') + + #%%### Get inputs from ReEDS + gdx_file = os.path.join(casedir,'ReEDS_Augur','augur_data',f'reeds_data_{t}.gdx') + gdxreeds = gdxpds.to_dataframes(gdx_file) + ### Use indices as multiindex + for key in gdxreeds: + # try: + if 'i' in gdxreeds[key]: + gdxreeds[key].i = gdxreeds[key].i.str.lower() + if 'ii' in gdxreeds[key]: + gdxreeds[key].ii = gdxreeds[key].ii.str.lower() + if 't' in gdxreeds[key]: + gdxreeds[key].t = gdxreeds[key].t.astype(int) + + #%% Load other inputs from ReEDS + inputs_case = os.path.join(casedir, 'inputs_case') + + sw = reeds.io.get_switches(casedir) + sw['t'] = t + + h_dt_szn = pd.read_csv(os.path.join(inputs_case, 'rep', 'h_dt_szn.csv')) + hmap_allyrs = pd.read_csv( + os.path.join(inputs_case, 'rep', 'hmap_allyrs.csv'), + low_memory=False, + ) + hmap_allyrs['szn'] = h_dt_szn['season'].copy() + + hmap_myr_stress = pd.read_csv( + os.path.join(inputs_case, f'stress{t}i{iteration}', 'hmap_myr.csv'), + low_memory=False, + index_col='*timestamp', + parse_dates=True, + ).rename_axis('timestamp') + + h_dt_szn = h_dt_szn.set_index(['year', 'hour']) + # Add explicit timestamp index + h_dt_szn['timestamp'] = pd.to_datetime( + h_dt_szn.index.map(hmap_allyrs.set_index(['year', 'hour'])['*timestamp'])) + h_dt_szn = h_dt_szn.reset_index().set_index('timestamp') + + load = reeds.io.read_file(os.path.join(inputs_case, 'load.h5'), parse_timestamps=True) + + resources = pd.read_csv(os.path.join(inputs_case, 'resources.csv')) + recf = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) + recf.columns = pd.MultiIndex.from_tuples([tuple(x.split('|')) for x in recf.columns], + names=('i','r')) + + tech_subset_table = reeds.techs.expand_GAMS_tech_groups( + reeds.techs.get_tech_subset_table(casedir).reset_index() + ).set_index('tech_group').i + + techs_vre = tech_subset_table.loc[['VRE', 'CSP', 'PVB']].unique() + if int(sw.pras_vre_combine): + ## Group all into a single "vre" tech + techs_vre_simplify = dict(zip( + techs_vre, + ['vre']*len(techs_vre) + )) + else: + ## Strip the resource class but keep resource type; + ## e.g. "upv_5" -> "upv", "csp2_3" -> "csp" + techs_vre_simplify = dict(zip( + techs_vre, + [re.sub('\d?_\d+$', '', i) for i in techs_vre] + )) + + try: + offshore = pd.read_csv( + os.path.join(casedir, 'inputs_case', 'offshore.csv'), + header=None, + ).squeeze(1).tolist() + except pd.errors.EmptyDataError: + offshore = [] + + #%%### Set up the output containers and a few other inputs + csvout, h5out = {}, {} + + #%%### Transmission routes, capacity, and losses + if int(sw.pras_trans_contingency): + trancap_reeds = gdxreeds['cap_trans_prm'] + else: + trancap_reeds = gdxreeds['cap_trans_energy'] + + #%%### Efficiencies and storage parameters + duration = gdxreeds['storage_duration'].loc[ + gdxreeds['storage_duration'].i.isin(gdxreeds['storage_standalone'].i) + ].set_index('i').Value + + + #%%### Nameplate capacity + cap_ivr_realvint = ( + gdxreeds['cap_ivrt'].loc[gdxreeds['cap_ivrt'].t==t].drop('t', axis=1) + .groupby(['i','v','r'], as_index=False).Value.sum() + ) + ### Reset the vintages of all storage units to 'new1' to reduce model size + cap_storage_devint = cap_ivr_realvint.loc[ + cap_ivr_realvint.i.isin(gdxreeds['storage_standalone'].i)].copy() + cap_storage_devint['v'] = 'new1' + cap_storage_devint = ( + cap_storage_devint.groupby(['i','v','r'], as_index=False).Value.sum()) + + def _devint_storage(dfin): + dfout = pd.concat([ + dfin.loc[~dfin.i.isin(gdxreeds['storage_standalone'].i)], + cap_storage_devint + ], axis=0) + return dfout + + cap_ivr = _devint_storage(cap_ivr_realvint) + + #%%### Nameplate energy capacity + cap_energy_ivr = ( + gdxreeds['cap_energy_ivrt'].loc[gdxreeds['cap_energy_ivrt'].t==t].drop('t', axis=1) + .groupby(['i','v','r'], as_index=False).Value.sum() + ) + ### Reset the vintages of all storage energy capacity units to 'new1' as well + cap_energy_ivr_devint = cap_energy_ivr.loc[ + cap_energy_ivr.i.isin(gdxreeds['storage_standalone'].i)].copy() + cap_energy_ivr_devint['v'] = 'new1' + cap_energy_ivr_devint = ( + cap_energy_ivr_devint.groupby(['i','v','r'], as_index=False).Value.sum()) + cap_energy_ivr = pd.concat([ + cap_energy_ivr.loc[~cap_energy_ivr.i.isin(gdxreeds['storage_standalone'].i)], + cap_energy_ivr_devint + ], axis=0) + + #%% Remove VRE and demand-modifying techs (H2 production, DAC, DR) + demand_techs = tech_subset_table[['CONSUME', 'DR_SHED']].values + cap_nonloadtechs = cap_ivr.loc[~cap_ivr.i.isin(demand_techs)].copy() + + vretechs_i = resources.i.str.lower().unique() + cap_vre = ( + cap_ivr.loc[cap_ivr.i.str.lower().isin(vretechs_i)] + .set_index(['i','v','r']).Value.copy() + ) + + #%%### VRE generation, accounting for generation + ### Apply CF adjustment to capacity (the resulting df is not meaningful but it's only a step + ### toward the generation df). + ### Some RE techs, like CSP, don't have cf_adj_t defined in all years, + ### so fill missing values with 1, then drop rows with missing region (indicating no capacity) + cf_adj_iv = ( + gdxreeds['cf_adj_t_filt'].loc[gdxreeds['cf_adj_t_filt'].t==t].drop('t', axis=1) + .set_index(['i','v']).Value + ) + cap_vre_derated = cap_vre.multiply(cf_adj_iv, fill_value=1).reset_index().dropna() + if len(cap_vre) != len(cap_vre_derated): + raise Exception( + "CF adjustment didn't work; probably missing values in cf_adj_t_filt. " + f"len(cap_vre) = {len(cap_vre)}; len(cap_vre_derated) = {(len(cap_vre_derated))}." + ) + cap_vre_derated = cap_vre_derated.groupby(['i','r']).Value.sum() + + ### Multiply derated capacity by CF to get generation + gen_vre_ir = recf.multiply(cap_vre_derated, axis=1).dropna(axis=1) + ### Check for missing data + if gen_vre_ir.shape[1] != cap_vre_derated.shape[0]: + missing_in_gen_vre_ir = set(cap_vre_derated.index) - set(gen_vre_ir.columns) + missing_in_cap_vre_derated = set(gen_vre_ir.columns) - set(cap_vre_derated.index) + raise Exception( + f"Mismatch between VRE capacity and available CF data. " + f"Missing in gen_vre_ir: {missing_in_gen_vre_ir or 'None'}, " + f"Missing in cap_vre_derated: {missing_in_cap_vre_derated or 'None'}" + ) + ### Aggregate by model zone + gen_vre_r = gen_vre_ir.copy() + gen_vre_r = gen_vre_r.groupby(axis=1, level='r').sum() + + ### Store generation by (i,r) for capacity_credit.py + gen_vre_resources = gen_vre_ir.reindex(resources[['i','r']], axis=1).fillna(0).clip(lower=0) + + vre_gen_exist = gen_vre_resources.copy() + vre_gen_exist.columns = ['|'.join(c) for c in vre_gen_exist.columns] + vre_gen_exist.index = h_dt_szn.set_index(['ccseason','year','h','hour']).index + h5out['vre_gen_exist'] = vre_gen_exist + + ### Store generation by (i,r) for PRAS, after aggregating i if necessary + pras_vre_gen = gen_vre_resources.copy() + pras_vre_gen.columns = pd.MultiIndex.from_arrays([ + pras_vre_gen.columns.get_level_values('i').map(lambda x: techs_vre_simplify.get(x,x)), + pras_vre_gen.columns.get_level_values('r') + ]) + pras_vre_gen = pras_vre_gen.groupby(['i','r'], axis=1).sum() + + pras_vre_gen.columns = ['|'.join(c) for c in pras_vre_gen.columns] + h5out['pras_vre_gen'] = pras_vre_gen + + + ###### Store marginal CF by (i,r) for capacity_credit.py + ## Use the cf_adj_iv for the latest available vintage + cf_adj_i = cf_adj_iv.reset_index() + ## Temporarily reformat the vintage so we can select the last one + def intify(v): + try: + return int(v) + except ValueError: + return v + cf_adj_i.v = ( + cf_adj_i.v.str.replace('new','') + .map(intify) + .map(lambda x: x if str(x).startswith('init') else f'new{x:>03}') + ) + cf_adj_i = ( + cf_adj_i.sort_values(['i','v']).drop_duplicates('i', keep='last') + .set_index('i').Value + .reindex(recf.columns.get_level_values('i').unique()).fillna(1) + ) + + ### Multiply [CF] * [CF adjustment] to get marginal CF + vre_cf_marg = ( + recf.multiply(cf_adj_i, level='i', axis=1) + .reindex(resources[['i','r']], axis=1) + ) + vre_cf_marg.columns = ['|'.join(c) for c in vre_cf_marg.columns] + vre_cf_marg.index = h_dt_szn.set_index(['ccseason','year','h','hour']).index + h5out['vre_cf_marg'] = vre_cf_marg + + h_dt_szn_load_years = h_dt_szn.loc[h_dt_szn.index.isin(load.index.get_level_values('datetime'))] + + #%%### Flexible load + ### H2 and DAC: Make it all inflexible (necessary for PRAS) + load_h2dac_all_hourly = ( + gdxreeds['prod_filt'] + .groupby(['r', 'allh']).Value.sum().reset_index() + .merge(h_dt_szn_load_years[['h']].reset_index(), left_on='allh', right_on='h') + .pivot(index='timestamp', columns='r', values='Value') + .fillna(0) + .reindex(h_dt_szn_load_years.index) + ) + + #%% Load shedding + ## Get the DR shed load for all weather years + gen_h_stress = gdxreeds['gen_h_stress_filt'] + gen_shed = gen_h_stress.loc[ + (gen_h_stress['t'] == t) + & gen_h_stress['i'].isin(tech_subset_table['DR_SHED'].values) + ].groupby(['r','allh']).Value.sum().unstack('r') + ## First assign values to all timestamps in GSw_HourlyChunkLengthStress + gen_shed_combined = gen_shed.reindex(hmap_myr_stress.h.values) + gen_shed_combined.index = hmap_myr_stress.index + ## Now fill other hours with zero + gen_shed_combined = gen_shed_combined.reindex(h_dt_szn.index).fillna(0) + + #%% Flexibly sited load -> pd.Series with index = regions and missing values 0-filled + ra_cap_loadsite = ( + gdxreeds['ra_cap_loadsite'] + .loc[gdxreeds['ra_cap_loadsite']['t'] == t] + .drop(columns='t') + .set_index('r') + .squeeze(1) + .reindex(load.columns).fillna(0) + ) + + #%%### Total load and net load + ### Get Candian exports and add to this solve year's load + can_exports = ( + gdxreeds['can_exports_h_filt'] + .merge(h_dt_szn_load_years[['h']].reset_index(), left_on='allh', right_on='h') + .pivot(index='timestamp', columns='r', values='Value') + ) + load_year = load.loc[t].add(can_exports, fill_value=0) + + ### PRAS doesn't yet handle flexible load, so include all H2/DAC load in the + ### version we write for PRAS + if int(sw['pras_include_h2dac']): + print(f'Added H2/DAC to PRAS load since pras_include_h2dac = {sw.pras_include_h2dac}') + pras_load = load_year.add(load_h2dac_all_hourly, fill_value=0) + else: + pras_load = load_year.copy() + + ### Add zeros for offshore zones + for r in offshore: + pras_load[r] = 0 + ra_cap_loadsite[r] = 0 + + ### Subtract dr-shed load + if int(sw.GSw_DRShed) and not gen_shed_combined.empty: + print(f'Subtracted shed load from PRAS load since GSw_DRShed = {sw.GSw_DRShed}') + pras_load = pras_load.subtract(gen_shed_combined, fill_value=0).clip(lower=0) + + ### Add flexibly sited load if its profile is inflexible (GSw_LoadSiteCF = 1) + if ( + np.isclose(float(sw.GSw_LoadSiteCF), 1) + and len(ra_cap_loadsite) + and int(sw.GSw_LoadSiteRA) + ): + print( + f'Added CAP_LOADSITE to PRAS load since GSw_LoadSiteCF = {sw.GSw_LoadSiteCF} ' + f'and GSw_LoadSiteRA = {sw.GSw_LoadSiteRA}' + ) + pras_load += ra_cap_loadsite + + h5out['pras_load'] = pras_load + ## Include the hourly H2/DAC load for debugging + h5out['pras_h2dac_load'] = load_h2dac_all_hourly + + ### Store load with the appropriate index for capacity_credit.py + h5out['load'] = ( + load_year.merge( + h_dt_szn[['ccseason', 'year', 'h', 'hour']], left_index=True, right_index=True + ) + .set_index(['ccseason', 'year', 'h', 'hour']) + ) + + #%%### Collect some csv's for ReEDS2PRAS + ### Transmission capacity: Subset for RA according to GSw_PRMTRADE_level switch + tran_cap = ( + trancap_reeds.set_index(['r','rr','trtype']).rename(columns={'Value':'MW'})) + if sw.GSw_PRMTRADE_level != 'country': + hierarchy = reeds.io.get_hierarchy(casedir) + if sw.GSw_PRMTRADE_level == 'r': + rmap = dict(zip(hierarchy.index, hierarchy.index)) + else: + rmap = hierarchy[sw.GSw_PRMTRADE_level] + tran_cap['level'] = tran_cap.index.get_level_values('r').map(rmap) + tran_cap['levell'] = tran_cap.index.get_level_values('rr').map(rmap) + tran_cap = ( + tran_cap.loc[tran_cap.level==tran_cap.levell] + .drop(['level','levell'], axis=1) + ) + csvout['tran_cap'] = tran_cap + + ### Converter capacity: Offshore zones don't need converters since offshore generation + ## exports are assumed to be in DC, so add converter capacity to each offshore zone + ## equal to the VSC transmission capacity into / out of the zone. + ## Transmission capacity is defined in both directions and VSC is the same in both, + ## so we can just keep offshore transmission in one direction. + offshore_with_transmission = [ + r for r in offshore if r in tran_cap.index.get_level_values('r').unique() + ] + csvout['cap_converter'] = pd.concat([ + gdxreeds['cap_converter_filt'].set_index('r').rename(columns={'Value':'MW'}), + tran_cap.loc[offshore_with_transmission].groupby('r').sum(), + ]) + + ### Nameplate capacity + max_cap = cap_nonloadtechs.set_index(['i','v','r']).Value.rename('MW') + ## Drop VRE since it is handled through pras_vre_gen + max_cap = max_cap.loc[ + ~max_cap.index.get_level_values('i').str.startswith( + tuple( + list(techs_vre_simplify.keys()) + list(techs_vre_simplify.values()) + ) + ) + ].copy() + ## Aggregate geothermal + simplify_geo = dict(zip( + tech_subset_table['GEO'].values, + ['geothermal']*len(tech_subset_table['GEO']) + )) + max_cap = max_cap.rename(index=simplify_geo, level='i').groupby(['i','v','r']).sum() + + ## Storage energy capacity [MWh] = power capacity [MW] * duration [h] + energy_cap = ( + cap_storage_devint + .set_index(['i','v','r']).Value + .multiply(duration) + .rename('MWh') + ) + ## Append batteries energy capacity + energy_cap = pd.concat([ + energy_cap, + cap_energy_ivr.set_index(['i','v','r'])['Value'].rename('MWh') + ]) + energy_cap = energy_cap.round(2) + energy_cap = energy_cap[energy_cap > 0] + + # Add to max_cap any index in energy_cap that's missing, setting them to 0 + missing_index = energy_cap.index.difference(max_cap.index) + max_cap = pd.concat([max_cap, pd.Series(0, index=missing_index, name=max_cap.name)]) + + ## Drop storage with energy or power capacity below the PRAS cutoff + too_small_storage = list(set( + energy_cap.loc[energy_cap < sw['storcap_cutoff']].index.tolist() + + max_cap.loc[energy_cap.index].loc[ + max_cap.loc[energy_cap.index] < sw['storcap_cutoff']].index.tolist() + + max_cap.loc[max_cap < sw['storcap_cutoff']/2].index.tolist() + )) + + csvout['energy_cap'] = energy_cap.drop(too_small_storage, errors='ignore') + csvout['max_cap'] = max_cap.drop(too_small_storage, errors='ignore') + + ### Storage efficiency + storage_hybrid = reeds.techs.expand_GAMS_tech_groups( + reeds.techs.get_tech_subset_table(casedir).loc[['STORAGE_HYBRID']].reset_index() + ).i + storage_eff = ( + gdxreeds['storage_eff'] + .loc[gdxreeds['storage_eff'].t==t] + .set_index('i') + .Value + .rename('fraction') + ## Only keep standalone storage + .drop(storage_hybrid, errors='ignore') + ) + ## As in ReEDS LP, storage losses are applied to charging side (none for discharging) + csvout['charge_eff'] = storage_eff + csvout['discharge_eff'] = pd.Series(index=storage_eff.index, data=1., name='fraction') + + #%% Planning reserve margin in MW (sometimes used during unit disaggregation) + csvout['max_unitsize'] = ( + gdxreeds['prm'].loc[gdxreeds['prm'].t == t].set_index('r').Value + * h5out['pras_load'].max() + ).round().astype(int).rename_axis('r').rename('mw') + ## Turn off for counties by setting to zero (zeros in this file mean the max unit + ## size is not enforced for that region in ReEDS2PRAS) + agglevel_variables = reeds.spatial.get_agglevel_variables( + reeds.io.reeds_path, os.path.join(casedir, 'inputs_case') + ) + counties = agglevel_variables['county_regions'] + if len(counties): + csvout['max_unitsize'].loc[counties] = 0 + + #%% Strip water tech suffixes from water-dependent technologies + ### and change upgrade techs to the tech they are upgraded FROM. + ### It would be more natural to use the tech they are upgraded TO but in most cases + ### (e.g. for CCS and H2) we don't have empirical outage rates for the upgraded-TO tech. + watertech2tech = pd.concat([ + pd.read_csv( + os.path.join(casedir,'inputs_case','i_coolingtech_watersource_link.csv'), + usecols=['*i','ii'], + ), + pd.read_csv( + os.path.join(casedir,'inputs_case','i_coolingtech_watersource_upgrades_link.csv'), + usecols=['*i','ii'], + ), + ]).apply(lambda x: x.str.lower()).set_index('*i').squeeze(1) + + upgrade2from = ( + pd.concat([ + pd.read_csv( + os.path.join(casedir, 'inputs_case', 'upgrade_link.csv') + ).rename(columns={'*TO':'TO'}), + pd.read_csv( + os.path.join(casedir, 'inputs_case', 'upgradelink_water.csv') + ).rename(columns={'*TO-WATER':'TO', 'FROM-WATER':'FROM', 'DELTA-WATER':'DELTA'}), + ]) + .apply(lambda x: x.str.lower()) + .set_index('TO').FROM + .map(lambda x: watertech2tech.get(x,x)) + ) + watertech2tech = watertech2tech.map(lambda x: upgrade2from.get(x,x)) + + techmap = pd.concat([upgrade2from, watertech2tech]).to_dict() + + ### Simplify all the techs in output csv files and sum the capacities + for key in csvout: + indices = csvout[key].index.names + if ('i' in indices) and ('cap' in key): + csvout[key] = csvout[key].rename(index=techmap, level='i').groupby(indices).sum() + + + #%% Check for errors before sending to ReEDS2PRAS + errorcheck_reeds2pras(casedir, csvout, h5out) + + + #%%### Write it + #%% .csv files + for key in csvout: + csvout[key].round(int(sw['decimals'])).to_csv( + os.path.join(casedir,'ReEDS_Augur','augur_data',f'{key}_{t}.csv'), + ) + + #%% .h5 files + for key in h5out: + if key.startswith('pras'): + reeds.io.write_profile_to_h5( + df=h5out[key].astype(np.float32), + filename=f'{key}_{t}.h5', + outfolder=os.path.join(casedir,'ReEDS_Augur','augur_data'), + ) + else: + h5out[key].astype(np.float32).to_hdf( + os.path.join(casedir,'ReEDS_Augur','augur_data',f'{key}_{t}.h5'), + key='data', complevel=4, mode='w', + ) + + #%%### Return outputs for debugging + return csvout, h5out diff --git a/reeds/resource_adequacy/ra.py b/reeds/resource_adequacy/ra.py new file mode 100644 index 00000000..22d8ee35 --- /dev/null +++ b/reeds/resource_adequacy/ra.py @@ -0,0 +1,125 @@ +### Imports +import os +import sys +import numpy as np +import pandas as pd +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +import reeds + + +### Functions +def get_pras_eue(case, t, iteration=0): + """ + """ + ### Get PRAS outputs + dfpras = reeds.io.read_pras_results( + os.path.join(case, 'ReEDS_Augur', 'PRAS', f"PRAS_{t}i{iteration}.h5") + ) + ### Create the time index + sw = reeds.io.get_switches(case) + dfpras.index = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) + + ### Keep the EUE columns by zone + eue_tail = '_EUE' + dfeue = dfpras[[ + c for c in dfpras + if (c.endswith(eue_tail) and not c.startswith('USA')) + ]].copy() + ## Drop the tailing _EUE + dfeue = dfeue.rename( + columns=dict(zip(dfeue.columns, [c[:-len(eue_tail)] for c in dfeue]))) + + return dfeue + + +def get_eue_periods( + case, t, iteration=0, + hierarchy_level='transgrp', + stress_metric='EUE', + period_agg_method='sum', + ): + """_summary_ + + Args: + sw (pd.series): ReEDS switches for this run. + t (int): Model solve year. + iteration (int, optional): Iteration number of this solve year. Defaults to 0. + hierarchy_level (str, optional): column of hierarchy.csv specifying the spatial + level over which to calculate stress_metric. Defaults to 'country'. + stress_metric (str, optional): 'EUE' or 'NEUE'. Defaults to 'EUE'. + period_agg_method (str, optional): 'sum' or 'max', indicating how to aggregate + over the hours in each period. Defaults to 'sum'. + + Raises: + NotImplementedError: if invalid value for stress_metric or GSw_PRM_StressModel + + Returns: + pd.DataFrame: Table of periods sorted in descending order by stress metric. + """ + sw = reeds.io.get_switches(case) + ### Get the region aggregator + rmap = reeds.io.get_rmap(case=case, hierarchy_level=hierarchy_level) + + ### Get EUE from PRAS + dfeue = get_pras_eue(case=case, t=t, iteration=iteration) + ## Aggregate to hierarchy_level + dfeue = ( + dfeue + .rename_axis('r', axis=1).rename_axis('h', axis=0) + .rename(columns=rmap).groupby(axis=1, level=0).sum() + ) + + ###### Calculate the stress metric by period + if stress_metric.upper() == 'EUE': + ### Aggregate according to period_agg_method + dfmetric_period = ( + dfeue + .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) + elif stress_metric.upper() == 'NEUE': + ### Get load at hierarchy_level + dfload = reeds.io.read_h5py_file( + os.path.join( + case,'ReEDS_Augur','augur_data',f'pras_load_{t}.h5') + ).rename(columns=rmap).groupby(level=0, axis=1).sum() + dfload.index = dfeue.index + + ### Recalculate NEUE [ppm] and aggregate appropriately + if period_agg_method == 'sum': + dfmetric_period = ( + dfeue + .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) / ( + dfload + .groupby([dfload.index.year, dfload.index.month, dfload.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) * 1e6 + elif period_agg_method == 'max': + dfmetric_period = ( + (dfeue / dfload) + .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) * 1e6 + + ### Sort and drop zeros and duplicates + dfmetric_top = ( + dfmetric_period.stack('r') + .sort_values(ascending=False) + .replace(0,np.nan).dropna() + .reset_index().drop_duplicates(['y','m','d'], keep='first') + .set_index(['y','m','d','r']).squeeze(1).rename(stress_metric) + .reset_index('r') + ) + ## Convert to timestamp, then to ReEDS period + dfmetric_top['actual_period'] = [ + reeds.timeseries.timestamp2h(pd.Timestamp(*d), sw['GSw_HourlyType']).split('h')[0] + for d in dfmetric_top.index.values + ] + + return dfmetric_top diff --git a/reeds/resource_adequacy/reeds2pras/Project.toml b/reeds/resource_adequacy/reeds2pras/Project.toml new file mode 100644 index 00000000..ab41a55e --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/Project.toml @@ -0,0 +1,26 @@ +name = "ReEDS2PRAS" +uuid = "c1db39dc-6a9d-11ed-0c9a-05c3ea2a1475" +version = "2025.2.0" + +[deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +PRAS = "05348d26-1c52-11e9-35e3-9d51842d34b9" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" + +[compat] +CSV = "~0.10" +DataFrames = "~1.7" +Dates = "1" +HDF5 = "~0.17" +InlineStrings = "~1.4" +JSON = "~0.21" +PRAS = "~0.7" +Statistics = "~1.11" +TimeZones = "~1.21" +julia = "^1.11" diff --git a/reeds/resource_adequacy/reeds2pras/R2P_Test_Summary.png b/reeds/resource_adequacy/reeds2pras/R2P_Test_Summary.png new file mode 100644 index 00000000..1a4d19b7 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/R2P_Test_Summary.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a0a46d1dc10b36da8e9891630d3699d7cbd9fd6c6f3ae8dae443fe9dc9c43e8 +size 148186 diff --git a/reeds/resource_adequacy/reeds2pras/README.md b/reeds/resource_adequacy/reeds2pras/README.md new file mode 100644 index 00000000..c0ed00a9 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/README.md @@ -0,0 +1,106 @@ +# ReEDS2PRAS + +## Introduction + +The purpose of ReEDS2PRAS is to translate a ReEDS system into a PRAS system ready for probabilistic resource adequacy analysis. + +### Julia Installation + +[Juliaup](https://github.com/JuliaLang/juliaup) is a cross platform installer of the Julia programming language. +Detailed instructions to install Julia on different platforms are available from [Juliaup Installation Instructions](https://github.com/JuliaLang/juliaup?tab=readme-ov-file#installation). + +#### Mac/Linux + +Julia is included in the conda environment, so Julia does not need to be installed separately. +If you wish to install it for testing, you can run: + +```shell +curl -fsSL https://install.julialang.org | sh +``` + +#### Windows + +```shell +winget install --name Julia --id 9NJNWW8PVKMN -e -s msstore +``` + +Then, from the ReEDS-2.0 directory, run `julia --project=. instantiate.jl` to ensure proper installation of Julia and the ReEDS2PRAS environment. + +## Basic Usage + +If you have a completed ReEDS run and a REPL with ReEDS2PRAS (`using ReEDS2PRAS` after running `add ReEDS2PRAS` in the julia package manager), an example of running ReEDS2PRAS is provided below + +```julia +using ReEDS2PRAS + +reedscase = "/projects/ntps/llavin/ReEDS-2.0/runs/ntpsrerun_Xlim_DemHi_90by2035EarlyPhaseout__core" # path to completed ReEDS run +solve_year = 2035 #need ReEDS Augur data for the input solve year +weather_year = 2012 # must be 2007-2013 or 2016-2023 +timesteps = 8760 +user_descriptors = "your_user_descriptors_json_location_here" # Optional - if not passed uses default values + +# returns a parameterized PRAS system +pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, user_descriptors = user_descriptors) +``` + +This will save out a pras system to the variable `pras_system` from the ReEDS2PRAS run. The user can also save a PRAS system to a specific location using `PRAS.savemodel(pras_system, joinpath("MYPATH"*".pras")`. The saved PRAS system may then be read in by other tools like PRAS Analytics (`https://github.nrel.gov/PRAS/PRAS-Analytics`) for further analysis, post-processing, and plotting. + +## Multi-year usage + +ReEDS2PRAS can be run for multiple weather years of a completed ReEDS run by passing more than 8760 hourly timestamps. For example running all 7 weather years can be accomplished as in the below example + +```julia +using ReEDS2PRAS + +# path to completed ReEDS run +reedscase = "/projects/ntps/llavin/ReEDS-2.0/runs/ntpsrerun_Xlim_DemHi_90by2035EarlyPhaseout__core" +solve_year = 2035 #need ReEDS Augur data for the input solve year +weather_year = 2007 # must be 2007-2013 or 2016-2023 +timesteps = 61320 +user_descriptors = "your_user_descriptors_json_location_here" # Optional - if not passed uses default values + +# returns a parameterized PRAS system +pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, user_descriptors = user_descriptors) +``` + +Importantly, the timesteps count from the first hour of the first `weather_year`, so the user must input `2007` as the `weather_year` to run all 61320 hourly timesteps. + +## Testing + +When CI runners aren't available and ReEDS2PRAS isn't run automatically, it is good practice to run tests and ensure all of them pass before a PR is merged. +It is always good practice to include new tests if the current tests don't cover the functionality you've developed. +You can run ReEDS2PRAS tests by running: + +```shell +cd ReEDS-2.0/reeds2pras/test +julia --project runtests.jl +``` + +or + +```shell +cd ReEDS-2.0/reeds2pras/test +julia --project +``` + +```julia +include("runtests.jl") +``` + +The tests include PRAS SystemModel building tests and PRAS and ReEDS2PRAS benchmark tests. +If your changes increase the benchmark time by a significant amount, that might be worth investigating. + +Typical successful tests summary would look something like this: + +![Test Summary](R2P_Test_Summary.png) + +## Contributing Guidelines + +It is always a good practice to follow the Julia Style Guide (`https://docs.julialang.org/en/v1/manual/style-guide/`). +Please make sure you format your code to follow our guidelines using the snippet below before you open a PR: + +```shell +julia -e 'using Pkg; Pkg.add("JuliaFormatter"); using JuliaFormatter; include(".github/workflows/formatter-code.jl")' +``` + +**NOTE: You have to run the snippet above at the repo folder level. diff --git a/reeds/resource_adequacy/reeds2pras/src/ReEDS2PRAS.jl b/reeds/resource_adequacy/reeds2pras/src/ReEDS2PRAS.jl new file mode 100644 index 00000000..67a923a8 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/ReEDS2PRAS.jl @@ -0,0 +1,40 @@ +module ReEDS2PRAS + +# Exports +export reeds_to_pras + +# Imports +import CSV +import DataFrames +import Dates +import HDF5 +import PRAS +import Statistics +import TimeZones +import InlineStrings +import JSON + +# Includes +# Models +include("models/Region.jl") +include("models/Storage.jl") +include("models/Battery.jl") +include("models/Gen_Storage.jl") +include("models/Generator.jl") +include("models/Thermal_Gen.jl") +include("models/Variable_Gen.jl") +include("models/Line.jl") +include("models/utils.jl") + +# Utils +include("utils/reeds_input_parsing.jl") +include("utils/runchecks.jl") +include("utils/reeds_data_parsing.jl") +#Main +include("main/parse_reeds_data.jl") +include("main/create_pras_system.jl") + +# Module +include("reeds_to_pras.jl") + +end diff --git a/reeds/resource_adequacy/reeds2pras/src/main/create_pras_system.jl b/reeds/resource_adequacy/reeds2pras/src/main/create_pras_system.jl new file mode 100644 index 00000000..386fe0a0 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/main/create_pras_system.jl @@ -0,0 +1,206 @@ +""" + This function creates a PRAS (Probabilistic Resource Adequacy System) model + from a set of regions, lines, generators, storages, and generator-storages. + It takes in a vector of Region objects, a vector of Line objects, a vector + of Generator objects, a vector of Storage objects, a vector of Gen_Storage + objects, an integer timesteps representing the number of timesteps, and an + integer weather_year representing the year of the simulation. It then + creates a StepRange object for the timestamps, creates + PRAS lines and interfaces from the sorted lines and interface indices, + creates PRAS regions from the regions, creates PRAS generators from the + sorted generators and generator indices, creates PRAS storages from the + storages and storage indices, creates PRAS generator-storages from the + sorted generator-storages and generator-storage indices, and finally + returns a PRAS system model object. + + Parameters + ---------- + regions : Vector{Region} + Vector of Region objects. + lines : Vector{Line} + Vector of Line objects. + gens : Vector{<:Generator} + Vector of Generator objects. + storages : Vector{<:Storage} + Vector of Storage objects. + gen_stors : Vector{<:Gen_Storage} + Vector of Gen_Storage objects. + timesteps : Int + Number of timesteps. + weather_year : Int + Year of the simulation. + + Returns + ------- + PRAS.SystemModel + PRAS system model object. +""" +function create_pras_system( + regions::Vector{Region}, + lines::Vector{Line}, + gens::Vector{<:Generator}, + storages::Vector{<:Storage}, + gen_stors::Vector{<:Gen_Storage}, + timesteps::Int, + weather_year::Int, +) + first_ts = TimeZones.ZonedDateTime(weather_year, 01, 01, 00, TimeZones.tz"UTC") + last_ts = first_ts + Dates.Hour(timesteps - 1) + my_timestamps = StepRange(first_ts, Dates.Hour(1), last_ts) + + out = get_sorted_lines(lines, regions) + sorted_lines, interface_reg_idxs, interface_line_idxs = out + pras_lines, pras_interfaces = + make_pras_interfaces(sorted_lines, interface_reg_idxs, interface_line_idxs, regions) + pras_regions = PRAS.Regions{timesteps, PRAS.MW}( + get_name.(regions), + reduce(vcat, get_load.(regions)), + ) + ## + sorted_gens, gen_idxs = get_sorted_components(gens, get_name.(regions)) + capacity_matrix = reduce(vcat, get_capacity.(sorted_gens)) + λ_matrix = reduce(vcat, get_λ.(sorted_gens)) + μ_matrix = reduce(vcat, get_μ.(sorted_gens)) + pras_gens = PRAS.Generators{timesteps, 1, PRAS.Hour, PRAS.MW}( + get_name.(sorted_gens), + get_type.(sorted_gens), + capacity_matrix, + λ_matrix, + μ_matrix, + ) + ## + storages, stor_idxs = get_sorted_components(storages, regions) + + stor_names = isempty(get_name.(storages)) ? String[] : get_name.(storages) + stor_types = isempty(get_type.(storages)) ? String[] : get_type.(storages) + stor_charge_cap_array = reduce( + vcat, + get_charge_capacity.(storages), + init = Matrix{Int64}(undef, 0, timesteps), + ) + stor_discharge_cap_array = reduce( + vcat, + get_discharge_capacity.(storages), + init = Matrix{Int64}(undef, 0, timesteps), + ) + stor_energy_cap_array = reduce( + vcat, + get_energy_capacity.(storages), + init = Matrix{Int64}(undef, 0, timesteps), + ) + stor_chrg_eff_array = reduce( + vcat, + get_charge_efficiency.(storages), + init = Matrix{Float64}(undef, 0, timesteps), + ) + stor_dischrg_eff_array = reduce( + vcat, + get_discharge_efficiency.(storages), + init = Matrix{Float64}(undef, 0, timesteps), + ) + stor_cryovr_eff = reduce( + vcat, + get_carryover_efficiency.(storages), + init = Matrix{Float64}(undef, 0, timesteps), + ) + λ_stor = reduce(vcat, get_λ.(storages), init = Matrix{Float64}(undef, 0, timesteps)) + μ_stor = reduce(vcat, get_μ.(storages), init = Matrix{Float64}(undef, 0, timesteps)) + pras_storages = PRAS.Storages{timesteps, 1, PRAS.Hour, PRAS.MW, PRAS.MWh}( + stor_names, + stor_types, + stor_charge_cap_array, + stor_discharge_cap_array, + stor_energy_cap_array, + stor_chrg_eff_array, + stor_dischrg_eff_array, + stor_cryovr_eff, + λ_stor, + μ_stor, + ) + ## + sorted_gen_stors, genstor_idxs = get_sorted_components(gen_stors, regions) + + gen_stor_names = + isempty(get_name.(sorted_gen_stors)) ? String[] : get_name.(sorted_gen_stors) + gen_stor_cats = + isempty(get_type.(sorted_gen_stors)) ? String[] : get_type.(sorted_gen_stors) + gen_stor_cap_array = reduce( + vcat, + get_charge_capacity.(sorted_gen_stors), + init = Matrix{Int64}(undef, 0, timesteps), + ) + gen_stor_dis_cap_array = reduce( + vcat, + get_discharge_capacity.(sorted_gen_stors), + init = Matrix{Int64}(undef, 0, timesteps), + ) + gen_stor_enrgy_cap_array = reduce( + vcat, + get_energy_capacity.(sorted_gen_stors), + init = Matrix{Int64}(undef, 0, timesteps), + ) + gen_stor_chrg_eff_array = reduce( + vcat, + get_charge_efficiency.(sorted_gen_stors), + init = Matrix{Float64}(undef, 0, timesteps), + ) + gen_stor_dischrg_eff_array = reduce( + vcat, + get_discharge_efficiency.(sorted_gen_stors), + init = Matrix{Float64}(undef, 0, timesteps), + ) + gen_stor_carryovr_eff_array = reduce( + vcat, + get_carryover_efficiency.(sorted_gen_stors), + init = Matrix{Float64}(undef, 0, timesteps), + ) + gen_stor_inflow_array = reduce( + vcat, + get_inflow.(sorted_gen_stors), + init = Matrix{Int64}(undef, 0, timesteps), + ) + gen_stor_grid_withdrawl_array = reduce( + vcat, + get_grid_withdrawl_capacity.(sorted_gen_stors), + init = Matrix{Int64}(undef, 0, timesteps), + ) + gen_stor_grid_inj_array = reduce( + vcat, + get_grid_injection_capacity.(sorted_gen_stors), + init = Matrix{Int64}(undef, 0, timesteps), + ) + gen_stor_λ = + reduce(vcat, get_λ.(sorted_gen_stors), init = Matrix{Float64}(undef, 0, timesteps)) + gen_stor_μ = + reduce(vcat, get_μ.(sorted_gen_stors), init = Matrix{Float64}(undef, 0, timesteps)) + + gen_stors = PRAS.GeneratorStorages{timesteps, 1, PRAS.Hour, PRAS.MW, PRAS.MWh}( + gen_stor_names, + gen_stor_cats, + gen_stor_cap_array, + gen_stor_dis_cap_array, + gen_stor_enrgy_cap_array, + gen_stor_chrg_eff_array, + gen_stor_dischrg_eff_array, + gen_stor_carryovr_eff_array, + gen_stor_inflow_array, + gen_stor_grid_withdrawl_array, + gen_stor_grid_inj_array, + gen_stor_λ, + gen_stor_μ, + ) + + return PRAS.SystemModel( + pras_regions, + pras_interfaces, + pras_gens, + gen_idxs, + pras_storages, + stor_idxs, + gen_stors, + genstor_idxs, + pras_lines, + interface_line_idxs, + my_timestamps, + ) +end diff --git a/reeds/resource_adequacy/reeds2pras/src/main/parse_reeds_data.jl b/reeds/resource_adequacy/reeds2pras/src/main/parse_reeds_data.jl new file mode 100644 index 00000000..42daeb6e --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/main/parse_reeds_data.jl @@ -0,0 +1,133 @@ +""" + This function creates PRAS objects based on data from the ReEDS + capacity expansion model. It processes regional load profiles, + interregional transmission lines, thermal generators, + storages, and GeneratorStorages into arrays suitable for creating + a PRAS system. + + Parameters + ---------- + ReEDS_data : ReEDSdatapaths + data paths with specific ReEDS file paths + timesteps : Int + Number of timesteps + solve_year : Int + ReEDS solve year + scheduled_outage : Bool, + Flag to read the scheduled_outage_hourly file (if available) + Returns + ------- + lines : Array{Line} + contains Line objects + regions : Array{Region} + contains Region objects + gens_array : Array{VegGen} + contains VG Gen objects + storage_array : Array{Storage} + contains Storage objects + genstor_array : Array{GeneratorStorage} + contains GeneratorStorage objects +""" +function parse_reeds_data( + ReEDS_data::ReEDSdatapaths, + timesteps::Int, + solve_year::Int; + hydro_energylim = false, + scheduled_outage = false, + pras_agg_ogs_lfillgas = false, + pras_existing_unit_size = true, + pras_max_unitsize_prm = true, +) + @info "Processing regions and associating load profiles..." + region_array = process_regions_and_load(ReEDS_data) + + @info "Processing lines and adding VSC-related regions, if applicable..." + lines = process_lines(ReEDS_data, get_name.(region_array), timesteps) + lines, regions = process_vsc_lines(lines, region_array) + + # Create Generator Objects + # **TODO: Should 0 MW generators be allowed after disaggregation? + # **TODO: is it important to also handle planned outages? + @info( + "splitting thermal, storage, variable and hydro generator types from installed " * + "ReEDS capacities..." + ) + thermal_builds, storage, hydro_disp_gens, hydro_non_disp_gens = + split_generator_types(ReEDS_data) + + @info "reading in ReEDS generator-type forced outage data..." + forced_outage_data = get_forced_outage_data(ReEDS_data) + FOR_dict = Dict(forced_outage_data[!, "ResourceType"] .=> forced_outage_data[!, "FOR"]) + + @info "reading hourly forced outage rates" + forcedoutage_hourly = get_hourly_forced_outage_data(ReEDS_data) + + @info "reading in ATB unit size data for use with disaggregation..." + unitsize_data = get_unitsize_mapping(ReEDS_data) + unitsize_dict = Dict(unitsize_data[!, "tech"] .=> unitsize_data[!, "MW"]) + + scheduled_outage_hourly = nothing + # read the scheduled_outage CSV file if scheduled_outage + if scheduled_outage + @info "reading hourly scheduled outage rates..." + scheduled_outage_hourly = get_hourly_scheduled_outage_data(ReEDS_data) + end + + @info "reading MTTR data" + mttr_dict = get_MTTR_data(ReEDS_data) + + @info "Processing conventional/thermal generators..." + thermal_gens = process_thermals_with_disaggregation( + ReEDS_data, + thermal_builds, + FOR_dict, + forcedoutage_hourly, + unitsize_dict, + timesteps, + solve_year, + mttr_dict, + scheduled_outage_hourly = scheduled_outage_hourly, + pras_agg_ogs_lfillgas = pras_agg_ogs_lfillgas, + pras_existing_unit_size = pras_existing_unit_size, + pras_max_unitsize_prm = pras_max_unitsize_prm, + ) + @info "Processing variable generation..." + gens_array = process_vg( + thermal_gens, + ReEDS_data, + ) + + @info "Processing Storages..." + @debug "storage is $(storage)" + storage_array = process_storages( + storage, + FOR_dict, + forcedoutage_hourly, + unitsize_dict, + ReEDS_data, + timesteps, + mttr_dict, + scheduled_outage_hourly = scheduled_outage_hourly, + ) + + @info "Processing hydroelectric generators..." + gens_array, genstor_array = process_hydro( + gens_array, + hydro_disp_gens, + hydro_non_disp_gens, + FOR_dict, + forcedoutage_hourly, + ReEDS_data, + solve_year, + timesteps, + mttr_dict, + unitsize_dict, + scheduled_outage_hourly = scheduled_outage_hourly, + hydro_energylim = hydro_energylim, + ) + + #@info "Processing GeneratorStorages" + #genstor_array = process_genstors(genstor_array, get_name.(regions), timesteps) + + return lines, regions, gens_array, storage_array, genstor_array +end diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Battery.jl b/reeds/resource_adequacy/reeds2pras/src/models/Battery.jl new file mode 100644 index 00000000..0bb86a3e --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Battery.jl @@ -0,0 +1,130 @@ +""" + This code defines a struct called Battery which is a subtype of + Storage. The struct has 13 fields: name, timesteps, region_name, type, + charge_cap, discharge_cap, energy_cap, legacy, charge_eff, + discharge_eff, carryover_eff, FOR and MTTR. The code also includes an + inner constructor and checks to ensure that the values passed are + valid. The constructor checks that charge_cap and discharge_cap are greater than 0.0, + energy_cap is greater than 0.0, legacy is either "Existing" or "New", all of the + efficiency values are between 0.0 and 1.0 (inclusive), FOR is between 0.0 + and 1.0 (inclusive) and MTTR is greater than 0. If any of these checks fail + an error will be thrown. + + Parameters + ---------- + name : String + The name of the battery + timesteps : Int64 + Number of PRAS timesteps + region_name: String + Name of the region + type : String + Type of battery + charge_cap : Float64 + Charge capacity + discharge_cap : Float64 + Discharge capacity + energy_cap : Float64 + Energy capacity + legacy : String + Battery's legacy (existing or new) + charge_eff : Float64 + Charge efficiency + discharge_eff : Float64 + Discharge efficiency + carryover_eff : Float64 + Carryover efficiency + FOR : Float64 + Factor of restoration + SOR : Vector{Float32} + Scheduled Outage Rate + MTTR : Int64 + Mean time to restore + + Returns + ------- + Struct with properties related to the batter +""" +struct Battery <: Storage + name::String + timesteps::Int64 + region_name::String + type::String + charge_cap::Float64 + discharge_cap::Float64 + energy_cap::Vector{Float64} + legacy::String + charge_eff::Float64 + discharge_eff::Float64 + carryover_eff::Float64 + FOR::Vector{Float32} + SOR::Vector{Float32} + MTTR::Int64 + + # Inner Constructors & Checks + function Battery(; + name = "init_name", + timesteps = 8760, + region_name = "init_name", + type = "init_type", + charge_cap = 1.0, + discharge_cap = 1.0, + energy_cap = fill(4.0, timesteps), + legacy = "New", + charge_eff = 1.0, + discharge_eff = 1.0, + carryover_eff = 1.0, + FOR = zeros(Float32, timesteps), + SOR = zeros(Float32, timesteps), + MTTR = 24, + ) + @debug "cap_P = $(discharge_cap) MW and cap_E = $(energy_cap) MWh" + + charge_cap > 0.0 || error( + "Charge capacity passed is not allowed (should be > 0.0) : $(name) - $(charge_cap) MW", + ) + + discharge_cap > 0.0 || error( + "Discharge capacity passed is not allowed (should be > 0.0) : $(name) - $(discharge_cap) MW", + ) + + all(energy_cap .> 0.0) || error( + "Energy capacity passed is not allowed (should be > 0.0) : $(name) - $(energy_cap) MWh", + ) + + legacy in ["Existing", "New"] || + error("$(name) has legacy $(legacy) which is not in [Existing, New]") + + all(0.0 .<= [charge_eff, discharge_eff, carryover_eff] .<= 1.0) || + error("$(name) charge/discharge/carryover efficiency value is < 0.0 or > 1.0") + + all(0.0 .<= FOR .<= 1.0) || error("$(name) FOR value is < 0 or > 1") + + MTTR > 0 || error("$(name) MTTR value is <= 0") + + return new( + name, + timesteps, + region_name, + type, + charge_cap, + discharge_cap, + energy_cap, + legacy, + charge_eff, + discharge_eff, + carryover_eff, + FOR, + SOR, + MTTR, + ) + end +end + +# Getter Functions + +get_charge_capacity(stor::Battery) = fill(round(Int, stor.charge_cap), 1, stor.timesteps) + +get_discharge_capacity(stor::Battery) = + fill(round(Int, stor.discharge_cap), 1, stor.timesteps) + diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Gen_Storage.jl b/reeds/resource_adequacy/reeds2pras/src/models/Gen_Storage.jl new file mode 100644 index 00000000..58144dde --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Gen_Storage.jl @@ -0,0 +1,166 @@ +""" + This code defines a struct called Gen_Storage which is a subtype of + Storage. The struct has 14 fields: name, timesteps, region_name, type, + charge_cap, discharge_cap, energy_cap, inflow, grid_withdrawl_cap, + grid_inj_cap, legacy, charge_eff, discharge_eff and carryover_eff. The + code also contains an inner constructor and checks to ensure that the + values passed are valid. Specifically, all capacity values must be + greater than or equal to 0.0,the legacy value must either be “Existing” + or “New”, all of the efficiency values must be between 0.0 and 1.0 (inclusive), + FOR must be between 0.0 and 1.0 (inclusive) and MTTR must be greater than 0. + Additionally, there is a commented out check that verifies that all of + the time series data is of the same size. Finally, if any of these + checks fail, an error will be thrown. + + Parameters + ---------- + name : string + Name of Gen_Storage + timesteps : integer + PRAS timesteps (timesteps) + region_name : string + Region name + type : string + Storage type + charge_cap : float + Charge capacity + discharge_cap : float + Discharge capacity + energy_cap : float + Energy capacity + inflow : float + Inflow time series data + grid_withdrawl_cap : float + Grid withdrawal capacity time series data + grid_inj_cap : floating point + Grid injection capacity time series data + legacy : string + Must be either "Existing" or "New" + charge_eff : float + Charge efficiency + discharge_eff : float + Discharge efficiency + carryover_eff : float + Carryover efficiency + FOR : float + Forced Outage Rate value + SOR : Vector{Float32} + Scheduled Outage Rate + MTTR : integer + Mean Time To Repair value + + Returns + ------- + A new instance of Gen_Storage. +""" +struct Gen_Storage <: Storage + name::String + timesteps::Int64 + region_name::String + type::String + charge_cap::Vector{Float64} + discharge_cap::Vector{Float64} + energy_cap::Vector{Float64} + inflow::Vector{Float64} + grid_withdrawl_cap::Vector{Float64} + grid_inj_cap::Vector{Float64} + legacy::String + charge_eff::Float64 + discharge_eff::Float64 + carryover_eff::Float64 + FOR::Vector{Float32} + SOR::Vector{Float32} + MTTR::Int64 + + # Inner Constructors & Checks + function Gen_Storage(; + name = "init_name", + timesteps = 8760, + region_name = "init_name", + type = "init_type", + charge_cap = zeros(Float64, timesteps), + discharge_cap = zeros(Float64, timesteps), + energy_cap = zeros(Float64, timesteps), + inflow = zeros(Float64, timesteps), + grid_withdrawl_cap = zeros(Float64, timesteps), + grid_inj_cap = zeros(Float64, timesteps), + legacy = "New", + charge_eff = 1.0, + discharge_eff = 1.0, + carryover_eff = 1.0, + FOR = zeros(Float32, timesteps), + SOR = zeros(Float32, timesteps), + MTTR = 24, + ) + all(charge_cap .>= 0.0) || error( + "Charge capacity passed is not allowed (should be >= 0.0) : $(name) - $(charge_cap) MW", + ) + + all(discharge_cap .>= 0.0) || error( + "Discharge capacity passed is not allowed (should be >= 0.0) : $(name) - $(discharge_cap) MW", + ) + + all(energy_cap .>= 0.0) || error( + "Energy capacity passed is not allowed (should be >= 0.0) : $(name) - $(energy_cap) MWh", + ) + + all(inflow .>= 0.0) || error( + "Inflow passed is not allowed (should be >= 0.0) : $(name) - $(inflow) MW", + ) + + all(grid_withdrawl_cap .>= 0.0) || error( + "Grid withdrawl capacity passed is not allowed (should be >= 0.0) : $(name) - $(grid_withdrawl_cap) MW", + ) + + all(grid_inj_cap .>= 0.0) || error( + "Grid injection capacity passed is not allowed (should be >= 0.0) : $(name) - $(grid_inj_cap) MW", + ) + + legacy in ["Existing", "New"] || + error("$(name) has legacy $(legacy) which is not in [Existing, New]") + + all(0.0 .<= [charge_eff, discharge_eff, carryover_eff] .<= 1.0) || + error("$(name) charge/discharge/carryover efficiency value is < 0.0 or > 1.0") + + all(0.0 .<= FOR .<= 1.0) || error("$(name) FOR value is < 0 or > 1") + + MTTR > 0 || error("$(name) MTTR value is <= 0") + + return new( + name, + timesteps, + region_name, + type, + charge_cap, + discharge_cap, + energy_cap, + inflow, + grid_withdrawl_cap, + grid_inj_cap, + legacy, + charge_eff, + discharge_eff, + carryover_eff, + FOR, + SOR, + MTTR, + ) + end +end + +# Getter Functions + +#TODO: is SOR for grid injection enough? Do we need to derate energy capacity too? + +get_charge_capacity(stor::Gen_Storage) = permutedims(round.(Int, stor.charge_cap)) + +get_discharge_capacity(stor::Gen_Storage) = permutedims(round.(Int, stor.discharge_cap)) + +get_inflow(stor::Gen_Storage) = permutedims(round.(Int, stor.inflow)) + +get_grid_withdrawl_capacity(stor::Gen_Storage) = + permutedims(round.(Int, stor.grid_withdrawl_cap)) + + +get_grid_injection_capacity(stor::Gen_Storage) = + permutedims(round.(Int, stor.grid_inj_cap .* (1 .-stor.SOR))) \ No newline at end of file diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Generator.jl b/reeds/resource_adequacy/reeds2pras/src/models/Generator.jl new file mode 100644 index 00000000..be3a2d75 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Generator.jl @@ -0,0 +1,46 @@ +abstract type Generator end + +# Getter Functions +get_name(gen::Generator) = gen.name + +get_legacy(gen::Generator) = gen.legacy + +# Helper Functions +get_outage_rate(gen::Generator) = outage_to_rate(gen.FOR, gen.MTTR) + +function get_λ(gen::Generator) + λ = getfield(get_outage_rate(gen), :λ) + if (isa(λ, Float64)) + out = fill(λ, 1, gen.timesteps) + else + out = reshape(λ, 1, :) + end + return out +end + +get_μ(gen::Generator) = fill(getfield(get_outage_rate(gen), :μ), 1, gen.timesteps) + +function get_generators_in_region(gens::Vector{<:Generator}, reg_name::String) + reg_gens = filter(gen -> gen.region_name == reg_name, gens) + if isempty(reg_gens) + @warn "No generators in region: $(reg_name)" + return Generator[] + else + return reg_gens + end +end + +get_generators_in_region(gens::Vector{<:Generator}, reg::Region) = + get_generators_in_region(gens, reg.name) + +function get_legacy_generators(gens::Vector{<:Generator}, leg::String) + leg in ["Existing", "New"] || error("Unidentified legacy passed") + + leg_gens = filter(gen -> gen.legacy == leg, gens) + if isempty(leg_gens) + @warn "No generators with legacy: $(leg)" + return Generator[] + else + return leg_gens + end +end diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Interface.jl b/reeds/resource_adequacy/reeds2pras/src/models/Interface.jl new file mode 100644 index 00000000..c73a7edb --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Interface.jl @@ -0,0 +1,150 @@ +""" + Constructs a model of a transmission interface (a group of lines). + + Parameters + ---------- + name : String + Name of interface object + timesteps : Int64 + Number of timeseries for the same identifier (assumed same for all + type of objects) + regions_from : String + Origin region of the line object + region_to : String + Destination region of the line object + forward_cap : Float64 + Capacity of the line object in forward direction + backward_cap : Float64 + Capacity of the line object in backward direction + legacy : String + Check whether it is existing or new i.e., RET or TEST + FOR : Float64 + Forced Outage Rate of the line object + MTTR : Int64 + Mean Time to Repair of the line object + VSC : Bool + Voltage Source Converter of the line object + converter_capacity : Dict{String, Float64} + Dictionary that stores the VSC capacities within Region_From and + Region_To + + Returns + ------- + Out: Line + A new instance of Line object as defined above + +""" +struct Line + name::String + timesteps::Int64 + category::String + region_from::String + region_to::String + forward_cap::Float64 + backward_cap::Float64 + legacy::String + FOR::Float64 + MTTR::Int64 + VSC::Bool + converter_capacity::Dict{String, Float64} + + # Inner Constructors & Checks + function Line(; + name = "init_name", + timesteps = 8760, + category = "AC", + region_from = "init_reg_from", + region_to = "init_reg_to", + forward_cap = 0.0, + backward_cap = 0.0, + legacy = "New", + FOR = 0.0, + MTTR = 24, + VSC = false, + converter_capacity = Dict(region_from => 0.0, region_to => 0.0), + ) + category in ReEDS_LINE_TYPES || + error("$(name) has category $(category) which is not in $(ReEDS_LINE_TYPES)") + + ~(region_from == region_to) || + error("Region_From and Region_To cannot be the same for $(name). PRAS only + considers inter-regional lines in zonal analysis") + + all([forward_cap, backward_cap] .>= 0.0) || + error("$(name) forward/backward capacity value < 0") + + legacy in ["Existing", "New"] || + error("$(name) has legacy $(legacy) which is not in [Existing, New]") + + 0.0 <= FOR <= 1.0 || error("$(name) FOR value is < 0 or > 1") + + MTTR > 0 || error("$(name) MTTR value is <= 0") + + all([region_from, region_to] .∈ Ref(keys(converter_capacity))) || + error("Check the keys of converter capacity dictionary for VSC DC + line: $(name)") + + return new( + name, + timesteps, + category, + region_from, + region_to, + forward_cap, + backward_cap, + legacy, + FOR, + MTTR, + VSC, + converter_capacity, + ) + end +end + +# Getter Functions + +get_name(ln::Line) = ln.name + +get_category(ln::Line) = ln.category + +get_forward_capacity(ln::Line) = fill(round(Int, ln.forward_cap), 1, ln.timesteps) + +get_backward_capacity(ln::Line) = fill(round(Int, ln.backward_cap), 1, ln.timesteps) + +get_region_from(ln::Line) = ln.region_from + +get_region_to(ln::Line) = ln.region_to + +#Helper functions + +get_outage_rate(ln::Line) = outage_to_rate(ln.FOR, ln.MTTR) + +get_λ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :λ), 1, ln.timesteps) + +get_μ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :μ), 1, ln.timesteps) + +""" + Return all lines with the specified legacy. + + Parameters + ---------- + lines : Vector{<:Line} + List of Lines to filter through. + leg : String + Legacy of Line, either 'Existing' or 'New'. + + Returns + ------- + Vector{<:Line} + List of all lines with the specified legacy. +""" +function get_legacy_lines(lines::Vector{Line}, leg::String) + leg in ["Existing", "New"] || error("Unidentified legacy passed") + + leg_lines = filter(ln -> ln.legacy == leg, lines) + if isempty(leg_lines) + # @warn "No lines with legacy: $(leg)" + else + return leg_lines + end +end diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Line.jl b/reeds/resource_adequacy/reeds2pras/src/models/Line.jl new file mode 100644 index 00000000..37d97653 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Line.jl @@ -0,0 +1,155 @@ +const ReEDS_LINE_TYPES = ["AC", "B2B", "LCC", "VSC", "VSC DC-AC converter"] + +""" + Constructs a model of LINE. + + Parameters + ---------- + name : String + Name chose for a line object + timesteps : Int64 + Number of timeseries for the same identifier (assumed same for all + type of objects) + category : String + Type of Line object. Category can be one of AC/B2B/LCC/VSC/VSC + DC-AC converter + region_from : String + Origin region of the line object + region_to : String + Destination region of the line object + forward_cap : Float64 + Capacity of the line object in forward direction + backward_cap : Float64 + Capacity of the line object in backward direction + legacy : String + Check whether it is existing or new i.e., RET or TEST + FOR : Float64 + Forced Outage Rate of the line object + MTTR : Int64 + Mean Time to Repair of the line object + VSC : Bool + Voltage Source Converter of the line object + converter_capacity : Dict{String, Float64} + Dictionary that stores the VSC capacities within Region_From and + Region_To + + Returns + ------- + Out: Line + A new instance of Line object as defined above + +""" +struct Line + name::String + timesteps::Int64 + category::String + region_from::String + region_to::String + forward_cap::Float64 + backward_cap::Float64 + legacy::String + FOR::Float64 + MTTR::Int64 + VSC::Bool + converter_capacity::Dict{String, Float64} + + # Inner Constructors & Checks + function Line(; + name = "init_name", + timesteps = 8760, + category = "AC", + region_from = "init_reg_from", + region_to = "init_reg_to", + forward_cap = 0.0, + backward_cap = 0.0, + legacy = "New", + FOR = 0.0, + MTTR = 24, + VSC = false, + converter_capacity = Dict(region_from => 0.0, region_to => 0.0), + ) + category in ReEDS_LINE_TYPES || + error("$(name) has category $(category) which is not in $(ReEDS_LINE_TYPES)") + + ~(region_from == region_to) || + error("Region_From and Region_To cannot be the same for $(name). PRAS only + considers inter-regional lines in zonal analysis") + + all([forward_cap, backward_cap] .>= 0.0) || + error("$(name) forward/backward capacity value < 0") + + legacy in ["Existing", "New"] || + error("$(name) has legacy $(legacy) which is not in [Existing, New]") + + 0.0 <= FOR <= 1.0 || error("$(name) FOR value is < 0 or > 1") + + MTTR > 0 || error("$(name) MTTR value is <= 0") + + all([region_from, region_to] .∈ Ref(keys(converter_capacity))) || + error("Check the keys of converter capacity dictionary for VSC DC + line: $(name)") + + return new( + name, + timesteps, + category, + region_from, + region_to, + forward_cap, + backward_cap, + legacy, + FOR, + MTTR, + VSC, + converter_capacity, + ) + end +end + +# Getter Functions + +get_name(ln::Line) = ln.name + +get_category(ln::Line) = ln.category + +get_forward_capacity(ln::Line) = fill(round(Int, ln.forward_cap), 1, ln.timesteps) + +get_backward_capacity(ln::Line) = fill(round(Int, ln.backward_cap), 1, ln.timesteps) + +get_region_from(ln::Line) = ln.region_from + +get_region_to(ln::Line) = ln.region_to + +#Helper functions + +get_outage_rate(ln::Line) = outage_to_rate(ln.FOR, ln.MTTR) + +get_λ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :λ), 1, ln.timesteps) + +get_μ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :μ), 1, ln.timesteps) + +""" + Return all lines with the specified legacy. + + Parameters + ---------- + lines : Vector{<:Line} + List of Lines to filter through. + leg : String + Legacy of Line, either 'Existing' or 'New'. + + Returns + ------- + Vector{<:Line} + List of all lines with the specified legacy. +""" +function get_legacy_lines(lines::Vector{Line}, leg::String) + leg in ["Existing", "New"] || error("Unidentified legacy passed") + + leg_lines = filter(ln -> ln.legacy == leg, lines) + if isempty(leg_lines) + # @warn "No lines with legacy: $(leg)" + else + return leg_lines + end +end diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Region.jl b/reeds/resource_adequacy/reeds2pras/src/models/Region.jl new file mode 100644 index 00000000..883d092f --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Region.jl @@ -0,0 +1,45 @@ +""" + Constructs the ReEDS2PRAS Region type. Region objects have three main + attributes - name (String), timesteps (Int64) and load + (Vector{Float64}). The load attribute represents the region's total + power demand data over N intervals of measure given in MW, which must + always be greater than 0. + + Parameters + ---------- + name : String + Name to give the region. + timesteps : Int64 + Number of PRAS timesteps. + load : Vector{Float64} + Time series data for the region's total power demand must match N + in length. + + Returns + ------- + A new instance of the PRAS Region type. +""" +struct Region + name::String + timesteps::Int64 + load::Vector{Float64} + + # Inner Constructors & Checks + function Region(name, timesteps, load = zeros(Float64, timesteps)) + length(load) == timesteps || error( + "The length of the region $(name) load time series data is $(length(load)) but it should be + equal to PRAS timesteps ($(timesteps))", + ) + + all(load .>= 0.0) || + error("Check for negative values in region $(name) load time series data.") + + return new(name, timesteps, load) + end +end + +# Getter Functions + +get_name(reg::Region) = reg.name + +get_load(reg::Region) = permutedims(round.(Int, reg.load)) diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Storage.jl b/reeds/resource_adequacy/reeds2pras/src/models/Storage.jl new file mode 100644 index 00000000..cdbf7ced --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Storage.jl @@ -0,0 +1,96 @@ +abstract type Storage end + +# Getter Functions + +get_name(stor::Storage) = stor.name + +get_type(stor::Storage) = stor.type + +get_legacy(stor::Storage) = stor.legacy + +get_energy_capacity(stor::Storage) = permutedims(round.(Int, stor.energy_cap .* (1 .-stor.SOR))) + +get_charge_efficiency(stor::Storage) = fill(stor.charge_eff, 1, stor.timesteps) + +get_discharge_efficiency(stor::Storage) = fill(stor.discharge_eff, 1, stor.timesteps) + +get_carryover_efficiency(stor::Storage) = fill(stor.carryover_eff, 1, stor.timesteps) + +# Helper Functions +get_outage_rate(stor::Storage) = outage_to_rate(stor.FOR, stor.MTTR) + +function get_λ(stor::Storage) + λ = getfield(get_outage_rate(stor), :λ) + if (isa(λ, Float64)) + out = fill(λ, 1, stor.timesteps) + else + out = reshape(λ, 1, :) + end + return out +end + +get_μ(stor::Storage) = fill(getfield(get_outage_rate(stor), :μ), 1, stor.timesteps) + +get_category(stor::Storage) = "$(stor.legacy)|$(stor.type)" + +""" + This function searches an array stors of type Vector{<:Storage} for + storages located in a specific region reg_name. First, it filters the array + for storages with a region_name field equal to the region name given. If no + such storages exist, a warning is issued and an empty array of type + Storage[] is returned. Otherwise, an array containing all the storages from + this region is returned. + + Parameters + ---------- + stors : Vector{<:Storage} + An array of instances of type Storage. + reg_name : String + The name of the region to search for in storages. + + Returns + ------- + reg_stors : Vector{<:Storage} + An array of Storage instances found in the specified region reg_name. +""" +function get_storages_in_region(stors::Vector{<:Storage}, reg_name::String) + reg_stors = filter(stor -> stor.region_name == reg_name, stors) + if isempty(reg_stors) + @debug "No storages in region: $(reg_name)" + return Storage[] + else + return reg_stors + end +end + +get_storages_in_region(stors::Vector{<:Storage}, reg::Region) = + get_storages_in_region(stors, reg.name) + +""" + Get the storage objects which match a given legacy ('Existing' or 'New'). + + Parameters + ---------- + stors: Vector{<:Storage} + The array of storage objects. + leg: str + Legacy of the storage objects. Accepted values are 'Existing' and + 'New'. + + Returns + ------- + leg_stors: <:Storage + A subset of ``stors`` that has matching legacy. + Returns an empty array if there is no match. +""" +function get_legacy_storages(stors::Vector{<:Storage}, leg::String) + leg in ["Existing", "New"] || error("Unidentified legacy passed") + + leg_stors = filter(stor -> stor.legacy == leg, stors) + if isempty(leg_stors) + @debug "No storages with legacy: $(leg)" + return Storage[] + else + return leg_stors + end +end diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Thermal_Gen.jl b/reeds/resource_adequacy/reeds2pras/src/models/Thermal_Gen.jl new file mode 100644 index 00000000..574336a0 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Thermal_Gen.jl @@ -0,0 +1,82 @@ +""" + This function is used to define a thermal generator in the model. It + contains one struct and an inner constructor to check if the inputs are + valid. + + Parameters + ---------- + name : String + The name of the generator. + timesteps : Int64 + Number of timesteps in the PRAS problem. + region_name : String + Name of the region associated with this generator. + capacity : Float64 + Capacity of the generator. + fuel : String + Fuel type of the generator (default "OT"). + legacy : String + Existing or New generator (default "New"). + FOR : Vector{Float32} + Forced Outage Rate (default 0.0). + SOR : Vector{Float32} + Scheduled Outage Rate (default 0.0). + MTTR : Int64 + Mean Time To Repair/Replace (default 24). + + Returns + ------- + An instance of a Thermal_Gen. +""" +struct Thermal_Gen <: Generator + name::String + timesteps::Int64 + region_name::String + capacity::Float64 + fuel::String + legacy::String + FOR::Vector{Float32} + SOR::Vector{Float32} + MTTR::Int64 + + # Inner Constructors & Checks + function Thermal_Gen(; + name = "init_name", + timesteps = 8760, + region_name = "init_name", + capacity = 10.0, + fuel = "OT", + legacy = "New", + FOR = zeros(Float32, timesteps), + SOR = zeros(Float32, timesteps), + MTTR = 24, + ) + capacity >= 0.0 || error("$(name) capacity value passed is < 0") + + legacy in ["Existing", "New"] || + error("$(name) has legacy $(legacy) which is not in [Existing, New]") + + length(FOR) == timesteps || + error("The length of the $(name) FOR time series data is $(length(FOR)) + but it should be should be equal to PRAS timesteps ($(timesteps))") + + if !isnothing(SOR) + length(SOR) == timesteps || + error("The length of the $(name) SOR time series data is $(length(SOR)) + but it should be should be equal to PRAS timesteps ($(timesteps))") + end + + MTTR > 0 || error("$(name) MTTR value is <= 0") + + return new(name, timesteps, region_name, capacity, fuel, legacy, FOR, SOR, MTTR) + end +end + +# Getter Functions +get_capacity(gen::Thermal_Gen) = round.(Int, gen.capacity * (1 .-gen.SOR)') + +get_fuel(gen::Thermal_Gen) = gen.fuel + +get_category(gen::Thermal_Gen) = "$(gen.legacy)_Thermal|$(gen.fuel)" + +get_type(gen::Thermal_Gen) = gen.fuel diff --git a/reeds/resource_adequacy/reeds2pras/src/models/Variable_Gen.jl b/reeds/resource_adequacy/reeds2pras/src/models/Variable_Gen.jl new file mode 100644 index 00000000..f0f43e4f --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/Variable_Gen.jl @@ -0,0 +1,109 @@ +""" + This function takes in the attributes of a variable generator (Variable_Gen) + and returns an object containing all its information. The input + parameters are: + + Parameters + ---------- + name : String + Name of the Variable Generator. + timesteps : Int64 + Number of timesteps for the PRAS model. + region_name : String + Name of the Region where Variable Generator's load area is present. + installed_capacity : Float64 + Installed Capacity of the Variable Generator. + capacity : Vector{Float64} + Capacity factor time series data ('forecasted capacity' / + 'nominal capacity') for the PRAS Model. + type : String + Type of Variable Generator being passed. + legacy : String + State of the Variable Generator, i.e., existing or new. + FOR : Float64 + Forced Outage Rate parameter of the Variable Generator. + SOR : Float64 + Scheduled Outage Rate parameter of the Variable Generator. + MTTR : Int64 + Mean Time To Repair parameter of the Variable Generator. + + Returns + ------- + Variable_Gen : Struct + Returns a struct with all the given attributes. +""" +struct Variable_Gen <: Generator + name::String + timesteps::Int64 + region_name::String + installed_capacity::Float64 + capacity::Vector{Float64} + type::String + legacy::String + FOR::Vector{Float32} + SOR::Vector{Float32} + MTTR::Int64 + + # Inner Constructors & Checks + function Variable_Gen(; + name = "init_name", + timesteps = 8760, + region_name = "init_name", + installed_capacity = 10.0, + capacity = zeros(Float64, timesteps), + type = "wind-ons_init_name", + legacy = "New", + FOR = zeros(Float32, timesteps), + SOR = zeros(Float32, timesteps), + MTTR = 24, + ) + all(0.0 .<= capacity .<= installed_capacity) || if ~(startswith(type, "hyd")) + # We do not need to ensure that capacity is < installed capacity + # for hydroelectric plants because we sometimes have + # capacity factors > 1 + error("$(name) time series has values < 0 or > installed capacity + ($(installed_capacity))") + end + + length(capacity) == timesteps || + error("The length of the $(name) capacity time series data is $(length(capacity)) + but it should be should be equal to PRAS timesteps ($(timesteps))") + + legacy in ["Existing", "New"] || + error("$(name) has legacy $(legacy) which is not in [Existing, New]") + + all(0.0 .<= FOR .<= 1.0) || error("$(name) FOR value is < 0 or > 1") + + if !isnothing(SOR) + length(SOR) == timesteps || + error("The length of the $(name) SOR time series data is $(length(SOR)) + but it should be should be equal to PRAS timesteps ($(timesteps))") + end + + MTTR > 0 || error("$(name) MTTR value is <= 0") + + return new( + name, + timesteps, + region_name, + installed_capacity, + capacity, + type, + legacy, + FOR, + SOR, + MTTR, + ) + end +end + +# Getter Functions + +get_capacity(gen::Variable_Gen) = + isnothing(gen.SOR) ? + round.(Int, gen.capacity)' : + round.(Int, gen.capacity .* (1 .-gen.SOR))' + +get_category(gen::Variable_Gen) = "$(gen.legacy)|$(gen.type)" + +get_type(gen::Variable_Gen) = gen.type diff --git a/reeds/resource_adequacy/reeds2pras/src/models/utils.jl b/reeds/resource_adequacy/reeds2pras/src/models/utils.jl new file mode 100644 index 00000000..cbb20a69 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/models/utils.jl @@ -0,0 +1,371 @@ +# Converting FOR and MTTR to λ and μ +""" + This function calculates the outage rate of a generator based on the + forced outage rate and its Mean Time To Repair (MTTR). + + Parameters + ---------- + for_gen: + forced outage rate (amount of time unit is out-of-service) + mttr: + Mean time to repair (in hours) + + Returns + ------- + (λ, μ): Tuple + λ is the probability of a unit being down, μ is probability + of recovery +""" +function outage_to_rate(for_gen, mttr) + μ = 1 / mttr + λ = (μ .* for_gen) ./ (1 .- for_gen) + return (λ = λ, μ = μ) +end + +emptyvec(::Vector{<:Storage}) = Storage[] + +get_components(comps::Vector{<:Storage}, region_name::String) = + get_storages_in_region(comps, region_name) + +emptyvec(::Vector{<:Generator}) = Generator[] + +get_components(comps::Vector{<:Generator}, region_name::String) = + get_generators_in_region(comps, region_name) +# Functions for processing ReEDS2PRAS generators and storages to prepare +# them for PRAS System generation +""" + Gets components in each region of the system and reorganizes them into + single sorted component vector and corresponding component index vector. + + Parameters + ---------- + comps : COMPONENTS + Vector containing components for each region + region_names : Vector{String} + Vector with names of regions in the system + + Returns + ------- + sorted_comps : COMPONENTS + Sorted vector of all components from every region + region_comp_idxs : UnitRange{Int64}, 1 + Index vector pointing to components belonging to each specified region +""" +function get_sorted_components( + comps::COMPONENTS, + region_names::Vector{String}, +) where {COMPONENTS <: Union{Vector{<:Generator}, Vector{<:Storage}}} + num_regions = length(region_names) + all_comps = [] + start_idx = Array{Int64}(undef, num_regions) + region_comp_idxs = Array{UnitRange{Int64}, 1}(undef, num_regions) + + for (idx, region_name) in enumerate(region_names) + region_comps = get_components(comps, region_name) + push!(all_comps, region_comps) + if idx == 1 + start_idx[idx] = 1 + else + prev_idx = start_idx[idx - 1] + prev_length = length(all_comps[idx - 1]) + start_idx[idx] = prev_idx + prev_length + end + region_comp_idxs[idx] = range(start_idx[idx], length = length(all_comps[idx])) + end + + sorted_comps = emptyvec(comps) + for idx in eachindex(all_comps) + if (length(all_comps[idx]) != 0) + append!(sorted_comps, all_comps[idx]) + end + end + return sorted_comps, region_comp_idxs +end + +get_sorted_components( + comps::COMPONENTS, + regions::Vector{Region}, +) where {COMPONENTS <: Union{Vector{<:Generator}, Vector{<:Storage}}} = + get_sorted_components(comps, get_name.(regions)) + +# Functions for processing ReEDS2PRAS lines (preparing PRAS lines) + +""" + Returns a list of tuples sorted by region name. + + Parameters + ---------- + lines : Vector{Line} + A list of lines containing the regions from and to. + + Returns + ------- + List[Tuple[str]] + A list of tuples sorted by region name. +""" +function get_sorted_region_tuples(lines::Vector{Line}, region_names::Vector{String}) + region_idxs = Dict(name => idx for (idx, name) in enumerate(region_names)) + + line_from_to_reg_idxs = similar(lines, Tuple{Int, Int}) + + for (l, line) in enumerate(lines) + from_name = get_region_from(line) + to_name = get_region_to(line) + + from_idx = region_idxs[from_name] + to_idx = region_idxs[to_name] + + line_from_to_reg_idxs[l] = + from_idx < to_idx ? (from_idx, to_idx) : (to_idx, from_idx) + end + + return line_from_to_reg_idxs +end + +function get_sorted_region_tuples(lines::Vector{Line}, regions::Vector{Region}) + get_sorted_region_tuples(lines, get_name.(regions)) +end + +function get_sorted_region_tuples(lines::Vector{Line}) + regions_from = get_region_from.(lines) + regions_to = get_region_to.(lines) + + region_names = unique(append!(regions_from, regions_to)) + + get_sorted_region_tuples(lines, region_names) +end + +""" + Returns a list of lines sorted + (acc. to sorted regions tuples and interface_region_idxs and interface_line_idxs for PRAS) + + Parameters + ---------- + lines : Vector{Line} + A Vector of ReEDS2PRAS Line objects + region_names : Vector{String} + Vector with names of regions in the system + + Returns + ------- + Vector{Line}, UnitRange{Int64,1}, UnitRange{Int64,1} + A list of sorted lines, Index vector pointing to interface region_from and to belonging to each specified region, + Index vector pointing to Lines belonging to an interface +""" + +function get_sorted_lines(lines::Vector{Line}, region_names::Vector{String}) + line_from_to_reg_idxs = get_sorted_region_tuples(lines, region_names) + line_ordering = sortperm(line_from_to_reg_idxs) + + sorted_lines = lines[line_ordering] + sorted_from_to_reg_idxs = line_from_to_reg_idxs[line_ordering] + interface_reg_idxs = unique(sorted_from_to_reg_idxs) + + # Ref tells Julia to use interfaces as Vector, only broadcasting over + # lines_sorted + interface_line_idxs = searchsorted.(Ref(sorted_from_to_reg_idxs), interface_reg_idxs) + + return sorted_lines, interface_reg_idxs, interface_line_idxs +end + +get_sorted_lines(lines::Vector{Line}, regions::Vector{Region}) = + get_sorted_lines(lines, get_name.(regions)) + +function check_if_line_exists(reg_from::String, reg_to::String, lines::Vector{Line}) + isnothing(findfirst(x -> (x.region_from == reg_from && x.region_to == reg_to),lines)) ? false : true +end + +""" + This code takes in a vector of Lines and a vector of Regions as input + parameters. It filters the Lines to find VSC (voltage source converter) + lines and non-VSC lines. Then, for each VSC line, it creates two new Line + objects representing direct current (DC) converter capacityfor the regional connections + to the VSC line. Finally, a vector of all lines and a vector of all + regions are returned as output. + + Parameters + ---------- + lines : Vector[Line] + Vector of Line objects that contain information about all the lines in + the system + regions : Vector[Region] + Vector of Region objects that contain information about all regions in + the system + + Returns + ------- + non_vsc_dc_lines : Vector[Line] + Vector of Line objects with non_vsc_dc_lines + regions : Vector[Region] + Vector of Region objects with added DC lines +""" +function process_vsc_lines(lines::Vector{Line}, regions::Vector{Region}) + timesteps = first(regions).timesteps + non_vsc_dc_lines = filter(line -> ~line.VSC, lines) + vsc_dc_lines = filter(line -> line.VSC, lines) + + for vsc_line in vsc_dc_lines + dc_region_from = "DC|$(vsc_line.region_from)" + dc_region_to = "DC|$(vsc_line.region_to)" + + for reg_name in [dc_region_from, dc_region_to] + if ~(reg_name in get_name.(regions)) + push!(regions, Region(reg_name, timesteps, zeros(Float64, timesteps))) + end + end + + push!( + non_vsc_dc_lines, + Line( + name = "$(vsc_line.name)|DC", + timesteps = vsc_line.timesteps, + category = vsc_line.category, + region_from = dc_region_from, + region_to = dc_region_to, + forward_cap = vsc_line.forward_cap, + backward_cap = vsc_line.backward_cap, + legacy = vsc_line.legacy, + FOR = vsc_line.FOR, + MTTR = vsc_line.MTTR, + ), + ) + if !(check_if_line_exists(dc_region_from, vsc_line.region_from, non_vsc_dc_lines)) + push!( + non_vsc_dc_lines, + Line( + name = "$(dc_region_from)_VSC", + timesteps = vsc_line.timesteps, + category = vsc_line.category, + region_from = dc_region_from, + region_to = vsc_line.region_from, + forward_cap = vsc_line.converter_capacity[vsc_line.region_from], + backward_cap = vsc_line.converter_capacity[vsc_line.region_from], + legacy = vsc_line.legacy, + FOR = vsc_line.FOR, + MTTR = vsc_line.MTTR, + ), + ) + end + if !(check_if_line_exists(dc_region_to, vsc_line.region_to, non_vsc_dc_lines)) + push!( + non_vsc_dc_lines, + Line( + name = "$(dc_region_to)_VSC", + timesteps = vsc_line.timesteps, + category = vsc_line.category, + region_from = dc_region_to, + region_to = vsc_line.region_to, + forward_cap = vsc_line.converter_capacity[vsc_line.region_to], + backward_cap = vsc_line.converter_capacity[vsc_line.region_to], + legacy = vsc_line.legacy, + FOR = vsc_line.FOR, + MTTR = vsc_line.MTTR, + ), + ) + end + end + return non_vsc_dc_lines, regions +end + +""" + This function creates interfaces between lines that are contained in + different regions. The get_name function then assigns the associated region + name to each interface. + + Parameters + ---------- + sorted_lines: Vector{Line} + A vector containing sorted line information + interface_reg_idxs: Vector{Tuple{Int64, Int64}} + A vector containing the index of the region for each interface + interface_line_idxs: Vector{UnitRange{Int64}} + A vector containing indices of the lines involved in the interface + regions: Vector{Region} + A vector containing the region information +""" +function make_pras_interfaces( + sorted_lines::Vector{Line}, + interface_reg_idxs::Vector{Tuple{Int64, Int64}}, + interface_line_idxs::Vector{UnitRange{Int64}}, + regions::Vector{Region}, +) + make_pras_interfaces( + sorted_lines, + interface_reg_idxs, + interface_line_idxs, + get_name.(regions), + ) +end + +""" + This function creates a Lines and Interfaces object from the given input + arguments. + + Parameters + ---------- + sorted_lines : Vector{Line} + A vector of sorted Line objects + interface_reg_idxs : Vector{Tuple{Int64, Int64}} + A vector of tuples, each tuple containing an interface region index + pair + interface_line_idxs : Vector{UnitRange{Int64}} + A vector of unit ranges indexing into the sorted_lines + region_names : Vector{String} + A vector of strings each denoting a region name + + Returns + ------- + new_lines : PRAS.Lines{timesteps, 1, PRAS.Hour, PRAS.MW} + A new Lines object created from the sorted_lines vector + new_interfaces : PRAS.Interfaces{, PRAS.MW} + A new interfaces object created from interface_reg_idxs and + interface_line_idxs +""" +function make_pras_interfaces( + sorted_lines::Vector{Line}, + interface_reg_idxs::Vector{Tuple{Int64, Int64}}, + interface_line_idxs::Vector{UnitRange{Int64}}, + region_names::Vector{String}, +) + num_interfaces = length(interface_reg_idxs) + interface_regions_from = first.(interface_reg_idxs) + interface_regions_to = last.(interface_reg_idxs) + + timesteps = first(sorted_lines).timesteps + + # Lines + line_names = get_name.(sorted_lines) + line_cats = get_category.(sorted_lines) + line_forward_cap = reduce(vcat, get_forward_capacity.(sorted_lines)) + line_backward_cap = reduce(vcat, get_backward_capacity.(sorted_lines)) + line_λ = reduce(vcat, get_λ.(sorted_lines)) + line_μ = reduce(vcat, get_μ.(sorted_lines)) + + new_lines = PRAS.Lines{timesteps, 1, PRAS.Hour, PRAS.MW}( + line_names, + line_cats, + line_forward_cap, + line_backward_cap, + line_λ, + line_μ, + ) + + interface_forward_capacity_array = Matrix{Int64}(undef, num_interfaces, timesteps) + interface_backward_capacity_array = Matrix{Int64}(undef, num_interfaces, timesteps) + + for i in 1:num_interfaces + fwd_sum = sum(line_forward_cap[interface_line_idxs[i], :], dims = 1) + interface_forward_capacity_array[i, :] = fwd_sum + back_sum = sum(line_backward_cap[interface_line_idxs[i], :], dims = 1) + interface_backward_capacity_array[i, :] = back_sum + end + + new_interfaces = PRAS.Interfaces{timesteps, PRAS.MW}( + interface_regions_from, + interface_regions_to, + interface_forward_capacity_array, + interface_backward_capacity_array, + ) + + return new_lines, new_interfaces +end diff --git a/reeds/resource_adequacy/reeds2pras/src/reeds_to_pras.jl b/reeds/resource_adequacy/reeds2pras/src/reeds_to_pras.jl new file mode 100644 index 00000000..897eacfe --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/reeds_to_pras.jl @@ -0,0 +1,73 @@ +#runs ReEDS2PRAS +""" + Generates a PRAS system from data in ReEDSfilepath + + Parameters + ---------- + ReEDSfilepath : String + Location of ReEDS filepath where inputs, results, and outputs are + stored + year : Int64 + ReEDS solve year + timesteps : Int + Number of timesteps + weather_year : Int + The weather year for variable gen profiles and load + scheduled_outage : Bool + Flag for reading the scheduled_outage_hourly.h5 file for a monthly + scheduled outage rate values, similar to ReEDS. If false, a zero value + is assumed. Default is false. + hydro_energylim : Bool + If this is false we process hydro with fixed capacity based one + name plate from the max_cap file. If true, we process non-dispatchable + hydro as a VRE with varying capacity and dispatchable hydro as + a generator storage with monthly inflows + + Returns + ------- + PRAS.SystemModel + PRAS SystemModel struct with regions, interfaces, generators, + region_gen_idxs, storages, region_stor_idxs, generatorstorages, + region_genstor_idxs, lines, interface_line_idxs, timestamps + +""" +function reeds_to_pras( + reedscase::String, + solve_year::Int64, + timesteps::Int, + weather_year::Int, + scheduled_outage::Bool = false, + hydro_energylim::Bool = false, + pras_agg_ogs_lfillgas::Bool = false, + pras_existing_unit_size::Bool = true, + pras_max_unitsize_prm::Bool = true, +) + ReEDS_data = ReEDSdatapaths(reedscase, solve_year) + + @info "Running checks on input data..." + run_checks(ReEDS_data) + + @info "Parsing ReEDS data and creating ReEDS2PRAS objects..." + out = parse_reeds_data( + ReEDS_data, + timesteps, + solve_year, + hydro_energylim = hydro_energylim, + scheduled_outage = scheduled_outage, + pras_agg_ogs_lfillgas = pras_agg_ogs_lfillgas, + pras_existing_unit_size = pras_existing_unit_size, + pras_max_unitsize_prm = pras_max_unitsize_prm, + ) + lines, regions, gens, storages, genstors = out + + @info "ReEDS data successfully parsed, creating a PRAS system" + return create_pras_system( + regions, + lines, + gens, + storages, + genstors, + timesteps, + weather_year, + ) +end diff --git a/reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl new file mode 100644 index 00000000..ec1acc2d --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl @@ -0,0 +1,1186 @@ +""" + Processes ReEDS data and loads the specified weather year and number of + time steps. + + Parameters + ---------- + ReEDS_data : ReEDSData + ReEDSData object containing the load data. + weather_year : Int + The weather year to be loaded. + timesteps : Int + The number of time steps to be loaded. + + Returns + ------- + List + A list of Region objects containing the load data for the specified + weather year and number of time steps. +""" +function process_regions_and_load(ReEDS_data) + load_data = get_load_file(ReEDS_data) + regions = names(load_data) + + return [ + Region(r, size(load_data, 1), Int.(round.(load_data[!, r]))) + for r in regions + ] +end + +""" + This function takes in ReEDS data, a vector of regions, a year, and a + number of time steps, and returns an array of Line objects. It first gets + the line capacity data from the ReEDS data, then gets the converter + capacity data from the ReEDS data. It then adds 0 converter capacity for + regions that lack a converter. It then creates a system line naming + dataframe, which is a subset of the line capacity dataframe, and combines + the MW column by summing it. It then creates a Line object for each row in + the system line naming dataframe, and adds it to the lines_array. If the + line is a VSC line, it adds the converter capacity data to the Line object. + + Parameters + ---------- + ReEDS_data : DataFrame + DataFrame containing ReEDS line capacity data. + regions : Vector{<:AbstractString} + Vector of region names. + timesteps : Int + Number of timesteps. + + Returns + ------- + lines_array : Vector{Line} + Vector of Line objects. +""" +function process_lines( + ReEDS_data, + regions::Vector{<:AbstractString}, + timesteps::Int, +) + #it is assumed this has prm line capacity data + line_base_cap_data = get_line_capacity_data(ReEDS_data) + + converter_capacity_data = get_converter_capacity_data(ReEDS_data) + converter_capacity_dict = Dict( + convert.(String, converter_capacity_data[!, "r"]) .=> + converter_capacity_data[!, "MW"], + ) + + #add 0 converter capacity for regions that lack a converter + if length(keys(converter_capacity_dict)) > 0 + for reg in regions + if !(reg in keys(converter_capacity_dict)) + @info("$reg does not have VSC converter capacity, so adding" * " a 0") + converter_capacity_dict[reg] = 0.0 + end + end + end + + function keep_line(from_pca, to_pca) + from_idx = findfirst(x -> x == from_pca, regions) + to_idx = findfirst(x -> x == to_pca, regions) + return !isnothing(from_idx) && !isnothing(to_idx) && (from_idx < to_idx) + end + system_line_naming_data = + DataFrames.subset(line_base_cap_data, [:r, :rr] => DataFrames.ByRow(keep_line)) + # split-apply-combine b/c some lines have same name convention + system_line_naming_data = DataFrames.combine( + DataFrames.groupby(system_line_naming_data, ["r", "rr", "trtype"]), + :MW => sum, + ) + + lines_array = Line[] + for row in eachrow(system_line_naming_data) + forward_cap = sum( + line_base_cap_data[ + (line_base_cap_data.r .== row.r) .& (line_base_cap_data.rr .== row.rr) .& (line_base_cap_data.trtype .== row.trtype), + "MW", + ], + ) + backward_cap = sum( + line_base_cap_data[ + (line_base_cap_data.r .== row.rr) .& (line_base_cap_data.rr .== row.r) .& (line_base_cap_data.trtype .== row.trtype), + "MW", + ], + ) + + name = "$(row.r)|$(row.rr)|$(row.trtype)" + @debug( + "a line $name, with $forward_cap MW forward and $backward_cap" * + " backward in $(row.trtype)" + ) + if row.trtype != "VSC" + push!( + lines_array, + Line( + name = name, + timesteps = timesteps, + category = row.trtype, + region_from = row.r, + region_to = row.rr, + forward_cap = forward_cap, + backward_cap = backward_cap, + legacy = "Existing", + # We do not model outages for lines so just use filler values + FOR = 0.0, + MTTR = 24, + ), + ) + else + push!( + lines_array, + Line( + name = name, + timesteps = timesteps, + category = row.trtype, + region_from = row.r, + region_to = row.rr, + forward_cap = forward_cap, + backward_cap = backward_cap, + legacy = "Existing", + # We do not model outages for lines so just use filler values + FOR = 0.0, + MTTR = 24, + VSC = true, + converter_capacity = Dict( + row.r => converter_capacity_dict[string(row.r)], + row.rr => converter_capacity_dict[string(row.rr)], + ), + ), + ) + end + end + return lines_array +end + +""" + Split generator types into thermal, storage, and variable generation + resources + + Parameters + ---------- + ReEDS_data : dict + Raw ReEDS data as a dict + year : Int + Year of interest + + Returns + ------- + DataFrames + (thermal, storage, dispatchable hydro, nondispatchable hydro) capacity +""" +function split_generator_types(ReEDS_data::ReEDSdatapaths) + ## Read {case}/inputs_case/tech-subset-table.csv + tech_subset_table = get_technology_types(ReEDS_data) + @debug "tech_subset_table is $(tech_subset_table)" + ## Read {case}/ReEDS_Augur/augur_data/max_cap_{year}.csv + capacity_data = get_ICAP_data(ReEDS_data) + ## Read {case}/inputs_case/resources.csv + resources = get_valid_resources(ReEDS_data) + @debug "resources is $(resources)" + vg_types = unique(resources.i) + push!(vg_types, "vre") + @debug "vg_types is $(vg_types)" + + hyd_disp_types = + lowercase.(DataFrames.dropmissing(tech_subset_table, :HYDRO_D)[:, "Column1"]) + hyd_non_disp_types = + lowercase.(DataFrames.dropmissing(tech_subset_table, :HYDRO_ND)[:, "Column1"]) + + @debug "hd_types is $(union(hyd_disp_types,hyd_non_disp_types))" + + storage_types = + unique(DataFrames.dropmissing(tech_subset_table, :STORAGE_STANDALONE)[:, "Column1"]) + + @debug "storage type is $(storage_types)" + + # clean vg/storage capacity on a regex, though there might be a better way... + clean_names!(vg_types) + @debug "vg_types is $(vg_types)" + clean_names!(storage_types) + + storage_capacity = filter(x -> x.i in storage_types, capacity_data) + + hyd_disp_capacity = filter(x -> x.i in hyd_disp_types, capacity_data) + hyd_non_disp_capacity = filter(x -> x.i in hyd_non_disp_types, capacity_data) + + thermal_capacity = filter( + x -> ~(x.i in union(vg_types, storage_types, hyd_disp_types, hyd_non_disp_types)), + capacity_data, + ) + + @debug "thermal_capacity is $(thermal_capacity)" + return thermal_capacity, + storage_capacity, + hyd_disp_capacity, + hyd_non_disp_capacity +end + +""" + Process existing thermal capacities with disaggregation. + + Parameters + ---------- + ReEDS_data : object + The ReEDS data object + thermal_builds : DataFrames.DataFrame + Data frame containing the thermal build information + FOR_dict : dict + Dictionary of Forced Outage Rates (FORs) for each resource + timesteps : int + Number of slices to disaggregate the capacity + year : int + Year associated with the capacity + scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} + a dataframe of hourly scheduled outage rates + (if the scheduled_outage_rate file is read). If no file is read, a default + nothing is used. + Returns + ------- + all_generators : Generator[] + Array of Generator objects containing the disaggregated capacity for + each resource +""" +function process_thermals_with_disaggregation( + ReEDS_data, + thermal_builds::DataFrames.DataFrame, + FOR_dict::Dict, + forcedoutage_hourly::DataFrames.DataFrame, + unitsize_dict::Dict, + timesteps::Int, + year::Int, + mttr_dict::Dict; + scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame}, + pras_agg_ogs_lfillgas = false, + pras_existing_unit_size = true, + pras_max_unitsize_prm = true, +) + all_generators = Generator[] + # csp-ns is not a thermal; just drop in for now + thermal_builds = thermal_builds[(thermal_builds.i .!= "csp-ns"), :] + # split-apply-combine to handle differently vintaged entries + thermal_builds = + DataFrames.combine(DataFrames.groupby(thermal_builds, ["i", "r"]), :MW => sum) + unitdata = get_unitdata(ReEDS_data) + + # Get the PRM [MW] in case we use it to set the max unit size + if pras_max_unitsize_prm + max_unitsize = get_max_unitsize(ReEDS_data) + end + + # Get the FOR for each build/tech + for row in eachrow(thermal_builds) + tech = row.i + i_r = "$tech|$(row.r)" + + if (i_r in DataFrames.names(forcedoutage_hourly)) + gen_for = forcedoutage_hourly[!, i_r] + elseif lowercase(tech) in keys(FOR_dict) + gen_for = fill(Float32(FOR_dict[lowercase(tech)]), timesteps) + @info( + "$tech ($(row.r)) was not found in forcedoutage_hourly so using " * + "static value of $(FOR_dict[tech]) from outage_forced_static.csv" + ) + else + @error( + "$(tech) ($(row.r)) was not found in forcedoutage_hourly or outage_forced_static.csv" + ) + end + + gen_sor = zeros(Float32, timesteps) + if !isnothing(scheduled_outage_hourly) && tech in DataFrames.names(scheduled_outage_hourly) + gen_sor = scheduled_outage_hourly[!, tech] + end + + mttr = Int64(mttr_dict[tech]) + + generator_array = disagg_existing_capacity( + unitdata, + unitsize_dict, + Int(round(row.MW_sum)), + String(row.i), + String(row.r), + gen_for, + timesteps, + year, + mttr, + gen_sor, + pras_agg_ogs_lfillgas = pras_agg_ogs_lfillgas, + pras_existing_unit_size = pras_existing_unit_size, + # Use PRM MW if pras_max_unitsize_prm switch is on; otherwise ignore by setting to 0 + max_unit_mw = (pras_max_unitsize_prm ? max_unitsize[row.r] : 0), + ) + append!(all_generators, generator_array) + end + return all_generators +end + +""" + We use this function if we want to process hydroelectric generators as fixed capacities. + It is a replication of the process thermal functions to retain the older way of + handling hydro plants, where we do not use hydro capacity factors, and distinguish + dispatchable and non dispatchable hydroelectric generators. + + Parameters + ---------- + all_generators : Generator[] + Array of Generator objects containing the capacity for + each resource, and it is modified in place. + ReEDS_data : object + The ReEDS data object + hd_builds : DataFrames.DataFrame + Data frame containing the hydro plant information + FOR_dict : dict + Dictionary of Forced Outage Rates (FORs) for each resource + timesteps : int + Number of slices to disaggregate the capacity + year : int + Year associated with the capacity + scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} + a dataframe of hourly scheduled outage rates + (if the scheduled_outage_rate file is read). If no file is read, a default + nothing is used. + Returns + ------- +""" +function process_hd_as_generator!( + all_generators, + ReEDS_data, + hd_builds::DataFrames.DataFrame, + FOR_dict::Dict, + forcedoutage_hourly::DataFrames.DataFrame, + unitsize_dict::Dict, + timesteps::Int, + year::Int, + mttr_dict::Dict, + scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame}, +) + + # split-apply-combine to handle differently vintaged entries + hd_builds = DataFrames.combine(DataFrames.groupby(hd_builds, ["i", "r"]), :MW => sum) + unitdata = get_unitdata(ReEDS_data) + + # Get the FOR for each build/tech + for row in eachrow(hd_builds) + tech = row.i + i_r = "$tech|$(row.r)" + + if (i_r in DataFrames.names(forcedoutage_hourly)) + gen_for = forcedoutage_hourly[!, i_r] + elseif lowercase(tech) in keys(FOR_dict) + gen_for = fill(Float32(FOR_dict[lowercase(tech)]), timesteps) + @info( + "$tech ($(row.r)) was not found in forcedoutage_hourly so using " * + "static value of $(FOR_dict[tech]) from outage_forced_static.csv" + ) + else + @error( + "$(tech) ($(row.r)) was not found in forcedoutage_hourly or outage_forced_static.csv" + ) + end + + mttr = Int64(mttr_dict[tech]) + + gen_sor = zeros(Float32, timesteps) + if !isnothing(scheduled_outage_hourly) && tech in DataFrames.names(scheduled_outage_hourly) + gen_sor = scheduled_outage_hourly[!, tech] + end + + generator_array = disagg_existing_capacity( + unitdata, + unitsize_dict, + Int(round(row.MW_sum)), + String(row.i), + String(row.r), + gen_for, + timesteps, + year, + mttr, + gen_sor, + ) + append!(all_generators, generator_array) + end +end + +""" + Add generators for each tech/region in pras_vre_gen_{year}.h5. + Capacity is taken as the maximum of the hourly generation profile. + VRE outages are already included in VRE profiles, so we do not disaggregate + VRE or apply outages in PRAS. + + Parameters + ---------- + generators_array : Vector{<:ReEDS2PRAS.Generator} + Vector of ReEDS Generators + ReEDS_data : DataFrames.DataFrame + A dataset from the ReEDS Program + scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} + a dataframe of hourly scheduled outage rates + (if the scheduled_outage_rate file is read). If no file is read, a default + nothing is used. + Returns + ------- + generators_array : Vector{<:ReEDS2PRAS.Generator} + An array of the VG generators +""" +function process_vg( + generators_array::Vector{<:ReEDS2PRAS.Generator}, + ReEDS_data, +) + vg_profiles = get_vg_cf_data(ReEDS_data) + timesteps = size(vg_profiles, 1) + + for name in names(vg_profiles) + tech, region = split(name, "|") + profile = vg_profiles[!, name] + push!( + generators_array, + Variable_Gen( + name = name, + timesteps = timesteps, + region_name = region, + installed_capacity = maximum(profile), + capacity = profile, + type = tech, + legacy = "New", + FOR = zeros(Float32, timesteps), + MTTR = 24, + ), + ) + end + return generators_array +end + +""" + Parameters + ---------- + generators_array : Vector{<:Generator} + a vector of strings of distinct regions in the model + hydro_disp_capacities : DataFrame + a dataframe containing details of dispatchable (reservoir) + HD generators + hydro_non_disp_capacities : DataFrame + a dataframe containing details of non-dispatchable + (run-of-river) HD generators + FOR_dict : Dict + a dictionary of forced outage rates, not applied to HD + devices for now + ReEDS_data + input data from ReEDS + timesteps : Int + number of timesteps + year : Int64 + ReEDS target simulation year + scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} + a dataframe of hourly scheduled outage rates + (if the scheduled_outage_rate file is read). If no file is read, a default + nothing is used. + hydro_energylim : Bool + a flag which allows users to choose whether to model HD + devices as flat generators, or as variable generator for + run-of-river plants and generatorstorage for reservoir + plants + unitsize_dict = nothing, + Returns + ------- + generators_array : Vector{<:Generator} + Generators array updated with hydro generators, either all + generators as static capacity, or run-of-river generators + as variable generators + gen_stors : Gen_Storage + Generator_storages array returned as empty if we don't + process energy limits, or returned with reservoir hydro + plants (dispatchable type) +""" +function process_hydro( + generators_array::Vector{<:Generator}, + hydro_disp_capacities::DataFrames.DataFrame, + hydro_non_disp_capacities::DataFrames.DataFrame, + FOR_dict::Dict, + forcedoutage_hourly::DataFrames.DataFrame, + ReEDS_data, + year::Int64, + timesteps::Int, + mttr_dict::Dict, + unitsize_dict; + scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame}, + hydro_energylim = false, +) + + # If we do not impose energy limits on hydro and model it as fixed capacity + if !(hydro_energylim) + @info "Processing HD generators as generator with fixed capacities..." + + process_hd_as_generator!( + generators_array, + ReEDS_data, + hydro_disp_capacities, + FOR_dict, + forcedoutage_hourly, + unitsize_dict, + timesteps, + year, + mttr_dict, + scheduled_outage_hourly, + ) + + process_hd_as_generator!( + generators_array, + ReEDS_data, + hydro_non_disp_capacities, + FOR_dict, + forcedoutage_hourly, + unitsize_dict, + timesteps, + year, + mttr_dict, + scheduled_outage_hourly + ) + genstor_array = Gen_Storage[] + + return generators_array, genstor_array + end + + hydcf, hydcapadj = get_hydro_data(ReEDS_data) + monthhours = monhours() + + timesteps_year = 8760 + num_years = Int(timesteps // timesteps_year) + + # Combine all plant vintages for each region and plant type for dispatchable plants + disp_combined_caps = + DataFrames.combine(DataFrames.groupby(hydro_disp_capacities, [:i, :r]), :MW => sum) + + genstor_array = Gen_Storage[] + + # We need the dispatch limit for each hour from each asset and each month's energy budget + # applied as an exogenous limit using inflow. We have monthly capacity factors for energy budget + # and monthly capacity adjustment factor on the nameplate capacity for dispatch limits. + # For each month, (i) energy budget is calculated based on number of hours in the month + # and (ii) dispatch limit is calculated based on the month. + for (idx, row) in enumerate(DataFrames.eachrow(disp_combined_caps)) + reg_plant_subset = + filter(x -> (x.r == row.r && x.i == row.i), hydcf)[:, [:month, :value]] + monthly_energy = zeros(timesteps_year) + dispatch_limit = zeros(timesteps_year) + energy_cap = zeros(timesteps_year) + for monhr in monthhours + try + reqd_slice = monhr.slice + monthly_energy[first(reqd_slice)] = + (monhr.numhrs * filter(x -> (x.month == monhr.month), reg_plant_subset)[ + :, + :value, + ] * row.MW_sum)[1] + + energy_cap[reqd_slice] .= + (monhr.numhrs * filter(x -> (x.month == monhr.month), reg_plant_subset)[ + :, + :value, + ] * row.MW_sum)[1] + + capacity_adjust = hydcapadj[ + (hydcapadj.i .== row.i) .&& (hydcapadj.r .== row.r), + [:month, :value], + ] + dispatch_limit[reqd_slice] .= + (filter(x -> (x.month == monhr.month), capacity_adjust)[ + :, + :value, + ] * row.MW_sum)[1] + catch e + if isa(e, BoundsError) + @error "$(row.r),$(row.i),$(e)" + else + error() + end + end + end + + category = string(row.i) + name = "$(category)_$(string(row.r))" + region = string(row.r) + + gen_sor = zeros(Float32, timesteps) + if !isnothing(scheduled_outage_hourly) && category in DataFrames.names(scheduled_outage_hourly) + gen_sor = scheduled_outage_hourly[!, category] + end + + mttr = Int64(mttr_dict[category]) + + # - Charging to genstore is limited by charge_capacity whether from grid or from + # inflows. So, that charge_capacity should be equal to the genflow timeseries. + # - Powerflow into grid is limited by grid_injection, which can come from discharge + # and/or exogenous inflow. So discharge capacity can be the dispatch limit or + # arbitrarily high, while the grid_injection cap has to the dispatch limit. + # - Energy capacity can be arbitrarily high in the absense of reservoir limit to + # ensure month to month energy energy carryover. + push!( + genstor_array, + Gen_Storage( + name = name, + timesteps = timesteps, + region_name = region, + charge_cap = repeat(monthly_energy, num_years), + discharge_cap = repeat(dispatch_limit, num_years), + energy_cap = repeat(energy_cap, num_years), + inflow = repeat(monthly_energy, num_years), + grid_withdrawl_cap = zeros(timesteps), + grid_inj_cap = repeat(dispatch_limit, num_years), + type = category, + legacy = "New", + FOR = zeros(Float32, timesteps), + SOR = gen_sor, + MTTR = mttr, + ), + ) + end + + # Non-dispatchable hydro power plants + non_disp_combined_caps = DataFrames.combine( + DataFrames.groupby(hydro_non_disp_capacities, [:i, :r]), + :MW => sum, + ) + + # For non dispatchable hydro plants, the monthly capacity factor is used to determine + # hourly capacity time series in each month. + for (idx, row) in enumerate(DataFrames.eachrow(non_disp_combined_caps)) + reg_type = hydcf[ + findall(x -> (x.r == row.r && x.i == row.i), eachrow(hydcf)), + [:month, :value], + ] + hourly_capacity = zeros(timesteps_year) + for monhr in monthhours + try + reqd_slice = monhr.slice + hourly_capacity[reqd_slice] .= + (filter(x -> (x.month == monhr.month), reg_type)[ + :, + :value, + ] * row.MW_sum)[1] + catch e + @error "$(row.r),$(row.i),$(e)" + end + end + + category = string(row.i) + name = "$(category)_$(string(row.r))" + region = string(row.r) + gen_sor = zeros(Float32, timesteps) + if !isnothing(scheduled_outage_hourly) && category in DataFrames.names(scheduled_outage_hourly) + gen_sor = scheduled_outage_hourly[!, category] + end + mttr = Int64(mttr_dict[category]) + + push!( + generators_array, + Variable_Gen( + name = name, + timesteps = timesteps, + region_name = region, + installed_capacity = row.MW_sum, + capacity = repeat(hourly_capacity, num_years), + type = category, + legacy = "New", + FOR = zeros(Float32, timesteps), + SOR = gen_sor, + MTTR = mttr, + ), + ) + end + + return generators_array, genstor_array +end + +""" + Process data associated with the regional storage build for modeled time + period + + Parameters + ---------- + storage_builds : DataFrames.DataFrame + Data construct containing regional storage build information + FOR_dict : Dict + dictionary of Forced Outage Rates (FOR) associated with storage types + ReEDS_data + input data from ReEDS + timesteps : Int + Number of timesteps + year : Int64 + simulated time period + scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} + a dataframe of hourly scheduled outage rates + (if the scheduled_outage_rate file is read). If no file is read, a default + nothing is used. + Returns + ------- + storages_array : Storage[] + array of modeled storages +""" +function process_storages( + storage_builds::DataFrames.DataFrame, + FOR_dict::Dict, + forcedoutage_hourly::DataFrames.DataFrame, + unitsize_dict::Dict, + ReEDS_data, + timesteps::Int, + mttr_dict::Dict; + scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame} +) + storage_energy_capacity_data = get_storage_energy_capacity_data(ReEDS_data) + @debug "storage_energy_capacity_data is $(storage_energy_capacity_data)" + # split-apply-combine to handle differently vintaged entries + energy_capacity_df = DataFrames.combine( + DataFrames.groupby(storage_energy_capacity_data, ["i", "r"]), + :MWh => sum, + ) + + efficiency_in = Dict( + polarity => DataFrames.DataFrame(CSV.File(joinpath( + ReEDS_data.ReEDSfilepath, + "ReEDS_Augur", + "augur_data", + "$(polarity)_eff_$(ReEDS_data.year).csv" + ))) + for polarity in ["charge", "discharge"] + ) + efficiency = Dict( + polarity => Dict(zip(efficiency_in[polarity][!,"i"], efficiency_in[polarity][!,"fraction"])) + for polarity in keys(efficiency_in) + ) + + ## Read {case}/inputs_case/tech-subset-table.csv + tech_subset_table = get_technology_types(ReEDS_data) + battery_types = DataFrames.dropmissing(tech_subset_table, :BATTERY)[:, "Column1"] + + storages_array = Storage[] + for (idx, row) in enumerate(eachrow(storage_builds)) + gen_sor = zeros(Float32, timesteps) + storage_type = string(row.i) + if !isnothing(scheduled_outage_hourly) && storage_type in DataFrames.names(scheduled_outage_hourly) + gen_sor = scheduled_outage_hourly[!, storage_type] + end + + name = "$(string(row.i))|$(string(row.r))" + + gen_for = nothing + if (name in DataFrames.names(forcedoutage_hourly)) + gen_for = forcedoutage_hourly[!, name] + else + gen_for = fill(Float32(FOR_dict[lowercase(storage_type)]), timesteps) + @info( + "$name was not found in forcedoutage_hourly so using " * + "static value of $(FOR_dict[storage_type]) from outage_forced_static.csv") + end + + name = "$(name)|"#append for later matching + mttr = Int64(mttr_dict[string(row.i)]) + + storage_duration = energy_capacity_df[idx, "MWh_sum"] / row.MW + if string(row.i) in battery_types + push!( + storages_array, + Battery( + name = name, + timesteps = timesteps, + region_name = string(row.r), + type = string(row.i), + charge_cap = row.MW, + discharge_cap = row.MW, + ## Battery FOR is applied to energy capacity, not power capacity + energy_cap = row.MW * storage_duration * (1 .- gen_for), + legacy = "New", + charge_eff = efficiency["charge"][string(row.i)], + discharge_eff = efficiency["discharge"][string(row.i)], + carryover_eff = 1.0, + FOR = zeros(Float32, timesteps), + SOR = gen_sor, + MTTR = mttr, + ), + ) + else + add_new_capacity!( + storages_array, + round(Int, row.MW), + storage_duration, + efficiency["charge"][string(row.i)], + efficiency["discharge"][string(row.i)], + # Always use the characteristic unit size + unitsize_dict[row.i], + row.i, + row.r, + gen_for, + timesteps, + mttr, + gen_sor, + ) + end + end + return storages_array +end + +""" + Disaggregates the existing capacity of a thermal generator given a certain + year, by taking into account its technology and associated balancing + authority. + + Parameters + ---------- + unitdata : DataFrames.DataFrame + DataFrame containing Expected Information Administration (EIA) data. + unitsize_dict: Dict + Map from techs to characteristic unit size [MW] + built_capacity : int + The current built capacity for the thermal generator. + tech : str + The technology for the thermal generator. + pca : str + The associated balancing authority (PCA). + gen_for : float + The forced outage rate associated with the generator. + gen_sor : float + The scheduled outage rate associated with the generator. + timesteps : Int + Number of timesteps. + year : int + The year. + mttr : int + mean time to repaire (mttr) + pras_agg_ogs_lfillgas : bool + If true, aggregate existing o-g-s and landfill gas using size for new units. + Applies to existing o-g-s and landfill gas capaity, so does not interact with + pras_existing_unit_size, which only affects new capacity. + pras_existing_unit_size : bool + If true, use average existing unit size by (tech,region) when disaggregating new + capacity. Applies to new capacity so does not interact with pras_agg_ogs_lfillgas. + max_unit_mw: int + If nonzero, caps the upper bound of disaggregated unit size + + Returns + ------- + generators_array : array + An array composed of thermal generator objects created with the + disaggregated existing capacities. +""" +function disagg_existing_capacity( + unitdata::DataFrames.DataFrame, + unitsize_dict::Dict, + built_capacity::Int, + tech::String, + pca::String, + gen_for::Vector{Float32}, + timesteps::Int, + year::Int, + mttr::Int, + gen_sor::Union{Nothing, Vector{Float32}} = nothing; + pras_agg_ogs_lfillgas = false, + pras_existing_unit_size = true, + max_unit_mw = 0, +) + if pras_agg_ogs_lfillgas == true + group_existing_techs = ["lfill-gas", "o-g-s"] + else + group_existing_techs = [] + end + + tech_ba_year_existing = DataFrames.subset( + unitdata, + :tech => DataFrames.ByRow(==(tech)), + :reeds_ba => DataFrames.ByRow(==(pca)), + :RetireYear => DataFrames.ByRow(>(year)), + :StartYear => DataFrames.ByRow(<=(year)), + ) + + generators_array = [] + # If there is no existing capacity, use the characteristic unit size + if size(tech_ba_year_existing, 1) == 0 && gen_for != 0.0 + add_new_capacity!( + generators_array, + built_capacity, + # If max_unit_mw is provided and is smaller than the characteristic unit size, + # use max_unit_mw; otherwise use the characteristic unit size from unitsize_dict + ((max_unit_mw > 0) ? min(max_unit_mw, unitsize_dict[tech]) : unitsize_dict[tech]), + tech, + pca, + gen_for, + timesteps, + mttr, + gen_sor, + ) + return generators_array + # If the FOR is zero, no need to disaggregate, so put all capacity in one unit + elseif size(tech_ba_year_existing, 1) == 0 && gen_for == 0.0 + return [ + Thermal_Gen( + name = "$(tech)|$(pca)|1", + timesteps = timesteps, + region_name = pca, + capacity = built_capacity, + fuel = tech, + legacy = "New", + FOR = gen_for, + SOR = gen_sor, + MTTR = mttr, + ), + ] + end + + remaining_capacity = built_capacity + if tech in group_existing_techs + existing_capacity_total = sum(tech_ba_year_existing[!, "summer_power_capacity_MW"]) + num_whole = Int(existing_capacity_total ÷ unitsize_dict[tech]) + remainder = existing_capacity_total % unitsize_dict[tech] + existing_capacity = vcat(ones(num_whole) * unitsize_dict[tech], remainder) + else + existing_capacity = tech_ba_year_existing[!, "summer_power_capacity_MW"] + end + + if pras_existing_unit_size == true + # If the average integer unit size rounds to 0 MW, use 1 MW + unit_capacity = max(Int(round(Statistics.mean(existing_capacity))), 1) + # If max_unit_mw is provided and is smaller than the mean, use max_unit_mw instead + if max_unit_mw > 0 + unit_capacity = min(unit_capacity, max_unit_mw) + end + else + unit_capacity = 0 + end + + @info "$tech $pca: unit capacity = $unit_capacity MW" + + for (idx, built_cap) in enumerate(existing_capacity) + int_built_cap = Int(round(built_cap)) + if int_built_cap < remaining_capacity + gen_cap = int_built_cap + remaining_capacity -= int_built_cap + else + gen_cap = remaining_capacity + remaining_capacity = 0 + end + gen = Thermal_Gen( + name = "$(tech)|$(pca)|$(idx)", + timesteps = timesteps, + region_name = pca, + capacity = gen_cap, + fuel = tech, + legacy = "Existing", + FOR = gen_for, + SOR = gen_sor, + MTTR = mttr, + ) + push!(generators_array, gen) + end + + #whatever remains, we want to build as new capacity + if remaining_capacity > 0 + add_new_capacity!( + generators_array, + remaining_capacity, + (if (unit_capacity > 0) unit_capacity else unitsize_dict[tech] end), + tech, + pca, + gen_for, + timesteps, + mttr, + gen_sor, + ) + end + + return generators_array +end + +""" + This function adds new capacity to an existing list of generators. The + unit_capacity parameter is used to determine how many generators + must be constructed to create the total new_capacity. If there are no + existing units, a single generator is built with all of the new_capacity. + For fixed unit_capacity values, the remaining capacity is divided + evenly among the new generators, adding additional capacity to each one, + then a small remainder may be created and added as a separate generator to + get the desired total new_capacity. The output of this function is the + updated generators_array containing all of the newly added generators. + + Parameters + ---------- + generators_array : Vector{<:Any} + a vector or list of existing generators + new_capacity : int + specified new capacity to be added + unit_capacity : int + target capacity for the units to be added + tech : string + type of technology used for the generator unit + pca : string + power control authority of the generator unit + gen_for : float + generation forecast + gen_sor : float + The scheduled outage rate associated with the generator. + timesteps : Int + number of timesteps + MTTR : int + mean time to repair for the generator unit + + Returns + ------- + generators_array: Vector{<:Any} + updated vector or list of generators containing the new capacity +""" +function add_new_capacity!( + generators_array::Vector{<:Any}, + new_capacity::Int, + unit_capacity::Int, + tech::AbstractString, + pca::AbstractString, + gen_for::Vector{Float32}, + timesteps::Int, + MTTR::Int, + gen_sor::Union{Nothing, Vector{Float32}} = nothing, +) + n_gens = floor(Int, new_capacity / unit_capacity) + if n_gens == 0 + return push!( + generators_array, + Thermal_Gen( + name = "$(tech)|$(pca)|new|1", + timesteps = timesteps, + region_name = pca, + capacity = new_capacity, + fuel = tech, + legacy = "New", + FOR = gen_for, + SOR = gen_sor, + MTTR = MTTR, + ), + ) + end + + for i in range(1, n_gens) + push!( + generators_array, + Thermal_Gen( + name = "$(tech)|$(pca)|new|$(i)", + timesteps = timesteps, + region_name = pca, + capacity = unit_capacity, + fuel = tech, + legacy = "New", + FOR = gen_for, + SOR = gen_sor, + MTTR = MTTR, + ), + ) + end + + remainder = new_capacity - (n_gens * unit_capacity) + if remainder > 0 + # integer remainder is made into a tiny gen + push!( + generators_array, + Thermal_Gen( + name = "$(tech)|$(pca)|new|$(n_gens+1)", + timesteps = timesteps, + region_name = pca, + capacity = remainder, + fuel = tech, + legacy = "New", + FOR = gen_for, + SOR = gen_sor, + MTTR = MTTR, + ), + ) + end + + return generators_array +end + +""" + This function is the same as above but takes an additional arg + new_duration, which will be picked up by multiple dispatch + and leading to Battery{} model handling +""" +function add_new_capacity!( + generators_array::Vector{<:Any}, + new_capacity::Int, + new_duration::Float64, + charge_eff::Float64, + discharge_eff::Float64, + unit_capacity::Int, + tech::AbstractString, + pca::AbstractString, + gen_for::Vector{Float32}, + timesteps::Int, + MTTR::Int, + gen_sor::Union{Nothing, Vector{Float32}} = nothing, +) + n_gens = floor(Int, new_capacity / unit_capacity) + if n_gens == 0 + return push!( + generators_array, + Battery( + name = "$(tech)|$(pca)|new|1", + timesteps = timesteps, + region_name = pca, + type = tech, + charge_cap = new_capacity, + discharge_cap = new_capacity, + energy_cap = fill(Float64(new_capacity * new_duration), timesteps), + legacy = "New", + charge_eff = charge_eff, + discharge_eff = discharge_eff, + carryover_eff = 1.0, + FOR = gen_for, + SOR = gen_sor, + MTTR = MTTR, + ), + ) + end + + for i in range(1, n_gens) + push!( + generators_array, + Battery( + name = "$(tech)|$(pca)|new|$(i)", + timesteps = timesteps, + region_name = pca, + type = tech, + charge_cap = unit_capacity, + discharge_cap = unit_capacity, + energy_cap = fill(Float64(unit_capacity * new_duration), timesteps), + legacy = "New", + charge_eff = charge_eff, + discharge_eff = discharge_eff, + carryover_eff = 1.0, + FOR = gen_for, + SOR = gen_sor, + MTTR = MTTR, + ), + ) + end + + remainder = new_capacity - (n_gens * unit_capacity) + if remainder > 0 + # integer remainder is made into a tiny gen + push!( + generators_array, + Battery( + name = "$(tech)|$(pca)|new|$(n_gens+1)", + timesteps = timesteps, + region_name = pca, + type = tech, + charge_cap = remainder, + discharge_cap = remainder, + energy_cap = fill(Float64(remainder * new_duration), timesteps), + legacy = "New", + charge_eff = charge_eff, + discharge_eff = discharge_eff, + carryover_eff = 1.0, + FOR = gen_for, + SOR = gen_sor, + MTTR = MTTR, + ), + ) + end + + return generators_array +end diff --git a/reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl new file mode 100644 index 00000000..985936bc --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl @@ -0,0 +1,497 @@ +""" + Creates a datapath for a given ReEDS model year. Used as a parameter for other + functions in order to access correctly dated input files. + + Parameters + ---------- + x : String + Path to ReEDS case (e.g. "~/github/ReEDS-2.0/runs/name_of_reeds_case") + y : Int + ReEDS model year + + Returns + ------- + A new object with filepath and valid year parameters +""" +struct ReEDSdatapaths + ReEDSfilepath::String + year::Int + + function ReEDSdatapaths(x, y) + return new(x, y) + end +end + +"Includes functions used for loading ReEDS data" + +""" + Loop through the vector and replace any '*' present in the elements with + the text between the asterisk, excluding the '_'. + + Parameters + ---------- + input_vec : Vector{<:AbstractString} + Vector of strings that need to be parsed + + Returns + ------- + input_vec : Vector{<:AbstractString} + Vector of strings which has been cleaned of '*' +""" +function clean_names!(input_vec::Vector{<:AbstractString}) + for (idx, a) in enumerate(input_vec) + if occursin("*", a) + input_vec[idx] = match(r"\*([a-zA-Z]+-*[a-zA-Z]*)_*", a)[1] + end + end + return input_vec +end + +""" + Loads the EIA-NEMS Generator Database from a given ReEDS directory. + + Parameters + ---------- + + Returns + ------- + unitdata : DataFrame + A DataFrame containing the EIA-NEMS Generator Database data. +""" +function get_unitdata(data::ReEDSdatapaths) + filepath = joinpath(data.ReEDSfilepath, "inputs_case", "unitdata.csv") + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + Loads an .h5 file from the given file path, containing Electrical Demand + data from the indicated year. + + Parameters + ---------- + data : ReEDSdatapaths + An instance of ReEDSdatapaths with the necessary arguments set. + + Returns + ------- + HDF5.h5read(filepath, "data") + A readout of the Augur load h5 file associated with the given ReEDS + filepath and year. + +""" +function get_load_file(data::ReEDSdatapaths) + filepath = joinpath( + data.ReEDSfilepath, + "ReEDS_Augur", + "augur_data", + "pras_load_$(string(data.year)).h5", + ) + columns = HDF5.h5read(filepath, "columns") + data = HDF5.h5read(filepath, "data") + df = DataFrames.DataFrame(transpose(data), columns) + return df +end + +""" + This function reads a hdf5 file from the ReEDS Augur directory, based on + the year provided in the ReEDSdatapaths struct. + + Parameters + ---------- + data : ReEDSdatapaths + Struct containing the `ReEDSfilepath` and a year, indicating which .h5 + file should be read. + + Returns + ------- + The requested ``hdf5`` as a data frame. +""" +function get_vg_cf_data(data::ReEDSdatapaths) + filepath = joinpath( + data.ReEDSfilepath, + "ReEDS_Augur", + "augur_data", + "pras_vre_gen_$(string(data.year)).h5", + ) + columns = HDF5.h5read(filepath, "columns") + data = HDF5.h5read(filepath, "data") + df = DataFrames.DataFrame(transpose(data), columns) + return df +end + +""" + Get tech-dependent MTTR. + + Parameters + ---------- + data : ReEDSdatapaths + Struct containing relevant datapaths and year from which to extract + the data. + + Returns + ------- + Dictionary + tech => MTTR [hours] +""" +function get_MTTR_data(data::ReEDSdatapaths) + filepath = joinpath(data.ReEDSfilepath, "inputs_case", "mttr.csv") + df = DataFrames.DataFrame(CSV.File(filepath)) + return Dict(df[!, "tech"] .=> df[!, "hours"]) +end + + +""" + Get region-dependent max unit size [MW] as dictionary +""" +function get_max_unitsize(data::ReEDSdatapaths) + filepath = joinpath( + data.ReEDSfilepath, "ReEDS_Augur", "augur_data", + "max_unitsize_$(string(data.year)).csv" + ) + df = DataFrames.DataFrame(CSV.File(filepath)) + return Dict(df[!, "r"] .=> df[!, "mw"]) +end + + +""" + Get the forced outage data from the augur files. + + Parameters + ---------- + data : ReEDSdatapaths + Struct containing relevant datapaths and year from which to extract + the data. + + Returns + ------- + DataFrames.DataFrame + Dataframe containing the forced outage data. +""" +function get_forced_outage_data(data::ReEDSdatapaths) + filepath = joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_static.csv") + df = DataFrames.DataFrame(CSV.File(filepath, header = false)) + return DataFrames.rename!(df, ["ResourceType", "FOR"]) +end + +""" + Get the valid resources from {case}/inputs_case/resources.csv + + Parameters + ---------- + data : ReEDSdatapaths + Struct containing ReEDS filepaths and year + + Returns + ------- + DataFrames.DataFrame + A DataFrame containing the valid resources of the ReEDS case +""" +function get_valid_resources(data::ReEDSdatapaths) + filepath = joinpath(data.ReEDSfilepath, "inputs_case", "resources.csv") + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + This function gets the technology types from {case}/inputs_case/tech-subset-table.csv + + Arguments + --------- + data : ReEDSdatapaths + A struct containing paths and dates related to ReEDS analyses. + + Returns + ------- + DataFrames.DataFrame + The technology type table in the form of a DataFrames object. +""" +function get_technology_types(data::ReEDSdatapaths) + filepath = joinpath(data.ReEDSfilepath, "inputs_case", "tech-subset-table.csv") + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + Gets line capacity data for the given ReEDS database. + + Parameters + ---------- + data : ReEDSdatapaths + Contains the filepath of data and year of analysis + + Returns + ------- + DataFrame + A dataframe with transmission capacity data; assumes this file has been + formatted by ReEDS +""" +function get_line_capacity_data(data::ReEDSdatapaths) + #assumes this file has been formatted by ReEDS to be PRM line capacity data + filepath = joinpath( + data.ReEDSfilepath, + "ReEDS_Augur", + "augur_data", + "tran_cap_$(string(data.year)).csv", + ) + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + Get the converter capacity data associated with the given ReEDSdatapaths + object. + + Parameters + ---------- + data : ReEDSdatapaths) + A ReEDSdatapaths object containing the relevant file paths and year. + + Returns + ------- + DataFrames.DataFrame + The DataFrame of the converter capacity data for the given year. +""" +function get_converter_capacity_data(data::ReEDSdatapaths) + filepath = joinpath( + data.ReEDSfilepath, + "ReEDS_Augur", + "augur_data", + "cap_converter_$(string(data.year)).csv", + ) + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + Returns a DataFrame containing the Annual Technology Baseline + default unit size for the ReEDSdatapaths object. + + Parameters + ---------- + data : ReEDSdatapaths + An object containing the filepaths to the ReEDS input files. + + Returns + ------- + DataFrame + A DataFrame containing the default unit size mapping. + + Raises + ------ + Error + If no table of unit size mapping is found. + +""" +function get_unitsize_mapping(data::ReEDSdatapaths) + filepath = joinpath(data.ReEDSfilepath, "inputs_case", "unitsize.csv") + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + Returns a DataFrame containing the installed capacity of generators for a + given year, read from {case}/ReEDS_Augur/augur_data/max_cap_{year}.csv. + + Parameters + ---------- + data : ReEDSdatapaths + A ReEDSdatapaths object containing the year and filepath. + + Returns + ------- + DataFrame + A DataFrame containing the installed capacity data. + + Raises + ------ + Error + If the year does not have generator installed capacity data. +""" +function get_ICAP_data(data::ReEDSdatapaths) + filepath = joinpath( + data.ReEDSfilepath, + "ReEDS_Augur", + "augur_data", + "max_cap_$(string(data.year)).csv", + ) + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + Returns DataFrames containing hydroelectric plants capacity factors + + Parameters + ---------- + data : ReEDSdatapaths + A ReEDSdatapaths object containing the year and filepath. + + Returns + ------- + hydcf: DataFrame + A DataFrame containing the seasonal capacity factors for both + dispatchable and non-dispatchable hydroelectric plants, subsetted + to the required output year. + + hydcapadj: DataFrame + A DataFrame containing seasonal capacity adjustment factors for + dispatchable hydroelectric plants which limits the maximum hourly + dispatch (MW) in each season. + + Raises + ------ + Error + If the filepath for the for the two files do not exist. +""" +function get_hydro_data(data::ReEDSdatapaths) + filepath_cf = joinpath(data.ReEDSfilepath, "inputs_case", "hydcf.csv") + hydcf = DataFrames.DataFrame(CSV.File(filepath_cf)) + + # Rename plant types as techtypes and capacity are lowercase + DataFrames.rename!(hydcf, [:"*i"] .=> [:i]) + hydcf.i = lowercase.(hydcf.i) + # Subset to ReEDS model year + hydcf = filter(x -> x.t == data.year, hydcf) + + filepath_capadj = joinpath(data.ReEDSfilepath, "inputs_case", "hydcapadj.csv") + + hydcapadj = DataFrames.DataFrame(CSV.File(filepath_capadj)) + + # Rename plant types as techtypes and capacity are lowercase + DataFrames.rename!(hydcapadj, [:"*i"] .=> [:i]) + hydcapadj.i = lowercase.(hydcapadj.i) + + return hydcf, hydcapadj +end + +""" + Returns a DataFrame containing the installed storage energy capacity data + for the year specified in the ReEDSdatapaths object. + + Parameters + ---------- + data : ReEDSdatapaths + An object containing paths to ReEDS data. + + Returns + ------- + DataFrame + DataFrame containing storage energy capacity data + + Raises + ------ + Error + If the filepath for the specified year does not exist. +""" +function get_storage_energy_capacity_data(data::ReEDSdatapaths) + filepath = joinpath( + data.ReEDSfilepath, + "ReEDS_Augur", + "augur_data", + "energy_cap_$(string(data.year)).csv", + ) + return DataFrames.DataFrame(CSV.File(filepath)) +end + +""" + Returns a DataFrame containing the hourly planned outage data, + read from reeds2pras/test/reeds_cases/Pacific/inputs_case + + Parameters + ---------- + data : ReEDSdatapaths + An object containing paths to ReEDS data. + + Returns + ------- + DataFrame + DataFrame the hourly scheduled outages + + Raises + ------ + Error + If the filepath for the specified year does not exist. +""" +function get_hourly_scheduled_outage_data(data::ReEDSdatapaths) + filepath = joinpath( + data.ReEDSfilepath, + "inputs_case", + "outage_scheduled_hourly.h5", + ) + + return DataFrames.DataFrame( + HDF5.h5read(filepath, "data")', + HDF5.h5read(filepath, "columns"), + ) +end + +""" + Get the hourly forced outage data from the augur files. + + Parameters + ---------- + data : ReEDSdatapaths + Struct containing relevant datapaths and year from which to extract + the data. + + Returns + ------- + DataFrames.DataFrame + Dataframe containing the hourly forced outage data. +""" +function get_hourly_forced_outage_data(data::ReEDSdatapaths) + filepath = joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_hourly.h5") + forcedoutage_hourly = DataFrames.DataFrame( + HDF5.h5read(filepath, "data")', + HDF5.h5read(filepath, "columns"), + ) + return forcedoutage_hourly +end + +# Struct to define monthhour values +mutable struct monthhour + month::String + numhrs::Int64 + cumsum::Int64 + slice::UnitRange{Int64} + + # Inner Constructors & Checks + function monthhour( + month = "month", + numhrs = 10, + cumsum = 0, + slice = range(1, length = 10), + ) + return new(month, numhrs, cumsum, slice) + end +end + +# Functions to augment collection of monthour +function cumsum!(collection::Vector{monthhour}) + sum = 0 + for element in collection + sum = sum + element.numhrs + element.cumsum = sum + end +end + +function addslices!(collection::Vector{monthhour}) + for element in collection + element.slice = (element.cumsum - element.numhrs + 1):(element.cumsum) + end +end + +# Generating necessary data +function monhours() + monthours = monthhour[] + start_date = Dates.Date("2021-01", "yyyy-mm") + for i in range(0, length = 12) + new_date = start_date + Dates.Month(i) + push!( + monthours, + monthhour( + uppercase(Dates.monthabbr(i + 1)), + Dates.daysinmonth(new_date) * 24, + ), + ) + end + + cumsum!(monthours) + addslices!(monthours) + + return monthours +end diff --git a/reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl b/reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl new file mode 100644 index 00000000..9a5a88a6 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl @@ -0,0 +1,47 @@ +# Check if you can open a file +function check_file(loc::String) + io = try + open(loc) + catch + nothing + end + + if (isnothing(io)) + return nothing, false + else + return io, isopen(io) + end +end + +function run_checks(data::ReEDSdatapaths) + augur_data_path = joinpath(data.ReEDSfilepath, "ReEDS_Augur", "augur_data") + filepaths = [ + joinpath(augur_data_path, "cap_converter_$(string(data.year)).csv"), + joinpath(augur_data_path, "charge_eff_$(string(data.year)).csv"), + joinpath(augur_data_path, "discharge_eff_$(string(data.year)).csv"), + joinpath(augur_data_path, "energy_cap_$(string(data.year)).csv"), + joinpath(augur_data_path, "max_cap_$(string(data.year)).csv"), + joinpath(augur_data_path, "max_unitsize_$(string(data.year)).csv"), + joinpath(augur_data_path, "pras_load_$(string(data.year)).h5"), + joinpath(augur_data_path, "pras_vre_gen_$(string(data.year)).h5"), + joinpath(augur_data_path, "tran_cap_$(string(data.year)).csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "hydcapadj.csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "hydcf.csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "mttr.csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_hourly.h5"), + joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_static.csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "outage_scheduled_hourly.h5"), + joinpath(data.ReEDSfilepath, "inputs_case", "resources.csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "tech-subset-table.csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "unitdata.csv"), + joinpath(data.ReEDSfilepath, "inputs_case", "unitsize.csv"), + ] + for filepath in filepaths + io, file_exists = check_file(filepath) + if (file_exists) + close(io) + else + error("Missing required file for ReEDS2PRAS: $filepath") + end + end +end diff --git a/reeds/resource_adequacy/reeds2pras/test/Project.toml b/reeds/resource_adequacy/reeds2pras/test/Project.toml new file mode 100644 index 00000000..e62453f6 --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/test/Project.toml @@ -0,0 +1,8 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +PRAS = "05348d26-1c52-11e9-35e3-9d51842d34b9" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + + diff --git a/reeds/resource_adequacy/reeds2pras/test/runtests.jl b/reeds/resource_adequacy/reeds2pras/test/runtests.jl new file mode 100644 index 00000000..6a6f505a --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/test/runtests.jl @@ -0,0 +1,38 @@ +using ReEDS2PRAS +using BenchmarkTools +using Aqua +using DataFrames +using PRAS +using Test + +const R2P = ReEDS2PRAS +include("utils.jl") + +@testset verbose = true "Aqua.jl" begin + Aqua.test_unbound_args(ReEDS2PRAS) + Aqua.test_undefined_exports(ReEDS2PRAS) + Aqua.test_ambiguities(ReEDS2PRAS) + Aqua.test_stale_deps(ReEDS2PRAS) + Aqua.test_deps_compat(ReEDS2PRAS) +end + +#= +Don't add your tests to runtests.jl. Instead, create files named + + test-title-for-my-test.jl + +The file will be automatically included inside a `@testset` with title "Title For My Test". +=# +@testset verbose = true "ReEDS2PRAS tests" begin + for (root, dirs, files) in walkdir(@__DIR__) + for file in files + if isnothing(match(r"^test.*\.jl$", file)) + continue + end + title = titlecase(replace(splitext(file[6:end])[1], "-" => " ")) + @testset verbose = true "$title" begin + include(file) + end + end + end +end \ No newline at end of file diff --git a/reeds/resource_adequacy/reeds2pras/test/test-ReEDS2PRAS.jl b/reeds/resource_adequacy/reeds2pras/test/test-ReEDS2PRAS.jl new file mode 100644 index 00000000..b346723d --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/test/test-ReEDS2PRAS.jl @@ -0,0 +1,71 @@ +reedscase = joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035") +solve_year = 2035 +timesteps = 8760 +weather_year = 2007 + +# ReEDS2PRAS System Generation with no kwargs +pras_sys_1 = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = false, scheduled_outage = false); +path = R2P.ReEDSdatapaths(reedscase, solve_year) + +@testset verbose = true "SystemModel" begin + @test pras_sys_1 isa PRAS.SystemModel + + @testset "Load" begin + # Load + @test check_region_load_data(pras_sys_1) + end + + @testset "Lines" begin + # VSC Lines + @test check_DC_region_in_pras_system(pras_sys_1, path) + @test check_converter_capacity(pras_sys_1, path) + + # Other Lines + @test check_line_capacities(pras_sys_1, path) + end + @testset "Resource Capacity" begin + # Generators + @test check_generator_capacities(pras_sys_1, path) + @test check_storage_capacities(pras_sys_1, path) + end + + @testset "Transition Probabilities" begin + # Generators + @test check_generator_outage_probabilities(pras_sys_1, path, weather_year, timesteps) + + # Storages + @test check_storage_outage_probabilities(pras_sys_1, path, weather_year, timesteps) + @test check_storage_recovery_probabilities(pras_sys_1) + end + +end + +# ReEDS2PRAS System Generation with only scheduled outages +pras_sys_2 = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = false, scheduled_outage = true); + +@testset verbose = true "SystemModel-ScheduledOutage" begin + @testset "Generator Capacities" begin + @test check_scheduled_outage_generator_capacities(pras_sys_1, pras_sys_2) + end + + @testset "Storage Energy Capacities" begin + @test check_scheduled_outage_storage_capacities(pras_sys_1, pras_sys_2) + end + +end + +# ReEDS2PRAS System Generation with only hydro energy limits +pras_sys_3 = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = true, scheduled_outage = false); + +@testset verbose = true "SystemModel-HydroEnergyLimits" begin + @test length(pras_sys_3.generatorstorages.names) > 0 + @test length(pras_sys_3.generators.names) < length(pras_sys_1.generators.names) # Because we don't disaggregate and we have GeneratorStorages + + @testset "Inflow" begin + @test check_hydro_energy_limits(pras_sys_3, path) + end + @testset "Outage Probability" begin + @test check_generatorstorage_outage_probabilities(pras_sys_3, path, weather_year, timesteps) + end + #TODO : Should we check others (discharge_capacity, etc.?)? +end \ No newline at end of file diff --git a/reeds/resource_adequacy/reeds2pras/test/test-benchmark.jl b/reeds/resource_adequacy/reeds2pras/test/test-benchmark.jl new file mode 100644 index 00000000..8d72d42f --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/test/test-benchmark.jl @@ -0,0 +1,35 @@ +@testset verbose = true "ReEDS2PRAS & PRAS Benchmark" begin + # Running this benchmark: + # Run this file first with the main branch and + # then the feature branch, record the reported mean time taken + # for both ReEDS2PRAS and PRAS, and the CONUS LOLE, nEUE output here + # while submitting pull request with the PR template + + # If making major changes to R2P model, increase number of MC samples used + + # Set up this test + reedscase = joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035") + solve_year = 2035 + timesteps = 8760 + weather_year = 2007 + samples = 10 + seed = 1 + + # ReEDS2PRAS Benchmarking + bm_r2p = @btimed R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = true, scheduled_outage = true) setup = (reedscase=joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035"); solve_year=2035; timesteps = 8760; weather_year = 2007); + + # PRAS Benchmarking + bm_pras = @btimed assess(pras_sys, simulation, Shortfall()) setup = (pras_sys=R2P.reeds_to_pras(joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035"), 2035, 8760, 2007, hydro_energylim = true, scheduled_outage = true); simulation = SequentialMonteCarlo(samples = 10, seed = 1)); + + # Print Results + pras_sys = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = true, scheduled_outage = true); + simulation = SequentialMonteCarlo(samples = samples, seed = seed) + shortfall = assess(pras_sys, simulation, Shortfall()) + + LOLE = PRAS.LOLE(shortfall[1]).lole.estimate + EUE = PRAS.EUE(shortfall[1]).eue.estimate + nEUE = PRAS.NEUE(shortfall[1]).neue.estimate + + @show "ReEDS2PRAS Benchmark Time : time - $(bm_r2p.time), gctime - $(bm_r2p.gctime); PRAS Benchmark Time : time - $(bm_pras.time), gctime - $(bm_pras.gctime), LOLE: $(LOLE), EUE: $(EUE), NEUE :$(nEUE)" + +end \ No newline at end of file diff --git a/reeds/resource_adequacy/reeds2pras/test/utils.jl b/reeds/resource_adequacy/reeds2pras/test/utils.jl new file mode 100644 index 00000000..1732d32e --- /dev/null +++ b/reeds/resource_adequacy/reeds2pras/test/utils.jl @@ -0,0 +1,393 @@ +function check_generator_capacities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) + # Currently doesn't check for weather_year VG profiles + # Just ensures 0 <= VG capacity time series <= Installed Capacity + # In some regions, distpv fails this tests, so skipping that category for now + capacity_data = R2P.get_ICAP_data(path) + vg_resource_types = R2P.get_valid_resources(path) + tech_list = R2P.get_technology_types(path) + vg_types = unique(vg_resource_types.i) + storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) + reg_cap = filter(x -> x.r == reg_name, capacity_data) + vg_counts = length(filter(x -> x ∈ vg_types, unique(reg_cap.i))) + vg_counts = ("distpv" in unique(reg_cap.i)) ? vg_counts - 1 : vg_counts # because some regions fail the distpv test + non_vg_counts = length(filter(x -> x ∉ union(vg_types,storage_types), unique(reg_cap.i))) + + reg_non_vg_count = 0 + reg_vg_count = 0 + for gen_cat in filter(x -> x ∉ storage_types, unique(reg_cap.i)) + pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.generators.categories[pras_sys.region_gen_idxs[reg_idx]]) + reeds_gen_cat_data = filter(x -> x.i == gen_cat, reg_cap) + if !(gen_cat in vg_types) + if (isapprox(sum(pras_sys.generators.capacity[pras_sys.region_gen_idxs[reg_idx]][pras_gen_cat_idx]), round(Int, sum(reeds_gen_cat_data.MW)), atol=1)) + reg_non_vg_count = reg_non_vg_count + 1 + end + else + if (gen_cat != "distpv") # because some regions fail the distpv test + if (all(0 .<= pras_sys.generators.capacity[pras_sys.region_gen_idxs[reg_idx],:][pras_gen_cat_idx,:] .<= round(Int, sum(reeds_gen_cat_data.MW)))) + reg_vg_count = reg_vg_count + 1 + end + end + end + end + + if ((reg_non_vg_count == non_vg_counts) && (reg_vg_count == vg_counts)) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys.regions.names)) + return true + else + return false + end +end + +function check_storage_capacities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) + capacity_data = R2P.get_ICAP_data(path) + tech_list = R2P.get_technology_types(path) + storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) + + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) + reg_cap = filter(x -> x.r == reg_name, capacity_data) + stor_cats = filter(x -> x ∈ storage_types, unique(reg_cap.i)) + stor_counts = length(stor_cats) + + reg_stor_count = 0 + for stor_cat in stor_cats + pras_stor_cat_idx = findall(x -> x == stor_cat, pras_sys.storages.categories[pras_sys.region_stor_idxs[reg_idx]]) + reeds_stor_cat_data = filter(x -> x.i == stor_cat, reg_cap) + + if (sum(pras_sys.storages.charge_capacity[pras_sys.region_stor_idxs[reg_idx]][pras_stor_cat_idx]) == round(Int, sum(reeds_stor_cat_data.MW))) + reg_stor_count = reg_stor_count + 1 + end + end + + if (reg_stor_count == stor_counts) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys.regions.names)) + return true + else + return false + end +end + +function check_line_capacities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) + # The capacities of interfaces between AC & DC regions are checked in a different function + line_data = R2P.get_line_capacity_data(path) + non_vsc_line_data = filter(x -> x.trtype != "VSC", line_data) + vsc_line_data = filter(x -> x.trtype == "VSC", line_data) + + ac_interfaces_count = length(findall(.~occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_from]) .&& .~occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_to]))) + dc_interfaces_count = length(findall(occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_from]) .&& occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_to]))) + ac_int_count = 0 + dc_int_count = 0 + + for i in 1:length(pras_sys.interfaces) + region_from = pras_sys.regions.names[pras_sys.interfaces.regions_from[i]] + region_to = pras_sys.regions.names[pras_sys.interfaces.regions_to[i]] + cap_forward_pras = pras_sys.interfaces.limit_forward[i] + cap_backward_pras = pras_sys.interfaces.limit_backward[i] + + if !((occursin("DC", region_from) && occursin("DC", region_to)) || (occursin("DC", region_from) || occursin("DC", region_to))) + to_from_lines = filter(x -> x.r == region_from && x.rr == region_to, non_vsc_line_data) + from_to_lines = filter(x -> x.r == region_to && x.rr == region_from, non_vsc_line_data) + + cap_forward_reeds = round(Int, sum(to_from_lines.MW)) + cap_backward_reeds = round(Int, sum(from_to_lines.MW)) + + if ((cap_forward_pras == cap_forward_reeds) && (cap_backward_pras == cap_backward_reeds)) + ac_int_count = ac_int_count + 1 + end + else + if ((occursin("DC", region_from) && occursin("DC", region_to))) + ac_reg_from = last(split(region_from, "|")) + ac_reg_to = last(split(region_to, "|")) + to_from_lines = filter(x -> x.r == ac_reg_from && x.rr == ac_reg_to, vsc_line_data) + from_to_lines = filter(x -> x.r == ac_reg_to && x.rr == ac_reg_from, vsc_line_data) + + cap_forward_reeds = round(Int, sum(to_from_lines.MW)) + cap_backward_reeds = round(Int, sum(from_to_lines.MW)) + + if ((cap_forward_pras == cap_forward_reeds) && (cap_backward_pras == cap_backward_reeds)) + dc_int_count = dc_int_count + 1 + end + end + end + end + + if ((ac_int_count == ac_interfaces_count) && (dc_int_count == dc_interfaces_count)) + return true + else + return false + end +end + +function check_region_load_data(pras_sys::PRAS.SystemModel) + dc_reg_idx = findall(occursin.("DC", pras_sys.regions.names)) + dc_flag = all(iszero.(pras_sys.regions.load[dc_reg_idx,:])) + ac_reg_idx = findall(.!(occursin.("DC", pras_sys.regions.names))) + ac_flag = all(pras_sys.regions.load[ac_reg_idx,:] .> 0) + + return (dc_flag && ac_flag) ? true : false +end + +function check_DC_region_in_pras_system(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) + line_base_cap_data = R2P.get_line_capacity_data(path) + vsc_data = filter(x -> x.trtype == "VSC", line_base_cap_data) + vsc_regions = union(Set(vsc_data.r), Set(vsc_data.rr)) + dc_region_names = "DC|".*vsc_regions + + if all(in.(dc_region_names, Ref(pras_sys.regions.names))) + return true + else + return false + end +end + +function check_converter_capacity(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) + cap_converter_data = R2P.get_converter_capacity_data(path) + vsc_region_names = unique(filter(x -> x.MW > 0.0, cap_converter_data)[!,"r"]) + cap_mws = unique(filter(x -> x.MW > 0.0, cap_converter_data)[!,"MW"]) + dc_region_names = "DC|".* vsc_region_names + count = 0 + for (reg_ac, reg_dc, cap_mw) in zip(vsc_region_names, dc_region_names, cap_mws) + reg_ac_idx = findfirst(pras_sys.regions.names .== reg_ac) + reg_dc_idx = findfirst(pras_sys.regions.names .== reg_dc) + + interface_idx = findfirst((pras_sys.interfaces.regions_from .== reg_ac_idx .&& pras_sys.interfaces.regions_to .== reg_dc_idx) .|| + (pras_sys.interfaces.regions_to .== reg_ac_idx .&& pras_sys.interfaces.regions_from .== reg_dc_idx)) + + if all(pras_sys.interfaces.limit_forward[interface_idx,:] .== round(Int,cap_mw)) + count+=1 + end + end + + if (count == length(vsc_region_names)) + return true + else + return false + end +end + +function check_scheduled_outage_generator_capacities(pras_sys_no_derate::PRAS.SystemModel, pras_sys_with_derate::PRAS.SystemModel) + + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys_no_derate.regions.names) + + reg_gen_count = 0 + reg_gen_cats = unique(pras_sys_no_derate.generators.categories[pras_sys_no_derate.region_gen_idxs[reg_idx]]) + for gen_cat in reg_gen_cats + pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys_no_derate.generators.categories[pras_sys_no_derate.region_gen_idxs[reg_idx]]) + pras_derate_reg_idx = findfirst(pras_sys_with_derate.regions.names .== reg_name) + pras_derate_gen_cat_idx = findall(x -> x == gen_cat, pras_sys_with_derate.generators.categories[pras_sys_with_derate.region_gen_idxs[pras_derate_reg_idx]]) + + if all(pras_sys_with_derate.generators.capacity[pras_sys_with_derate.region_gen_idxs[pras_derate_reg_idx],:][pras_derate_gen_cat_idx,:] .<= + pras_sys_no_derate.generators.capacity[pras_sys_no_derate.region_gen_idxs[reg_idx],:][pras_gen_cat_idx,:]) + reg_gen_count = reg_gen_count + 1 + end + + end + + if (reg_gen_count == length(reg_gen_cats)) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys_no_derate.regions.names)) + return true + else + return false + end +end + +function check_scheduled_outage_storage_capacities(pras_sys_no_derate::PRAS.SystemModel, pras_sys_with_derate::PRAS.SystemModel) + + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys_no_derate.regions.names) + + reg_stor_count = 0 + reg_stor_cats = unique(pras_sys_no_derate.storages.categories[pras_sys_no_derate.region_stor_idxs[reg_idx]]) + for stor_cat in reg_stor_cats + pras_stor_cat_idx = findall(x -> x == stor_cat, pras_sys_no_derate.storages.categories[pras_sys_no_derate.region_stor_idxs[reg_idx]]) + pras_derate_reg_idx = findfirst(pras_sys_with_derate.regions.names .== reg_name) + pras_derate_stor_cat_idx = findall(x -> x == stor_cat, pras_sys_with_derate.storages.categories[pras_sys_with_derate.region_stor_idxs[pras_derate_reg_idx]]) + + if all(pras_sys_with_derate.storages.energy_capacity[pras_sys_with_derate.region_stor_idxs[pras_derate_reg_idx],:][pras_derate_stor_cat_idx,:] .<= + pras_sys_no_derate.storages.energy_capacity[pras_sys_no_derate.region_stor_idxs[reg_idx],:][pras_stor_cat_idx,:]) + reg_stor_count = reg_stor_count + 1 + end + + end + + if (reg_stor_count == length(reg_stor_cats)) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys_no_derate.regions.names)) + return true + else + return false + end +end + +function check_hydro_energy_limits(pras_sys::PRAS.SystemModel,path::R2P.ReEDSdatapaths) + tech_list = R2P.get_technology_types(path) + hyd_disp_types = + lowercase.(DataFrames.dropmissing(tech_list, :HYDRO_D)[:, "Column1"]) + + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) + hyd_disp_cats = filter(x -> x ∈ hyd_disp_types, pras_sys.generatorstorages.categories[pras_sys.region_genstor_idxs[reg_idx]]) + hyd_disp_counts = length(hyd_disp_cats) + + reg_hyd_disp_count = 0 + for gen_cat in hyd_disp_cats + pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.generatorstorages.categories[pras_sys.region_genstor_idxs[reg_idx]]) + + if (length(unique(pras_sys.generatorstorages.inflow[pras_sys.region_genstor_idxs[reg_idx],:][pras_gen_cat_idx,:])) > 1 || + all(iszero.(pras_sys.generatorstorages.inflow[pras_sys.region_genstor_idxs[reg_idx],:][pras_gen_cat_idx,:]))) + # Need to do this becuase some regions don't have inflow data (p80 in particular) + reg_hyd_disp_count = reg_hyd_disp_count + 1 + end + end + + if (reg_hyd_disp_count == length(hyd_disp_cats)) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys.regions.names)) + return true + else + return false + end +end + +function check_generator_outage_probabilities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths, weather_year::Int, timesteps::Int) + capacity_data = R2P.get_ICAP_data(path) + tech_list = R2P.get_technology_types(path) + storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) + hyd_disp_types = lowercase.(DataFrames.dropmissing(tech_list, :HYDRO_D)[:, "Column1"]) + hyd_non_disp_types = lowercase.(DataFrames.dropmissing(tech_list, :HYDRO_ND)[:, "Column1"]) + for_hourly = R2P.get_hourly_forced_outage_data(path) + + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) + reg_cap = filter(x -> x.r == reg_name, capacity_data) + non_stor_cats = filter(x -> x ∉ storage_types, unique(reg_cap.i)) + + upgraded_non_stor_cats = String[] + for cat in non_stor_cats + push!(upgraded_non_stor_cats, cat) + end + cat_idx = findall((upgraded_non_stor_cats .* "|" .* reg_name) .∈ Ref(names(for_hourly))) + hourly_for_cats = non_stor_cats[cat_idx] + upgraded_hourly_for_cats = upgraded_non_stor_cats[cat_idx] + hourly_for_cat_counts = length(hourly_for_cats) + + reg_hourly_for_cat_count = 0 + for (gen_cat, upgraded_gen_cat) in zip(hourly_for_cats, upgraded_hourly_for_cats) + pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.generators.categories[pras_sys.region_gen_idxs[reg_idx]]) + reeds_unique_for_count = length(unique(for_hourly[!,upgraded_gen_cat*"|"* reg_name])) + if (length(unique(sum(pras_sys.generators.λ[pras_sys.region_gen_idxs[reg_idx],:][pras_gen_cat_idx,:], dims = 1))) == reeds_unique_for_count) + reg_hourly_for_cat_count = reg_hourly_for_cat_count + 1 + end + + end + + if (reg_hourly_for_cat_count == hourly_for_cat_counts) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys.regions.names)) + return true + else + return false + end +end + +function check_storage_outage_probabilities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths, weather_year::Int, timesteps::Int) + # This test doesn't account for that fact that FOR for batteries is accounted for in the energy capacity. + # but passes because λ assigned is 0.0 + capacity_data = R2P.get_ICAP_data(path) + tech_list = R2P.get_technology_types(path) + storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) + + for_hourly = R2P.get_hourly_forced_outage_data(path) + + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) + reg_cap = filter(x -> x.r == reg_name, capacity_data) + stor_cats = filter(x -> x ∈ storage_types, unique(reg_cap.i)) + + cat_idx = findall((stor_cats .* "|" .* reg_name) .∈ Ref(names(for_hourly))) + hourly_for_cats = stor_cats[cat_idx] + hourly_for_cat_counts = length(hourly_for_cats) + + reg_hourly_for_cat_count = 0 + for gen_cat in hourly_for_cats + pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.storages.categories[pras_sys.region_stor_idxs[reg_idx]]) + reeds_unique_for_count = length(unique(for_hourly[!,gen_cat*"|"* reg_name])) + if (length(unique(sum(pras_sys.storages.λ[pras_sys.region_stor_idxs[reg_idx],:][pras_gen_cat_idx,:], dims = 1))) == reeds_unique_for_count) + reg_hourly_for_cat_count = reg_hourly_for_cat_count + 1 + end + + end + + if (reg_hourly_for_cat_count == hourly_for_cat_counts) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys.regions.names)) + return true + else + return false + end +end + +function check_storage_recovery_probabilities(pras_sys::PRAS.SystemModel) + # Just check storages MTTR is parsed as a test + + reg_count = 0 + for (reg_idx,reg_name) in enumerate(pras_sys.regions.names) + if (all(pras_sys.storages.μ[pras_sys.region_stor_idxs[reg_idx]] .== (1/24))) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys.regions.names)) + return true + else + return false + end +end + +function check_generatorstorage_outage_probabilities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths, weather_year::Int, timesteps::Int) + for_hourly = R2P.get_hourly_forced_outage_data(path) + + reg_count = 0 + for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) + reg_genstor_idx = pras_sys.region_genstor_idxs[reg_idx] + reg_genstor_cats = pras_sys.generatorstorages.categories[reg_genstor_idx] + reg_genstor_cat_count = 0 + for genstor_cat in unique(reg_genstor_cats) + pras_genstor_cat_idx = findall(x -> x == genstor_cat, pras_sys.generatorstorages.categories[reg_genstor_idx]) + tech = genstor_cat + reeds_unique_for_count = length(unique(for_hourly[!,tech*"|"* reg_name])) + if (length(unique(sum(pras_sys.generatorstorages.λ[reg_genstor_idx,:][pras_genstor_cat_idx,:], dims = 1))) == reeds_unique_for_count) + reg_genstor_cat_count = reg_genstor_cat_count + 1 + end + + end + + if (reg_genstor_cat_count == length(unique(reg_genstor_cats))) + reg_count = reg_count + 1 + end + end + if (reg_count == length(pras_sys.regions.names)) + return true + else + return false + end +end diff --git a/reeds/resource_adequacy/run_pras.jl b/reeds/resource_adequacy/run_pras.jl new file mode 100644 index 00000000..a44e86f6 --- /dev/null +++ b/reeds/resource_adequacy/run_pras.jl @@ -0,0 +1,486 @@ +#%% Imports +import ArgParse +import DataFrames +import Logging +import LoggingExtras +import Dates +import PRAS +import HDF5 + +const DF = DataFrames + +#%% Functions +""" + Parse command line arguments for use with ReEDS2PRAS and PRAS +""" +function parse_commandline() + s = ArgParse.ArgParseSettings() + + @ArgParse.add_arg_table s begin + "--reeds_path" + help = "Path to ReEDS-2.0 folder" + arg_type = String + required = true + "--reedscase" + help = "Path to ReEDS run (usually .../ReEDS-2.0/runs/{casename})" + arg_type = String + required = true + "--solve_year" + help = "ReEDS solve year (usually in [2020..2050])" + arg_type = Int + required = true + "--weather_year" + help = "The weather year to start from, in [2007..2013,2016..2023]" + arg_type = Int + default = 2007 + required = true + "--samples" + help = "Number of Monte Carlo samples to run in PRAS" + arg_type = Int + default = 10 + required = false + "--timesteps" + help = "Number of hourly timesteps to use" + arg_type = Int + default = 61320 + required = false + "--hydro_energylim" + help = "Model hydropower as an energy-limited resource" + arg_type = Int + default = 0 + required = false + "--scheduled_outage" + help = "Include monthly scheduled outage" + arg_type = Int + default = 0 + required = false + "--write_flow" + help = "Write the hourly interface flows" + arg_type = Int + default = 0 + required = false + "--write_surplus" + help = "Write the hourly surplus" + arg_type = Int + default = 0 + required = false + "--write_energy" + help = "Write the hourly storage energy" + arg_type = Int + default = 0 + required = false + "--write_shortfall_samples" + help = "Write the sample-level shortfall" + arg_type = Int + default = 0 + required = false + "--write_availability_samples" + help = "Write the sample-level generator and storage availability" + arg_type = Int + default = 0 + required = false + "--iteration" + help = "Solve-year iteration number (only used in file label)" + arg_type = Int + default = 0 + required = false + "--overwrite" + help = "Overwrite an existing .pras file" + arg_type = Int + default = 1 + required = false + "--include_samples" + help = "Include the number of samples in the output .csv filename" + arg_type = Int + default = 0 + required = false + "--pras_agg_ogs_lfillgas" + help = "Aggregate existing o-g-s and landfill gas using size for new units" + arg_type = Int + default = 0 + required = false + "--pras_existing_unit_size" + help = "Use average existing unit size by (tech,region) when disaggregating new units" + arg_type = Int + default = 1 + required = false + "--pras_max_unitsize_prm" + help = "Cap the upper bound of disaggregated unit size by zone at the zonal PRM in MW" + arg_type = Int + default = 1 + required = false + "--pras_seed" + help = "Random seed for PRAS (positive integer; ignored and set randomly if 0)" + arg_type = Int + default = 1 + required = false + "--debug" + help = "Log debug-level messages" + arg_type = Int + default = 0 + required = false + end + return ArgParse.parse_args(s) +end + +""" + Set up logging to file and console +""" +function setup_logger(pras_system_path::String, args::Dict) + if ~isnothing(pras_system_path) + logfile = replace(pras_system_path, ".pras"=>".log") + + if args["debug"] == 1 + logfilehandle = LoggingExtras.MinLevelLogger( + LoggingExtras.FileLogger(logfile; append=true), + Logging.Debug) + else + logfilehandle = LoggingExtras.MinLevelLogger( + LoggingExtras.FileLogger(logfile; append=true), + Logging.Info) + end + + logger = LoggingExtras.TeeLogger( + Logging.global_logger(), + logfilehandle + ) + + ### https://github.com/JuliaLogging/LoggingExtras.jl#add-timestamp-to-all-logging + timestamp_logger(logger) = LoggingExtras.TransformerLogger(logger) do log + merge( + log, + (; message = "$(Dates.format(Dates.now(), "yyyy-mm-dd HH:MM:SS")) | $(log.message)") + ) + end + + Logging.global_logger(timestamp_logger(logger)) + end +end + +""" + Simple PRAS analysis. + + Parameters + ---------- + + Returns + ------- +""" +function run_pras(pras_system_path::String, args::Dict) + #%% Load the system model + @info "Parsing PRAS System ..." + sys = PRAS.SystemModel(pras_system_path); + + #%% Specify the results to save + resultspec = Dict{String,Any}("short" => PRAS.Shortfall()) + if args["write_flow"] == 1 + resultspec["flow"] = PRAS.Flow() + end + if args["write_surplus"] == 1 + resultspec["surplus"] = PRAS.Surplus() + end + if args["write_energy"] == 1 + resultspec["energy"] = PRAS.StorageEnergy() + end + if args["write_shortfall_samples"] == 1 + resultspec["short_samples"] = PRAS.ShortfallSamples() + end + if args["write_availability_samples"] == 1 + resultspec["avail_gen"] = PRAS.GeneratorAvailability() + resultspec["avail_stor"] = PRAS.StorageAvailability() + resultspec["avail_genstor"] = PRAS.GeneratorStorageAvailability() + resultspec["energy_samples"] = PRAS.StorageEnergySamples() + end + + #%% Run PRAS + if args["pras_seed"] > 0 + method = PRAS.SequentialMonteCarlo( + samples=args["samples"], threaded=true, verbose=true, seed=args["pras_seed"]) + else + method = PRAS.SequentialMonteCarlo( + samples=args["samples"], threaded=true, verbose=true) + end + results_tuple = PRAS.assess(sys, method, values(resultspec)...) + results = Dict{String,Any}(zip(keys(resultspec), results_tuple)) + + #%% Print some results for the entire modeled region to show it worked + @info "$(PRAS.LOLE(results["short"])) event-h" + @info "$(PRAS.EUE(results["short"])) MWh" + @info "NEUE = $(1e6 * PRAS.EUE(results["short"]).eue.estimate / sum(sys.regions.load)) ppm" + + ## Filter out DC regions used for VSC HVDC transmission + regions = [r for r in sys.regions.names if !(occursin("|", r))] + + #%% Print some more detailed results if debugging + for (i, reg) in enumerate(regions) + @debug "$reg: $(round(PRAS.LOLE(results["short"],reg).lole.estimate)) event-h" + @debug "$reg: $(round(PRAS.EUE(results["short"],reg).eue.estimate)) MWh" + @debug "$reg: NEUE = $(round( + 1e6 * PRAS.EUE(results["short"],reg).eue.estimate + / sum(sys.regions.load[i,:]) + )) ppm\n\n" + end + + #%% Record the EUE and LOLE outputs by region and timestep + ## Units are: + ## * LOLE: event-h + ## * EUE: MWh + ## First for the whole modeled area (labeled as "USA" but if modeling a smaller + ## region (speicified by GSw_Region) it will be for that modeled region) + dfout = DF.DataFrame( + USA_LOLE=[PRAS.LOLE(results["short"],h).lole.estimate for h in sys.timestamps], + USA_EUE=[PRAS.EUE(results["short"],h).eue.estimate for h in sys.timestamps], + ) + ## Now for each constituent region + for (i,r) in enumerate(regions) + dfout[!, "$(r)_LOLE"] = [PRAS.LOLE(results["short"],r,h).lole.estimate for h in sys.timestamps] + dfout[!, "$(r)_EUE"] = [PRAS.EUE(results["short"],r,h).eue.estimate for h in sys.timestamps] + end + + #%% Write it + if args["include_samples"] == 1 + outfile = replace(pras_system_path, ".pras"=>"-$(args["samples"]).h5") + else + outfile = replace(pras_system_path, ".pras"=>".h5") + end + HDF5.h5open(outfile, "w") do f + for column in DF.names(dfout) + f[column, compress=4] = convert(Array, dfout[!, column]) + end + end + @info("Wrote PRAS EUE and LOLE to $(outfile)") + + #%%### Record more operational details if desired + + ### Flow + if args["write_flow"] == 1 + dfflow = DF.DataFrame() + for i in results["flow"].interfaces + ## Flow results are tuples of (mean, standard deviation). Keep the mean. + dfflow[!, "$(i)"] = [results["flow"][i,h][1] for h in sys.timestamps] + end + ## Write it + flowfile = replace(outfile, ".h5"=>"-flow.h5") + HDF5.h5open(flowfile, "w") do f + for column in DF._names(dfflow) + f["$column", compress=4] = convert(Array, dfflow[!, column]) + end + end + @info("Wrote PRAS flow to $(flowfile)") + end + + ### Surplus + if args["write_surplus"] == 1 + dfsurplus = DF.DataFrame() + for r in regions + ## Surplus results are tuples of (mean, standard deviation). Keep the mean. + dfsurplus[!, "$(r)"] = [results["surplus"][r,h][1] for h in sys.timestamps] + end + ## Write it + surplusfile = replace(outfile, ".h5"=>"-surplus.h5") + HDF5.h5open(surplusfile, "w") do f + for column in DF._names(dfsurplus) + f["$column", compress=4] = convert(Array, dfsurplus[!, column]) + end + end + @info("Wrote PRAS surplus to $(surplusfile)") + end + ### Storage energy + if args["write_energy"] == 1 + dfenergy = DF.DataFrame() + for i in sys.storages.names + ## Energy results are tuples of (mean, standard deviation). Keep the mean. + dfenergy[!, strip("$(i)", '_')] = [results["energy"][i,h][1] for h in sys.timestamps] + end + ## Write it + energyfile = replace(outfile, ".h5"=>"-energy.h5") + HDF5.h5open(energyfile, "w") do f + for column in DF._names(dfenergy) + f["$column", compress=4] = convert(Array, dfenergy[!, column]) + end + end + @info("Wrote PRAS storage energy to $(energyfile)") + end + + ### Sample-level shortfall + if args["write_shortfall_samples"] == 1 + dictshort = Dict(s => DF.DataFrame() for s = 1:args["samples"]) + for s in range(1, args["samples"]) + dictshort[s] = DF.DataFrame( + transpose(getindex.(results["short_samples"][:, :], s)), + sys.regions.names + ) + # subset to regions (filter out DC regions) + dictshort[s] = dictshort[s][:,findall(regions .∈ Ref(sys.regions.names))] + end + ## Write it + shortfile = replace(outfile, ".h5"=>"-shortfall_samples.h5") + HDF5.h5open(shortfile, "w") do f + ## Create a group for each sample. Within each group, write an array for each region. + for s in range(1, args["samples"]) + HDF5.create_group(f, "$s") + for column in DF._names(dictshort[s]) + f["$s"]["$column", compress=4] = convert(Array, dictshort[s][!, column]) + end + end + end + @info("Wrote PRAS shortfall by sample to $(shortfile)") + end + + ### Sample-level generator and storage availability + if args["write_availability_samples"] == 1 + dictavail = Dict(s => DF.DataFrame() for s = 1:args["samples"]) + for s in range(1, args["samples"]) + dictavail[s] = hcat( + DF.DataFrame( + transpose(getindex.(results["avail_gen"][:, :], s)), + strip.(results["avail_gen"].generators, '_') + ), + DF.DataFrame( + transpose(getindex.(results["avail_stor"][:, :], s)), + strip.(results["avail_stor"].storages, '_') + ), + DF.DataFrame( + transpose(getindex.(results["avail_genstor"][:, :], s)), + strip.(results["avail_genstor"].generatorstorages, '_') + ), + ) + end + ## Write it + availabilityfile = replace(outfile, ".h5"=>"-avail.h5") + HDF5.h5open(availabilityfile, "w") do f + ## Create a group for each sample. Within each group, write an array for each unit. + for s in range(1, args["samples"]) + HDF5.create_group(f, "$s") + for column in DF._names(dictavail[s]) + f["$s"]["$column", compress=4] = convert(Array, dictavail[s][!, column]) + end + end + end + @info("Wrote PRAS unit availability to $(availabilityfile)") + ### Same for storage energy by sample + dictstoravail = Dict(s => DF.DataFrame() for s = 1:args["samples"]) + for s in range(1, args["samples"]) + dictstoravail[s] = DF.DataFrame( + transpose(getindex.(results["energy_samples"][:, :], s)), + strip.(results["energy_samples"].storages, '_') + ) + end + ## Write it + energysamplesfile = replace(outfile, ".h5"=>"-energy_samples.h5") + HDF5.h5open(energysamplesfile, "w") do f + for s in range(1, args["samples"]) + HDF5.create_group(f, "$s") + for column in DF._names(dictstoravail[s]) + f["$s"]["$column", compress=4] = convert(Array, dictstoravail[s][!, column]) + end + end + end + @info("Wrote PRAS storage energy by sample to $(energysamplesfile)") + end + + #%% + return dfout +end + + +#%% Main function +""" + Run ReEDS2PRAS and PRAS +""" +function main(args::Dict) + #%% Define some intermediate filenames + pras_system_path = joinpath( + args["reedscase"],"ReEDS_Augur","PRAS", + "PRAS_$(args["solve_year"])i$(args["iteration"]).pras" + ) + + #%% Set up the logger + setup_logger(pras_system_path, args) + @info "Julia version: $(VERSION)" + @info "Julia executable: $(joinpath(Sys.BINDIR, "julia"))" + @info "Running ReEDS2PRAS with the following inputs:" + for (arg, val) in args + @info "$arg => $val" + end + + #%% Run ReEDS2PRAS + if (args["overwrite"] == 1) | ~isfile(pras_system_path) + ### Create and save the PRAS system + ## Could use compression_level={integer} here but it doesn't really help + PRAS.savemodel( + ReEDS2PRAS.reeds_to_pras( + args["reedscase"], + args["solve_year"], + args["timesteps"], + args["weather_year"], + # Boolean switches: == to convert from integer to boolean + args["scheduled_outage"] == 1, + args["hydro_energylim"] == 1, + args["pras_agg_ogs_lfillgas"] == 1, + args["pras_existing_unit_size"] == 1, + args["pras_max_unitsize_prm"] == 1, + ), + pras_system_path, + verbose=true, + ) + @info "Finished ReEDS2PRAS" + end + + #%% Run PRAS + if args["samples"] > 0 + @info "Running PRAS" + dfout = run_pras(pras_system_path, args) + @info "Finished PRAS" + #%% + return dfout + end +end + + +#%% Procedure +if abspath(PROGRAM_FILE) == @__FILE__ + #%% Inputs for debugging + # julia --project=/path/to/ReEDS-2.0 --threads=1 + # args = Dict( + # "reeds_path" => "/path/to/ReEDS-2.0", + # "reedscase" => ( + # "/path/to/ReEDS-2.0/runs/" + # *"runname"), + # "solve_year" => 2035, + # "weather_year" => 2007, + # "samples" => 10, + # "iteration" => 0, + # "timesteps" => 131400, + # "hydro_energylim" => 1, + # "write_flow" => 0, + # "write_surplus" => 0, + # "write_energy" => 0, + # "write_shortfall_samples" => 1, + # "write_availability_samples" => 0, + # "overwrite" => 1, + # "debug" => 0, + # "include_samples" => 0, + # "scheduled_outage" => 0, + # "pras_agg_ogs_lfillgas" => 0, + # "pras_existing_unit_size" => 1, + # "pras_max_unitsize_prm" => 1, + # "pras_seed" => 1, + # ) + # reedscase = args["reedscase"] + # solve_year = args["solve_year"] + # timesteps = args["timesteps"] + # weather_year = args["weather_year"] + # include(joinpath(args["reeds_path"], "reeds2pras", "src", "ReEDS2PRAS.jl")) + + #%% Parse the command line arguments + args = parse_commandline() + + #%% Include ReEDS2PRAS + include(joinpath(args["reedscase"], "reeds2pras", "src", "ReEDS2PRAS.jl")) + + #%% Run it + main(args) + + #%% +end diff --git a/reeds/resource_adequacy/stress_periods.py b/reeds/resource_adequacy/stress_periods.py new file mode 100644 index 00000000..8774f6fa --- /dev/null +++ b/reeds/resource_adequacy/stress_periods.py @@ -0,0 +1,615 @@ +#%%### General imports +import os +import site +import traceback +import pandas as pd +import numpy as np +from glob import glob +import re +import matplotlib.pyplot as plt +### Local imports + +## use this to import reeds when running locally for debugging +# import site +# this_dir_path = os.path.dirname(os.path.realpath(__file__)) +# site.addsitedir(os.path.join(this_dir_path, "..")) + +import reeds + +# #%% Debugging +# sw['reeds_path'] = os.path.expanduser('~/github/ReEDS-2.0/') +# sw['casedir'] = os.path.join(sw['reeds_path'],'runs','v20230123_prmM3_Pacific_d7sIrh4sh2_y2') +# import importlib +# importlib.reload(functions) + + +#%%### Functions +def plot_eue_diagnostics(sw, t, iteration, high_eue_periods): + try: + dates = ( + pd.concat(high_eue_periods) + .reset_index().actual_period.map(reeds.timeseries.h2timestamp) + .dt.strftime('%Y-%m-%d') + .tolist() + ) + vmax = {'forced': 40, 'scheduled': 25, 'both': 50} + aggfunc = 'max' + for outage_type in vmax: + savename = f'map-outage_{outage_type}_{aggfunc}-{t}i{iteration}.png' + plt.close() + f, ax, _ = reeds.reedsplots.map_outage_days( + sw.casedir, + dates=dates, + outage_type=outage_type, + aggfunc=aggfunc, + vmax=vmax[outage_type], + ) + plt.savefig( + os.path.join(sw.casedir, 'outputs', 'Augur_plots', savename) + ) + plt.close() + except Exception as err: + print(err) + + +def get_and_write_neue(sw, write=True): + """ + Write dropped load across all completed years to outputs + so it can be plotted alongside other ReEDS outputs. + + Notes + ----- + * The denominator of NEUE is exogenous electricity demand; it does not include + endogenous load from losses or H2 production or exogenous H2 demand. + """ + infiles = [ + i for i in sorted(glob( + os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', 'PRAS_*.h5'))) + if re.match(r"PRAS_[0-9]+i[0-9]+.h5", os.path.basename(i)) + ] + eue = {} + for infile in infiles: + year_iteration = os.path.basename(infile)[len('PRAS_'):-len('.h5')].split('i') + year = int(year_iteration[0]) + iteration = int(year_iteration[1]) + eue[year,iteration] = reeds.io.read_pras_results(infile)['USA_EUE'].sum() + eue = pd.Series(eue).rename('MWh') + eue.index = eue.index.rename(['year','iteration']) + + load = reeds.io.read_file(os.path.join(sw['casedir'],'inputs_case','load.h5')) + loadyear = load.sum(axis=1).groupby('year').sum() + + neue = ( + (eue / loadyear * 1e6).rename('NEUE [ppm]') + .rename_axis(['t','iteration']).sort_index() + ) + + if write: + neue.to_csv(os.path.join(sw['casedir'],'outputs','neue.csv')) + eue.to_csv(os.path.join(sw['casedir'],'outputs','eue.csv')) + return neue + + +def get_annual_neue(case, t, iteration=0): + """ + """ + ### Get EUE from PRAS + dfeue = reeds.resource_adequacy.get_pras_eue(case=case, t=t, iteration=iteration) + + ### Get load (for calculating NEUE) + dfload = reeds.io.read_h5py_file( + os.path.join( + case,'ReEDS_Augur','augur_data',f'pras_load_{t}.h5') + ) + dfload.index = dfeue.index + + levels = ['country','interconnect','nercr','transreg','transgrp','st','r'] + _neue = {} + for hierarchy_level in levels: + ### Get the region aggregator + rmap = reeds.io.get_rmap(case=case, hierarchy_level=hierarchy_level) + ### Get NEUE summed over year + _neue[hierarchy_level,'sum'] = ( + dfeue.rename(columns=rmap).groupby(axis=1, level=0).sum().sum() + / dfload.rename(columns=rmap).groupby(axis=1, level=0).sum().sum() + ) * 1e6 + ### Get max NEUE hour + _neue[hierarchy_level,'max'] = ( + dfeue.rename(columns=rmap).groupby(axis=1, level=0).sum() + / dfload.rename(columns=rmap).groupby(axis=1, level=0).sum() + ).max() * 1e6 + + ### Combine it + neue = pd.concat(_neue, names=['level','metric','region']).rename('NEUE_ppm') + + return neue + + +def get_shoulder_periods(sw, criterion, dfenergy_r, high_eue_periods): + ## Stop if not needed + if sw.GSw_PRM_StressStorageCutoff.lower() in ['off', '0', 'false']: + print( + f"GSw_PRM_StressStorageCutoff={sw.GSw_PRM_StressStorageCutoff} " + "so not adding shoulder stress periods based on storage level" + ) + return {} + if dfenergy_r.empty: + print( + "No storage capacity, so no shoulder stress periods will be added " + "based on storage level" + ) + return {} + + ## Parse inputs + hierarchy = reeds.io.get_hierarchy(sw.casedir) + timeindex = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) + cutofftype, cutoff = sw.GSw_PRM_StressStorageCutoff.lower().split('_') + periodhours = {'day':24, 'wek':24*5, 'year':24}[sw.GSw_HourlyType] + (hierarchy_level, ppm, stress_metric, period_agg_method) = criterion.split('_') + + ## Aggregate storage energy to hierarchy_level + dfenergy_agg = ( + dfenergy_r.rename(columns=hierarchy[hierarchy_level]) + .groupby(axis=1, level=0).sum() + ) + dfheadspace_MWh = dfenergy_agg.max() - dfenergy_agg + dfheadspace_frac = dfheadspace_MWh / dfenergy_agg.max() + + shoulder_periods = {} + for i, row in high_eue_periods[criterion, f'high_{stress_metric}'].iterrows(): + if row.r not in dfheadspace_MWh: + continue + + day = pd.Timestamp('-'.join(row[['y','m','d']].astype(str).tolist())) + + start_headspace_MWh = dfheadspace_MWh.loc[day.strftime('%Y-%m-%d'),row.r].iloc[0] + end_headspace_MWh = dfheadspace_MWh.loc[day.strftime('%Y-%m-%d'),row.r].iloc[-1] + + start_headspace_frac = dfheadspace_frac.loc[day.strftime('%Y-%m-%d'),row.r].iloc[0] + end_headspace_frac = dfheadspace_frac.loc[day.strftime('%Y-%m-%d'),row.r].iloc[-1] + + day_eue = high_eue_periods[criterion, f'high_{stress_metric}'].loc[i,'EUE'] + day_index = np.where( + timeindex == dfenergy_agg.loc[day.strftime('%Y-%m-%d')].iloc[0].name + )[0][0] + + day_before = timeindex[day_index - periodhours] + day_after = timeindex[(day_index + periodhours) % len(timeindex)] + + if ( + ((cutofftype == 'eue') and (end_headspace_MWh / day_eue >= float(cutoff))) + or ((cutofftype[:3] == 'cap') and (end_headspace_frac >= float(cutoff))) + or (cutofftype[:3] == 'abs') + ): + shoulder_periods[criterion, f'after_{row.name}'] = pd.Series({ + 'actual_period':day_after.strftime('y%Yd%j'), + 'y':day_after.year, 'm':day_after.month, 'd':day_after.day, 'r':row.r, + }).to_frame().T.set_index('actual_period') + print(f"Added {day_after} as shoulder stress period after {day}") + + if ( + ((cutofftype == 'eue') and (start_headspace_MWh / day_eue >= float(cutoff))) + or ((cutofftype[:3] == 'cap') and (start_headspace_frac >= float(cutoff))) + or (cutofftype[:3] == 'abs') + ): + shoulder_periods[criterion, f'before_{row.name}'] = pd.Series({ + 'actual_period':day_before.strftime('y%Yd%j'), + 'y':day_before.year, 'm':day_before.month, 'd':day_before.day, 'r':row.r, + }).to_frame().T.set_index('actual_period') + print(f"Added {day_before} as shoulder stress period before {day}") + + return shoulder_periods + + +def get_eue_sorted_periods(sw, t, iteration): + ### Get storage state of charge (SOC) to use in selection of "shoulder" stress periods + dfenergy = reeds.io.read_pras_results( + os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', f"PRAS_{t}i{iteration}-energy.h5") + ) + timeindex = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) + dfenergy.index = timeindex + ## Sum by region + dfenergy_r = ( + dfenergy + .rename(columns={c: c.split('|')[1] for c in dfenergy.columns}) + .groupby(axis=1, level=0).sum() + ) + + ### Get NEUE + neue = pd.read_csv( + os.path.join(sw.casedir, 'outputs', f'neue_{t}i{iteration}.csv'), + index_col=['level', 'metric', 'region'], + ).squeeze(1) + + ### Load this year's stress periods so we don't duplicate + stressperiods_this_iteration = pd.read_csv( + os.path.join( + sw['casedir'], 'inputs_case', f'stress{t}i{iteration}', 'period_szn.csv') + ) + + ### Check all stress criteria; for regions that fail, add new stress periods + _eue_sorted_periods = {} + failed = {} + high_eue_periods = {} + shoulder_periods = {} + for criterion in sw.GSw_PRM_StressThreshold.split('/'): + ## Example: criterion = 'transgrp_10_EUE_sum' + (hierarchy_level, ppm, stress_metric, period_agg_method) = criterion.split('_') + + eue_periods = reeds.resource_adequacy.get_eue_periods( + case=sw.casedir, t=t, iteration=iteration, + hierarchy_level=hierarchy_level, + stress_metric=stress_metric, + period_agg_method=period_agg_method, + ) + + ### Sort in descending stress_metric order + _eue_sorted_periods[criterion] = ( + eue_periods + .sort_values(stress_metric, ascending=False) + .reset_index().set_index('actual_period') + ) + + ### Get the threshold(s) and see if any of them failed + this_test = neue[hierarchy_level][period_agg_method] + + if (this_test > float(ppm)).any(): + failed[criterion] = this_test.loc[this_test > float(ppm)] + print(f"GSw_PRM_StressThreshold = {criterion} failed for:") + print(failed[criterion]) + ###### Add GSw_PRM_StressIncrement periods to the list for the next iteration + high_eue_periods[criterion, f'high_{stress_metric}'] = ( + _eue_sorted_periods[criterion].loc[ + ## Only include new stress periods for the region(s) that failed + _eue_sorted_periods[criterion].r.isin(failed[criterion].index) + ## Don't repeat existing stress periods + & ~(_eue_sorted_periods[criterion].index.isin( + stressperiods_this_iteration.actual_period)) + ] + ## Don't add dates more than once + .drop_duplicates(subset=['y','m','d']) + ## Keep the GSw_PRM_StressIncrement worst periods for each region. + ## If you instead want to keep the GSw_PRM_StressIncrement worst periods + ## overall, use .nlargest(int(sw.GSw_PRM_StressIncrement), stress_metric) + .groupby('r').head(int(sw.GSw_PRM_StressIncrement)) + ) + for period, row in high_eue_periods[criterion, f'high_{stress_metric}'].iterrows(): + print( + f"Added {period} " + f"({reeds.timeseries.h2timestamp(period).strftime('%Y-%m-%d')}) " + f"as stress period for {row.r} " + f"({stress_metric} = {row[stress_metric]})" + ) + + ### Include "shoulder periods" before or after each period + ### if the storage state of charge is low + shoulder_periods = { + **shoulder_periods, + **get_shoulder_periods(sw, criterion, dfenergy_r, high_eue_periods) + } + + ### Dealing with earlier criteria may also address later criteria, so stop here + break + + else: + print(f"GSw_PRM_StressThreshold = {criterion} passed") + + eue_sorted_periods = pd.concat(_eue_sorted_periods, names=['criterion']) + + ### Get lists of stress periods: new (added this iteration) and all + if len(failed): + new_stress_periods = pd.concat( + {**high_eue_periods, **shoulder_periods}, names=['criterion','periodtype'], + ).reset_index().drop_duplicates(subset='actual_period', keep='first') + else: + return failed, None, None + + ## Reproduce the format of inputs_case/stress_period_szn.csv + p = 'w' if sw.GSw_HourlyType == 'wek' else 'd' + new_stressperiods_write = pd.DataFrame({ + 'rep_period': new_stress_periods.actual_period, + 'year': new_stress_periods.actual_period.map( + lambda x: int(x.strip('sy').split(p)[0])), + 'yperiod': new_stress_periods.actual_period.map( + lambda x: int(x.strip('sy').split(p)[1])), + 'actual_period': new_stress_periods.actual_period, + }) + + ### Add new stress periods to the stress periods used for this year/iteration, then write + newstresspath = f'stress{t}i{iteration+1}' + os.makedirs(os.path.join(sw['casedir'], 'inputs_case', newstresspath), exist_ok=True) + outpath = os.path.join(sw['casedir'], 'inputs_case', newstresspath, 'period_szn.csv') + + combined_periods_write = pd.concat( + [stressperiods_this_iteration, new_stressperiods_write], + axis=0, + ).drop_duplicates(keep='first') + + if int(sw.GSw_PRM_CapCredit): + pd.DataFrame(columns=['rep_period','year','yperiod','actual_period']).to_csv( + outpath, + index=False, + ) + else: + combined_periods_write.to_csv(outpath, index=False) + + ### Tables and plots for debugging + eue_sorted_periods.round(2).rename(columns={'EUE':'EUE_MWh','NEUE':'NEUE_ppm'}).to_csv( + os.path.join(sw.casedir, 'inputs_case', newstresspath, 'eue_sorted_periods.csv') + ) + new_stress_periods.round(2).rename(columns={'EUE':'EUE_MWh','NEUE':'NEUE_ppm'}).to_csv( + os.path.join(sw.casedir, 'inputs_case', newstresspath, 'new_stress_periods.csv'), + index=False, + ) + plot_eue_diagnostics(sw, t, iteration, high_eue_periods) + + return failed, new_stressperiods_write, combined_periods_write + + +def prm_increment_pras(sw, t, iteration, combined_periods_write, failed_regions): + try: + hmap = pd.read_csv( + os.path.join(sw.casedir, 'inputs_case', f'stress{t}i{iteration+1}', 'hmap_allyrs.csv') + ) + stress_hours = hmap.loc[ + hmap.actual_period.str.contains('|'.join(combined_periods_write.actual_period)) + ] + except FileNotFoundError: + # if there are no stress periods being modeled, use dispatch year to + # fill in for stress hours + stress_hours = pd.read_csv( + os.path.join(sw.casedir, 'inputs_case', 'rep', 'hmap_myr.csv') + ) + + ## shortfall data + # read the net shortfall (positive) and net surplus (negative) results + # by sample from PRAS run (MWh) + filepath = os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', + f'PRAS_{sw["t"]}i{iteration}-shortfall_samples.h5') + net_short = reeds.io.read_pras_results(filepath) + # get number of samples + n_samples = len(net_short) + # collapse dict of dataframes by sample in 1 dataframe (keep index to preserve hours) + net_short = pd.concat( + (df.assign(**{"sample": k}) for k, df in net_short.items()), ignore_index=False) + # convert to long format with shortfall by sample, hour, and r + net_short.index.names=['hour'] + net_short = net_short.reset_index().set_index(['sample','hour']) + net_short = net_short.sort_index(level=['sample', 'hour'], ascending=[True, True]) + net_short = net_short.melt( + ignore_index=False, var_name='r', value_name='net_short_mwh').reset_index() + + # zero-out negative values (net surplus) for determining regional unserved energy totals + net_short['net_short_mwh'] = net_short['net_short_mwh'].clip(lower=0) + # calaculate total regional net shortfall for all hours by sample + net_short_crit = net_short.groupby(['r','sample'], as_index=False)['net_short_mwh'].sum() + + ## get load data + dfload = reeds.io.read_file( + os.path.join( + sw['casedir'],'ReEDS_Augur','augur_data',f'pras_load_{t}.h5'), + parse_timestamps=True + ) + + # add an index to represent each hour + dfload = dfload.reset_index().reset_index().rename(columns={"index":"hour"}) + + # melt to long + dfload = dfload.melt(id_vars=['datetime', 'hour'], var_name='r', value_name='load_mwh') + + ## get regional load for (1) all hours (2) just the stress periods + ## total load is used to translate the ppm target to EUE, whereas + ## the stress period load is used to back-calculate the incremental prm + ## needed to get to the target + + # total load by r + dfload_all = dfload.groupby(['r'], as_index=False)['load_mwh'].sum() + + # total stress period load by r + # note: use hour0 to subset to stress periods here since load data starts with hour index 0 + dfload_stress = dfload.loc[dfload.hour.isin(stress_hours.hour0)] + dfload_stress = dfload_stress.groupby(['r'], as_index=False)['load_mwh'].sum() + dfload_stress = dfload_stress.rename(columns={'load_mwh':'stress_load_mwh'}) + + # combine + dfload_all = dfload_all.merge(dfload_stress) + + # transform the reliability target criteria by region from ppm into + # unserved energy (MWh) + dfload_all = dfload_all.merge(failed_regions, on='r') + dfload_all['target_eue_mwh'] = ( + dfload_all['ppm'] / 1e6 * dfload_all['load_mwh'] + ) + + ## calculate piece-wise linear function (plf) that estimates the change in EUE + ## across the samples as a function of the amount of surplus added added to address + ## unserved energy in each sample each segment of the plf is defined by a slope and + ## two points: (x1, y1) and (x2, y2) + plfs = net_short_crit.loc[net_short_crit.net_short_mwh > 0].copy() + ## y-intercept: initial EUE + plfs['intercept'] = plfs.groupby('r')['net_short_mwh'].transform('sum') / n_samples + ## slope: computed from the lolp based on the remaining periods with unserved energy + ## as surplus is added sort unserved by descending first to calculate slopes + plfs = plfs.sort_values(['r', 'net_short_mwh'], ascending=False) + plfs['slope'] = -1 + plfs['slope'] = plfs.groupby(['r'])['slope'].transform('cumsum') / n_samples + # resort in ascending order for later calculations + plfs = plfs.sort_values(['r', 'net_short_mwh'], ascending=True) + ## x1: surplus to add to eliminate unserved energy from previous sample + plfs['x1'] = plfs.groupby('r')['net_short_mwh'].shift(1, fill_value=0) + ## x2: surplus to add to eliminate unserved energy from this sample + plfs['x2'] = plfs['net_short_mwh'] + # compute change in y value over each segment + plfs['Dy'] = plfs['slope'] * (plfs['x2']-plfs['x1']) + # check: Dy should never be positive + assert plfs['Dy'].max() <= 0, "Error in Dy calculation" + ## y1: intercept + cumulative change in unserved (Dy) + plfs['y1'] = plfs['intercept'] + plfs.groupby('r')['Dy'].transform( + lambda x: x.cumsum().shift(1, fill_value=0)) + ## y2: y1 + change over that segment (next y1 value) + plfs['y2'] = plfs.groupby('r')['y1'].shift(-1, fill_value=0) + + # now merge load merge with plf functions to find the segment that captures the target + plfs = plfs.merge(dfload_all, on='r') + plfs['seg'] = 0 + plfs.loc[(plfs['target_eue_mwh']<=plfs['y1']) & ( + plfs['target_eue_mwh']>=plfs['y2']), 'seg'] = 1 + # calculate the energy surplus to add by backtracking from the target_eue on the + # relevant segment(y): y=a+b*x => x=(y-a)/b + prm_increment = plfs.loc[plfs['seg']==1].copy() + prm_increment['surplus_mwh'] = prm_increment['x1'] + ( + prm_increment['target_eue_mwh'] - prm_increment['y1']) * (1 / prm_increment['slope']) + # calculate the prm increase as the required surplus as a fraction of + # load during stress periods + prm_increment['fraction'] = ( + prm_increment['surplus_mwh'] / prm_increment['stress_load_mwh'] + ) + prm_increment = prm_increment[['r','fraction']].reset_index(drop=True) + return prm_increment + + +def update_prm(sw, t, iteration, failed, combined_periods_write): + """Update the energy reserve margin by region r for stress periods, either using a + static increment (GSw_PRM_UpdateMethod=1) or based on the estimated surplus needed by PRAS + to recover the desired reliabiliaty criteria (GSw_PRM_UpdateMethod>1). + + Args: + sw (pd.series): ReEDS switches for this run. + t (int): Model solve year. + iteration (int): ReEDS-PRAS iteration + failed (dict): Dictionary of regions with unserved energy at the hierarchy_level + and their criterion evaluations + stress_hours (pd.DataFrame): data frame of stress periods + + Returns: + pd.DataFrame: Table of prm levels for the next PRAS iteration + """ + # Get regions that failed criteria + _failed_regions = [] + for criterion in failed: + # Example: criterion = 'transgrp_10_EUE_sum' + (hierarchy_level, ppm, __, __) = criterion.split('_') + # Recover regions where the PRM criterion failed + rmap = reeds.io.get_rmap(sw['casedir'], hierarchy_level=hierarchy_level).reset_index() + df = rmap.loc[ + rmap[hierarchy_level].isin(failed[criterion].index) + ].rename(columns={hierarchy_level:'region'}) + df['hierarchy_level'] = hierarchy_level + df['ppm'] = float(ppm) + _failed_regions.append(df) + # For zones that failed multiple criteria, use the most stringent (lowest EUE target) + failed_regions = ( + pd.concat(_failed_regions) + .sort_values(by=['ppm']) + .drop_duplicates(subset='r', keep='first') + ) + + ## Fixed-increment update + if int(sw.GSw_PRM_UpdateMethod) == 1: + prm_increment = failed_regions.copy() + prm_increment['fraction'] = float(sw['GSw_PRM_UpdateFraction']) + ## PRAS-informed PRM update + else: + prm_increment = prm_increment_pras( + sw, + t, + iteration, + combined_periods_write, + failed_regions, + ) + prm_increment = ( + prm_increment.rename(columns={'r':'*r'}) + .set_index('*r').fraction + ) + + ## Add the PRM increment to last iteration's PRM + prm = pd.read_csv( + os.path.join(sw['casedir'], 'inputs_case', f'stress{t}i{iteration}', 'prm.csv'), + index_col='*r', + ).fraction + prm_next_iteration = prm.add(prm_increment, fill_value=0).round(3) + + return prm_next_iteration + + +#%%### Procedure +def main(sw, t, iteration=0, logging=True): + """ + """ + #%% More imports and settings + site.addsitedir(os.path.join(sw['casedir'],'reeds','inputs')) + import hourly_writetimeseries + newstresspath = f'stress{t}i{iteration+1}' + + #%% Write consolidated NEUE so far + try: + _neue_simple = get_and_write_neue(sw, write=True) + neue = get_annual_neue(sw.casedir, t, iteration=iteration) + neue.round(2).to_csv( + os.path.join(sw.casedir, 'outputs', f"neue_{t}i{iteration}.csv") + ) + + except Exception as err: + if int(sw['pras']) == 2: + print(traceback.format_exc()) + if int(sw.GSw_PRM_StressIterateMax): + raise Exception(err) + + #%% Stop here if not iterating or if before ReEDS can build new capacity + if (not int(sw.GSw_PRM_StressIterateMax)) or (t < int(sw['GSw_StartMarkets'])): + return + + #%% Identify and write new stress periods + failed, new_stressperiods_write, combined_periods_write = get_eue_sorted_periods( + sw=sw, t=t, iteration=iteration, + ) + + #%% Stop here if all thresholds pass or if there are no new stress periods + if ( + (not len(failed)) + or ((len(new_stressperiods_write) == 0) and (int(sw.GSw_PRM_UpdateMethod) == 0)) + ): + print('No new stress periods and no PRM update, so stopping here') + return + + #%% Write timeseries data for stress periods for the next iteration of ReEDS + hourly_writetimeseries.main( + sw=sw, reeds_path=sw['reeds_path'], + inputs_case=os.path.join(sw['casedir'], 'inputs_case'), + periodtype=newstresspath, + make_plots=0, + logging=logging + ) + + #%% Write updated PRM values + if ( + (int(sw.GSw_PRM_UpdateMethod) == 0) + or (len(new_stressperiods_write) and (int(sw.GSw_PRM_UpdateMethod) == 3)) + ): + ## Not updating PRM, so copy last year's + prm_next_iteration = pd.read_csv( + os.path.join(sw.casedir, 'inputs_case', f'stress{t}i{iteration}', 'prm.csv'), + index_col='*r', + ) + else: + prm_next_iteration = update_prm(sw, t, iteration, failed, combined_periods_write) + + prm_next_iteration.to_csv( + os.path.join(sw.casedir, 'inputs_case', newstresspath, 'prm.csv'), + ) + + #%% Done + return + + +# if __name__ == '__main__': +# #%%### option to run script directly for debugging +# casedir = "/path/to/ReEDS-2.0/runs/runname" +# t = 2030 # previous solve year +# iteration = 0 +# # load switches +# sw = reeds.io.get_switches(casedir) +# sw['t'] = t +# sw['GSw_PRM_UpdateMethod'] = 2 +# #%%### +# main(sw, t, iteration, logging=False) diff --git a/reeds/solver/cbc.opt b/reeds/solver/cbc.opt new file mode 100644 index 00000000..28df6f39 --- /dev/null +++ b/reeds/solver/cbc.opt @@ -0,0 +1,3 @@ +startalg barrier +crash off +threads 8 \ No newline at end of file diff --git a/reeds/solver/cplex.op2 b/reeds/solver/cplex.op2 new file mode 100644 index 00000000..e01f19dd --- /dev/null +++ b/reeds/solver/cplex.op2 @@ -0,0 +1,71 @@ +*** https://www.gams.com/latest/docs/S_CPLEX.html +*** threads (integer): global default thread count +threads = 8 + +*** lpmethod (integer): algorithm to be used for LP problems [4 = barrier] +lpmethod = 4 + +*** advind (integer): advanced basis use [0 = do not use advanced basis] +advind 0 + +*** reslim (integer): solve time limit in seconds [172800 seconds = 2 days] +reslim 172800 + +*** scaind (integer): matrix scaling on/off [1 = modified, more aggressive scaling method] +scaind 1 + +*** aggind (integer): aggregator on/off [5 = aggregator will be applied 5 times] +aggind 5 + +*** iis (boolean): run the conflict refiner also known as IIS finder if the problem is infeasible +iis 1 + +*** eprhs (float): feasibility tolerance [default = 1e-6] +eprhs = 1e-5 + +*** epopt (float): optimality tolerance [default = 1e-6] +epopt = 1e-5 + +*** memoryemphasis (boolean): reduces use of memory but writes TBs of files to disk +* memoryemphasis 1 + +*** epmrk (float): Markowitz pivot tolerance +*epmrk = 0.1 + +*** barepcomp (float): tolerance on complementarity for convergence of the barrier algorithm [default 1e-8] +barepcomp = 1e-10 + +*############################################################################################ +*## The settings below can reduce solve time substantially, but their impacts on the solution +*## have not yet been fully characterized, and some of them might do nothing. Use at your own risk. +*## Some background details are available at +*## https://www.slideshare.net/IEA-ETSAP/improving-the-solution-time-of-times-by-playing-with-cplexbarrier +*## https://iea-etsap.org/webinar/CPLEX%20options%20for%20running%20TIMES%20models.pdf + +*** numericalemphasis (boolean): emphasizes precision in numerically unstable or difficult problems [default 0] +* numericalemphasis 1 + +*** depind (integer): dependency checker on/off. 3 = turn on at beginning and end of preprocessing [default -1] +* depind 3 + +*** barcolnz (integer): specifies the number of entries in columns to be considered as dense [default 0] +*** commenting this out (i.e., setting back to zero) can improve solve time when running with hydrogen transport (GSw_H2=2) +barcolnz 100 + +*** barorder (integer): row ordering algorithm selection. 1 = approximate minimum degree (AMD) [default 0] +* barorder 1 + +*** baralg (integer): barrier algorithm selection. 1 = infeasibility-estimate start, 3 = standard barrier [default 0] +* baralg 3 + +*** barstartalg (integer): barrier starting point algorithm. 2 = default primal, estimate dual [default 1] +* barstartalg 2 + +*** scaind (described above) +* scaind 0 + +*** Can use bardisplay=2 to print more diagnostics about the barrier method and choice of barcolnz +* bardisplay 2 + +*** solutiontype (integer): type of solution (basic or non basic): 0 does basic with crossover, 2 skips crossover +* solutiontype 2 diff --git a/reeds/solver/cplex.opt b/reeds/solver/cplex.opt new file mode 100644 index 00000000..0a6c3ea6 --- /dev/null +++ b/reeds/solver/cplex.opt @@ -0,0 +1,71 @@ +*** https://www.gams.com/latest/docs/S_CPLEX.html +*** threads (integer): global default thread count +threads = 8 + +*** lpmethod (integer): algorithm to be used for LP problems [4 = barrier] +lpmethod = 4 + +*** advind (integer): advanced basis use [0 = do not use advanced basis] +advind 0 + +*** reslim (integer): solve time limit in seconds [172800 seconds = 2 days] +reslim 172800 + +*** scaind (integer): matrix scaling on/off [1 = modified, more aggressive scaling method] +scaind 1 + +*** aggind (integer): aggregator on/off [5 = aggregator will be applied 5 times] +aggind 5 + +*** iis (boolean): run the conflict refiner also known as IIS finder if the problem is infeasible +iis 1 + +*** eprhs (float): feasibility tolerance [default = 1e-6] +eprhs = 1e-5 + +*** epopt (float): optimality tolerance [default = 1e-6] +epopt = 1e-6 + +*** memoryemphasis (boolean): reduces use of memory but writes TBs of files to disk +memoryemphasis 0 + +*** epmrk (float): Markowitz pivot tolerance +*epmrk = 0.1 + +*** barepcomp (float): tolerance on complementarity for convergence of the barrier algorithm [default 1e-8] +barepcomp = 1e-8 + +*############################################################################################ +*## The settings below can reduce solve time substantially, but their impacts on the solution +*## have not yet been fully characterized, and some of them might do nothing. Use at your own risk. +*## Some background details are available at +*## https://www.slideshare.net/IEA-ETSAP/improving-the-solution-time-of-times-by-playing-with-cplexbarrier +*## https://iea-etsap.org/webinar/CPLEX%20options%20for%20running%20TIMES%20models.pdf + +*** numericalemphasis (boolean): emphasizes precision in numerically unstable or difficult problems [default 0] +* numericalemphasis 1 + +*** depind (integer): dependency checker on/off. 3 = turn on at beginning and end of preprocessing [default -1] +* depind 3 + +*** barcolnz (integer): specifies the number of entries in columns to be considered as dense [default 0] +*** 0 lets CPLEX choose; values ranging from 30-300 can sometimes shorten runtime +barcolnz 30 + +*** barorder (integer): row ordering algorithm selection. 1 = approximate minimum degree (AMD) [default 0] +* barorder 1 + +*** baralg (integer): barrier algorithm selection. 1 = infeasibility-estimate start, 3 = standard barrier [default 0] +* baralg 1 + +*** barstartalg (integer): barrier starting point algorithm. 2 = default primal, estimate dual [default 1] +* barstartalg 2 + +*** scaind (described above) +* scaind 0 + +*** Can use bardisplay=2 to print more diagnostics about the barrier method and choice of barcolnz +* bardisplay 2 + +*** solutiontype (integer): type of solution (basic or non basic): 0 does basic with crossover, 2 skips crossover +* solutiontype 2 diff --git a/reeds/solver/gurobi.opt b/reeds/solver/gurobi.opt new file mode 100644 index 00000000..cba6c407 --- /dev/null +++ b/reeds/solver/gurobi.opt @@ -0,0 +1,4 @@ +objscale = 1e9 +ScaleFlag = 2 +Threads = 4 +method = 2 diff --git a/run.py b/run.py new file mode 100644 index 00000000..fd08b810 --- /dev/null +++ b/run.py @@ -0,0 +1,1811 @@ +#%% =========================================================================== +### --- IMPORTS --- +### =========================================================================== + +import os +import git +import queue +import threading +import time +import shutil +import csv +import importlib +import numpy as np +import pandas as pd +import subprocess +import re +from datetime import datetime +import argparse +from pathlib import Path +import reeds + +# Assert core programs are accessible +CORE_PROGRAMS = ["gams"] +if not all(shutil.which(program) for program in CORE_PROGRAMS): + msg = ( + "Programs needed to run reeds not accessible on the environment. " + f"Check that all the {CORE_PROGRAMS=} are accessible on the PATH." + ) + raise ImportError(msg) + +#%% Constants +LINUXORMAC = True if os.name == 'posix' else False +ext = '.sh' if LINUXORMAC else '.bat' + +YAMPASERVERS = ['constellation01','cepheus','corvus','dorado','delphinus'] + +#%% =========================================================================== +### --- FUNCTIONS --- +### =========================================================================== + +def writeerrorcheck(checkfile, errorcode=17): + """ + Inputs + ------ + checkfile: Filename to check. If it does not exist, stop the run. + errorcode: Value to return if check fails. Should be >0. + """ + if LINUXORMAC: + return f'if [ ! -f {checkfile} ]; then echo "missing {checkfile}"; exit {errorcode}; fi\n' + else: + return f'\nif not exist {checkfile} (\n echo file {checkfile} missing \n goto:eof \n) \n \n' + +def writescripterrorcheck(script, errorcode=18): + """ + """ + if LINUXORMAC: + return f'if [ $? != 0 ]; then echo "{script} returned $?" >> gamslog.txt; exit {errorcode}; fi\n' + else: + return f'if not %errorlevel% == 0 (echo {script} returned %errorlevel%\ngoto:eof\n)\n' + + +def write_delete_file(checkfile, deletefile, PATH): + if LINUXORMAC: + PATH.writelines(f"if [ -f {checkfile} ]; then rm {deletefile}; fi\n") + else: + PATH.writelines("if exist " + checkfile + " (del " + deletefile + ')\n' ) + + +def comment(text, PATH): + commentchar = '#' if LINUXORMAC else '::' + PATH.writelines(f'{commentchar} {text}\n') + + +def big_comment(text, PATH): + commentchar = '#' if LINUXORMAC else '::' + PATH.writelines(f'\n{commentchar}\n') + comment(text, PATH) + PATH.writelines(f'{commentchar}\n') + + +def create_case_lists(df_cases:pd.DataFrame, BatchName:str, single:str=''): + """ + """ + # Initiate the empty lists which will be filled with info from cases + # Needs to be done after the MCS runs are processed, so that the case names are correct + caseList = [] + caseSwitches = [] #list of dicts, one dict for each case + # Redefine casenames to include all the Monte Carlo cases, which have been expanded in df_cases. + casenames = list(df_cases.columns) + + for case in casenames: + # If --single/-s was passed, only keep those cases (regardless of ignore) + # otherwise, drop any case marked ignore + if single: + if case not in single.split(','): + continue + else: + if int(df_cases.loc['ignore', case]) == 1: + continue + # Add switch settings to list of options passed to GAMS + shcom = f' --case={BatchName}_{case}' + for i,v in df_cases[case].items(): + #exclude certain switches that don't need to be passed to GAMS + if i not in ['file_replacements','keep_run_terminal']: + shcom += f' --{i}={v}' + caseList.append(shcom) + caseSwitches.append(df_cases[case].to_dict()) + + return caseSwitches, casenames, caseList + + +def get_ivt_numclass(reeds_path, casedir, caseSwitches): + """ + Extend ivt if necessary and calculate numclass + """ + ivt = pd.read_csv( + os.path.join( + reeds_path, 'inputs', 'userinput', 'ivt_{}.csv'.format(caseSwitches['ivt_suffix'])), + index_col=0) + ivt_step = pd.read_csv(os.path.join(reeds_path, 'inputs', 'userinput', 'ivt_step.csv'), + index_col=0).squeeze(1) + lastdatayear = max([int(c) for c in ivt.columns]) + addyears = list(range(lastdatayear + 1, int(caseSwitches['endyear']) + 1)) + num_added_years = len(addyears) + ### Add v for the extra years + ivt_add = {} + for i in ivt.index: + vlast = ivt.loc[i,str(lastdatayear)] + if ivt_step[i] == 0: + ### Use the same v forever + ivt_add[i] = [vlast] * num_added_years + else: + ### Use the same spacing forever + forever = [[vlast + 1 + x] * ivt_step[i] for x in range(1000)] + forever = [item for sublist in forever for item in sublist] + ivt_add[i] = forever[:num_added_years] + ivt_add = pd.DataFrame(ivt_add, index=addyears).T + ### Concat and resave + ivtout = pd.concat([ivt, ivt_add], axis=1) + ivtout.to_csv(os.path.join(casedir, 'inputs_case', 'ivt.csv')) + ### Get numclass, which is used in b_inputs.gms + numclass = ivtout.max().max() + + return numclass + + +def get_rev_paths(revswitches, caseSwitches): + # Expand on reV path based on where this run is happening + # when running on the HPC this links to the shared-projects folder + hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False + if os.environ.get('NREL_CLUSTER') == 'kestrel': + hpc_path = '/kfs2/shared-projects/reeds/Supply_Curve_Data' + else: + hpc_path = '/shared-projects/reeds/Supply_Curve_Data' + + if hpc: + rev_prefix = hpc_path + else: + hostname = os.environ.get('HOSTNAME') + if (hostname) and (hostname.split('.')[0] in YAMPASERVERS): + drive = '/data/shared/shared_data' + elif LINUXORMAC: + drive = '/Volumes' + else: + drive = '//nrelnas01' + rev_prefix = os.path.join(drive,'ReEDS','Supply_Curve_Data') + revswitches['hpc_sc_path'] = revswitches['sc_path'].apply(lambda row: os.path.join(hpc_path,row)) + revswitches['sc_path'] = revswitches['sc_path'].apply(lambda row: os.path.join(rev_prefix,row)) + revswitches['rev_path'] = revswitches.apply(lambda row: os.path.join(row.sc_path, "reV", row.rev_case), axis=1) + + # link to the pre-processed reV supply curves from hourlize + def get_rev_sc_file_name(caseSwitches, rev_row, use_hpc=False): + if pd.isnull(rev_row.original_sc_file): + return "" + else: + if caseSwitches['GSw_RegionResolution'] == "county": + sc_folder_suffix = "_county" + else: + sc_folder_suffix = "_ba" + + # link to HPC or other sc_path + if use_hpc: + sc_path = rev_row.hpc_sc_path + else: + sc_path = rev_row.sc_path + + # supply curve name should be in format of {tech}_rev_supply_curves_raw.csv + # in the hourlize results folder (must match format in 'save_sc_outputs' function of hourlize/resource.py) + sc_file = os.path.join(sc_path, + rev_row.tech + "_" + rev_row.access_case + sc_folder_suffix, + "results", + rev_row.tech + "_supply_curve_raw.csv" + ) + return sc_file + revswitches['sc_file'] = revswitches.apply(lambda row: get_rev_sc_file_name(caseSwitches, row), axis=1) + revswitches['hpc_sc_file'] = revswitches.apply(lambda row: get_rev_sc_file_name(caseSwitches, row, use_hpc=True), axis=1) + + return revswitches + +def check_compatibility(sw): + if int(sw['startyear']) < 2010: + raise ValueError(f"startyear = {sw['startyear']} but must be ≥ 2010") + + if (sw['GSw_HourlyType'] in ['year']) and int(sw['GSw_InterDayLinkage']): + raise ValueError( + "GSw_HourlyType cannot be 'year' when GSw_InterDayLinkage is enabled. " + f"Current values: GSw_HourlyType={sw['GSw_HourlyType']}, GSw_InterDayLinkage={sw['GSw_InterDayLinkage']}" + ) + + if 24 % (int(sw['GSw_HourlyWindowOverlap']) * int(sw['GSw_HourlyChunkLengthRep'])): + raise ValueError( + ('24 must be divisible by GSw_HourlyWindowOverlap * GSw_HourlyChunkLengthRep:' + '\nGSw_HourlyWindowOverlap = {}\nGSw_HourlyChunkLengthRep = {}'.format( + sw['GSw_HourlyWindowOverlap'], sw['GSw_HourlyChunkLengthRep']))) + + if int(sw['GSw_HourlyWindow']) <= int(sw['GSw_HourlyWindowOverlap']): + raise ValueError( + ('GSw_HourlyWindow must be greater than GSw_HourlyWindowOverlap:' + '\nGSw_HourlyWindow = {}\nGSw_HourlyWindowOverlap = {}'.format( + sw['GSw_HourlyWindow'], sw['GSw_HourlyWindowOverlap']))) + + if ((sw['GSw_HourlyClusterAlgorithm'] not in ['hierarchical','optimized','kmeans','kmedoids']) + and ('user' not in sw['GSw_HourlyClusterAlgorithm']) + ): + if sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical'): + args = sw['GSw_HourlyClusterAlgorithm'].split('_') + assert len(args) == 3 + ## https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html + assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] + assert args[2] in ['ward', 'complete', 'average', 'single'] + if args[2] == 'ward': + assert args[1] == 'euclidean' + elif sw['GSw_HourlyClusterAlgorithm'].startswith('kmedoids'): + args = sw['GSw_HourlyClusterAlgorithm'].split('_') + assert len(args) == 3 + assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] + assert args[2] in ['heuristic','k-medoids++','random','build'] + else: + raise ValueError( + "GSw_HourlyClusterAlgorithm must be set to 'hierarchical', 'optimized', " + "'kmeans', or 'kmedoids', or must " + "contain the substring 'user' and match a scenario in " + "inputs/temporal/period_szn_user.csv" + ) + + if ((sw['GSw_PRM_StressModel'].lower() not in ['pras']) + and ('user' not in sw['GSw_PRM_StressModel'])): + raise ValueError( + "GSw_PRM_StressModel must be set to 'pras' or must " + "contain the substring 'user' and match a scenario at " + "inputs/temporal/stressperiods_{GSw_PRM_StressModel}.csv" + ) + + if (int(sw['GSw_H2_PTC']) == 1) and (int(sw['GSw_H2']) != 2): + raise ValueError( + 'When running with the H2 PTC enabled, GSw_H2 should be set to 2.\n' + f"GSw_H2_PTC={sw['GSw_H2_PTC']}, GSw_H2={sw['GSw_H2']}" + ) + + if int(sw['GSw_H2_SMR']) == 0 and sw['GSw_H2_Demand_Case'] in ['BAU', 'Aggressive', 'Decarb_with_BAU']: + raise ValueError( + f"GSw_H2_SMR is set to 0, but GSw_H2_Demand_Case is set to '{sw['GSw_H2_Demand_Case']}', which requires SMR set to 1.\n" + "When GSw_H2_SMR is 0, GSw_H2_Demand_Case must be one of: 'none', 'Decarb', or 'LTS'." + ) + + if ('usa' not in sw['GSw_Region'].lower()) and (int(sw['GSw_GasCurve']) != 2): + raise ValueError( + 'Should use GSw_GasCurve=2 (fixed prices) when running sub-nationally\n' + f"GSw_Region={sw['GSw_Region']}, GSw_GasCurve={sw['GSw_GasCurve']}" + ) + + if sw['GSw_RegionResolution'] in ['county','mixed']: + err_switch_configs = [] + if int(sw['GSw_OffshoreZones']): + err_switch_configs.append('GSw_OffshoreZones=1') + if sw['GSw_LoadAllocationMethod'] == 'state_lpf': + err_switch_configs.append('GSw_LoadAllocationMethod=state_lpf') + + if len(err_switch_configs) > 0: + raise NotImplementedError( + 'The following switch configurations are not implemented for ' + 'county/mixed resolution:\n{}\n' + .format('\n'.join(err_switch_configs)) + ) + + reeds.parse.validate_zoneset(sw['GSw_ZoneSet']) + + ### Aggregation + if (sw['GSw_RegionResolution'] != 'aggreg') and (int(sw['GSw_NumCSPclasses']) != 12): + raise NotImplementedError( + 'Aggregated CSP classes only work with aggregated regions. ' + 'GSw_NumCSPclasses is incompatible with ' + 'GSw_RegionResolution != aggreg') + + ### Parsed string switches + ## Automatic inputs + reeds_path = os.path.dirname(__file__) + hierarchy = reeds.io.get_hierarchy(GSw_ZoneSet=sw['GSw_ZoneSet']).reset_index() + + for threshold in sw['GSw_PRM_StressThreshold'].split('/'): + ## Example: threshold = 'transgrp_10_EUE_sum' + allowed_levels = ['country','interconnect','nercr','transreg','transgrp','st','r'] + (hierarchy_level, ppm, stress_metric, period_agg_method) = threshold.split('_') + if hierarchy_level not in allowed_levels: + raise ValueError( + f"GSw_PRM_StressThreshold: level={hierarchy_level} but must be in:\n" + + '\n'.join(allowed_levels) + ) + if period_agg_method.lower() not in ['sum','max']: + raise ValueError("Fix period agg method in GSw_PRM_StressThreshold") + if not (float(ppm) >= 0): + raise ValueError( + "ppm in GSw_PRM_StressThreshold must be a positive number " + f"but '{ppm}' was provided" + ) + if stress_metric.upper() not in ['EUE','NEUE']: + raise ValueError( + "stress metric in GSw_PRM_StressThreshold must be 'EUE' or 'NEUE' " + f"but '{stress_metric}' was provided" + ) + if (sw['GSw_PRM_StressModel'].lower() != 'pras') and (stress_metric.upper() != 'EUE'): + err = ( + f"The combination of GSw_PRM_StressModel={sw['GSw_PRM_StressModel']} and " + f"stress_metric={stress_metric} is not supported." + ) + raise NotImplementedError(err) + + if sw['GSw_PRM_StressStorageCutoff'].lower() not in ['off','0','false']: + metric, value = sw['GSw_PRM_StressStorageCutoff'].split('_') + if metric.lower()[:3] not in ['eue', 'cap', 'abs']: + raise ValueError( + "The first argument of GSw_PRM_StressStorageCutoff must be in " + f"['eue', 'cap', 'abs'] but {metric} was provided" + ) + try: + float(value) + except ValueError: + raise ValueError( + "The second argument of GSw_PRM_StressStorageCutoff must be a number " + f"but {value} was provided" + ) + if (metric.lower()[:3] == 'abs') and (int(value) != 1): + raise NotImplementedError( + "GSw_PRM_StressStorageCutoff: only abs_1 is implemented for abs but " + f"{metric}_{value} was provided" + ) + + for keyval in sw['GSw_PRM_NetImportLimitScen'].split('/'): + err = ( + "GSw_PRM_NetImportLimitScen accepts inputs in the format " + "{year1}_{'hist' or float}/{year2}_{float}/{year3}_{float} " + "or a single value given as {year1}_{'hist' or float}. Examples are " + "2024_hist/2035_40, 2025_20/2032_40, 2024_hist, 2025_20/2032_40/2050_60. " + f"You entered {sw['GSw_PRM_NetImportLimitScen']}." + ) + year, limit = keyval.split('_') + try: + int(year) + except ValueError: + raise ValueError(err) + if limit not in ['hist', 'histmax']: + try: + float(limit) + except ValueError: + raise ValueError(err) + + for bir in sw['GSw_PVB_BIR'].split('_'): + if not (float(bir) >= 0): + raise ValueError("Fix GSw_PVB_BIR") + + for ilr in sw['GSw_PVB_ILR'].split('_'): + if not (float(ilr) >= 0): + raise ValueError("Fix GSw_PVB_ILR") + + for pvbtype in sw['GSw_PVB_Types'].split('_'): + if not (1 <= int(pvbtype) <= 3): + raise ValueError("Fix GSw_PVB_Types") + + try: + prm = float(sw['GSw_PRM_scenario']) + if prm >= 1: + raise Exception( + f"GSw_PRM_scenario={sw['GSw_PRM_scenario']} but should be formatted as a " + "fraction, not a percent" + ) + except ValueError: + pass + + scalars = reeds.io.get_scalars() + ilr_upv = scalars['ilr_utility'] * 100 + + if ( + int(sw['GSw_PVB']) + and not all([np.isclose(float(ilr), ilr_upv) for ilr in sw['GSw_PVB_ILR'].split('_')]) + ): + raise ValueError( + f"GSw_PVB_ILR = {sw['GSw_PVB_ILR']} but all entries must be {int(ilr_upv)}" + ) + + allowed_years = list(range(2007,2014)) + list(range(2016,2024)) + allowed_years_string = ','.join([str(year) for year in allowed_years]) + + resource_adequacy_years = [int(y) for y in sw['resource_adequacy_years'].split('_')] + for year in resource_adequacy_years: + if year not in allowed_years: + raise ValueError( + f"resource_adequacy_years must be in {allowed_years_string} but is " + f"{sw['resource_adequacy_years']}" + ) + + for year in sw['GSw_HourlyWeatherYears'].split('_'): + if int(year) not in allowed_years: + raise ValueError( + f"GSw_HourlyWeatherYears must be in {allowed_years_string} but is " + f"{sw['GSw_HourlyWeatherYears']}" + ) + + if int(year) not in resource_adequacy_years: + raise ValueError( + "GSw_HourlyWeatherYears must be a subset of resource_adequacy_years but " + f"GSw_HourlyWeatherYears={sw['GSw_HourlyWeatherYears']} and " + f"resource_adequacy_years={sw['resource_adequacy_years']}" + ) + + solveyears = reeds.parse.parse_yearset(sw['yearset']) + if int(sw['endyear']) not in solveyears: + err = f"`endyear` = {sw['endyear']} but must be in `yearset`: {sw['yearset']}" + raise ValueError(err) + + # Add a row for each county + ## TEMPORARY 20260402 until the aggregation procedure is updated + county2zone = reeds.io.get_county2zone(GSw_ZoneSet='z134', as_map=False) + county2zone['county'] = 'p' + county2zone.FIPS + # Add county info to hierarchy + hierarchy = hierarchy.merge(county2zone.drop(columns=['FIPS','state']), on='r') + + # Make sure specified regions are allowed for the specified hierarchy level + region_groups = sw['GSw_Region'].split('//') if '//' in sw['GSw_Region'] else [sw['GSw_Region']] + for group in region_groups: + level, regions = group.split('/') + if level not in hierarchy: + err = ( + f"The specified hierarchy level '{level}' does not exist in the hierarchy file." + f"\nUpdate GSw_Region={sw['GSw_Region']} to specify a valid level." + ) + raise ValueError(err) + invalid_regions = [ + region for region in regions.split('.') + if region.lower() not in hierarchy[level].str.lower().values + ] + if invalid_regions: + err = f"GSw_Region: {', '.join(invalid_regions)} need to be in {hierarchy[level].unique()}" + raise Exception(err) + + ### Compatible switch combinations + if sw['GSw_LoadProfiles'] == 'historic': + if ('demand_' + sw['demandscen'] +'.csv') not in os.listdir(os.path.join(reeds_path, 'inputs','load')) : + raise ValueError("The demand file specified by the demandscen switch is not in the inputs/load folder") + + if ( + re.match(r'(\/|[a-zA-Z]:[\\\/]).+$', sw['GSw_LoadProfiles']) + and not Path(sw['GSw_LoadProfiles']).is_file() + ): + err = f"GSw_LoadProfiles={sw['GSw_LoadProfiles']} but the specified file does not exist" + raise FileNotFoundError(err) + + ### Dependent model availability + if ( + ((int(sw['pras']) == 2) or int(sw['GSw_PRM_StressIterateMax'])) + and (not os.path.isfile(os.path.join(reeds_path, 'Manifest.toml'))) + ): + err = ( + "Manifest.toml does not exist. " + "Please set up julia by following the instructions at " + "https://natlabrockies.github.io/ReEDS-2.0/setup.html#reeds2pras-julia-and-stress-periods-setup" + ) + raise Exception(err) + + ### Land use and reeds_to_rev + if (int(sw['land_use_analysis'])) and (not int(sw['reeds_to_rev'])): + raise ValueError( + "'reeds_to_rev' must be enable for land_use analysis to run." + ) + + disallowed_characters = ['~', '|', '*'] + invalid_switches = [ + key for key, val in sw.items() + if any([char in val for char in disallowed_characters]) + ] + if len(invalid_switches) > 0: + raise ValueError( + "The following switches have values with disallowed characters " + f"({', '.join(disallowed_characters)}): {', '.join(invalid_switches)}" + ) + + ### Contents of user-specified files + reeds.checks.check_switches(sw) + + ### Uncommonly used packages + if sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): + if importlib.util.find_spec("sklearn_extra") is None: + err = ( + "The scikit-learn-extra package is required for GSw_HourlyClusterAlgorithm=" + f"{sw['GSw_HourlyClusterAlgorithm']} but is not available in your conda " + "environment. Please install it by running:\n" + " pip install 'scikit-learn-extra>=0.2.0,<0.3.0'" + "\nor:\n" + " conda install -c conda-forge scikit-learn-extra=0.2" + ) + raise ModuleNotFoundError(err) + + +def setup_sequential_year( + cur_year, prev_year, next_year, + caseSwitches, hpc, + solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, + ): + ## Get save file (for this year) and restart file (from previous year) + savefile = f"{batch_case}_{cur_year}i0" + restartfile = batch_case if cur_year == min(solveyears) else f"{batch_case}_{prev_year}i0" + + ## Run the ReEDS LP + if (cur_year >= min(solveyears)): + ## solve one year + OPATH.writelines( + reeds.parse.solvestring_sequential( + batch_case, caseSwitches, + cur_year, next_year, prev_year, restartfile, + toLogGamsString, hpc, + )) + OPATH.writelines(writescripterrorcheck(f"d_solveoneyear.gms_{cur_year}")) + OPATH.writelines(f'python {logger} --year={cur_year}\n') + + if int(caseSwitches['GSw_ValStr']): + OPATH.writelines("python valuestreams.py" + '\n') + + ## check to see if the restart file exists + OPATH.writelines(writeerrorcheck(os.path.join("g00files", savefile + ".g*"))) + + ## Run Augur if it not the final solve year and if not skipping Augur + if (( + (cur_year < max(solveyears)) + and (next_year > int(caseSwitches['GSw_SkipAugurYear'])) + ) or (cur_year == max(solveyears))): + OPATH.writelines( + f"\npython Augur.py {next_year} {cur_year} {casedir}\n") + ## Check to make sure Augur ran successfully; quit otherwise + OPATH.writelines( + writeerrorcheck(os.path.join( + "ReEDS_Augur", "augur_data", f"ReEDS_Augur_{cur_year}.gdx"))) + + ## delete the previous restart file unless we're keeping them + if (cur_year > min(solveyears)) and (not int(caseSwitches['keep_g00_files'])): + write_delete_file( + checkfile=os.path.join("g00files", savefile + ".g00"), + deletefile=os.path.join("g00files", restartfile + '.g00'), + PATH=OPATH, + ) + + +def setup_sequential( + caseSwitches, reeds_path, hpc, + solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, + ): + ### loop over solve years + for i in range(len(solveyears)): + ## current year is the value in solveyears + cur_year = solveyears[i] + + if cur_year < max(solveyears): + ## next year becomes the next item in the solveyears vector + next_year = solveyears[i+1] + ## Get previous year if after first year + if i: + prev_year = solveyears[i-1] + else: + prev_year = solveyears[i] + + ### make an indicator in the batch file for what year is being solved + big_comment(f'Year: {cur_year}', OPATH) + + ### Write the tax credit phaseout call + OPATH.writelines(f"python {Path('reeds','core','solve','1_tc_phaseout.py')} {cur_year} {casedir}\n\n") + + ### Write the GAMS LP and Augur calls + if int(caseSwitches['GSw_PRM_StressIterateMax']): + OPATH.writelines( + f"python {Path('reeds','core','solve','solve.py')} {casedir} {cur_year}\n" + ) + OPATH.writelines(writescripterrorcheck(f"solve.py_{cur_year}")) + else: + setup_sequential_year( + cur_year, prev_year, next_year, + caseSwitches, hpc, + solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, + ) + + if int(caseSwitches['GSw_CheckInputs']): + ### Run input parameter error checks after the first solve year (since financial + ### multipliers aren't created until the first solve year is run) + if cur_year == min(solveyears): + OPATH.writelines( + f"\npython {os.path.join(casedir, 'reeds', 'inputs', 'check_inputs.py')} " + f"{casedir}\n" + ) + OPATH.writelines(writescripterrorcheck('check_inputs.py')+'\n') + + ### Run Augur plots in background + OPATH.writelines( + f"python {Path('reeds','resource_adequacy','diagnostic_plots.py')} " + f"--reeds_path={reeds_path} --casedir={casedir} --t={cur_year} &\n") + + +def setup_intertemporal( + caseSwitches, startiter, niter, ccworkers, + solveyears, endyear, batch_case, toLogGamsString, yearset_augur, OPATH, + ): + ### beginning year is passed to augurbatch + begyear = min(solveyears) + ### first save file from d_solveprep is just the case name + savefile = batch_case + ### if this is the first iteration + if startiter == 0: + ## restart file becomes the previous calls save file + restartfile = savefile + ## if this is not the first iteration... + if startiter > 0: + ## restart file is now the case name plus the iteration number + restartfile = batch_case + "_" + startiter + + ### per the instructions, iterations are + ### the number of iterations after the first solve + niter = niter+1 + + ### for the number of iterations we have... + for i in range(startiter,niter): + ## make an indicator in the batch file for what iteration is being solved + big_comment(f'Iteration: {i}', OPATH) + ## call the intertemporal solve + savefile = batch_case + "_" + str(i) + + if i==0: + ## check to see if the restart file exists + ## only need to do this with the zeroth iteration + ## as the other checks will all be after the solves + OPATH.writelines(writeerrorcheck(os.path.join("g00files", restartfile + ".g*"))) + + OPATH.writelines( + f"gams {Path('reeds','core','d_solveallyears.gms')} o=" + +os.path.join("lstfiles",batch_case + "_" + str(i) + ".lst") + +" r="+os.path.join("g00files", restartfile) + + " gdxcompress=1 xs="+os.path.join("g00files", savefile) + toLogGamsString + + " --niter=" + str(i) + " --case=" + batch_case + ' \n') + + ## check to see if the save file exists + OPATH.writelines(writeerrorcheck(os.path.join("g00files",savefile + ".g*"))) + + ## start threads for cc/curt + ## no need to run cc curt scripts for final iteration + if i < niter-1: + ## batch out calls to augurbatch + OPATH.writelines( + "python augurbatch.py " + batch_case + " " + str(ccworkers) + " " + + yearset_augur + " " + savefile + " " + str(begyear) + " " + + str(endyear) + " " + caseSwitches['distpvscen'] + " " + + str(caseSwitches['calc_csp_cc']) + " " + + str(caseSwitches['timetype']) + " " + + str(caseSwitches['GSw_WaterMain']) + " " + str(i) + " " + + str(caseSwitches['marg_vre_mw']) + " " + + str(caseSwitches['marg_stor_mw']) + " " + + str(caseSwitches['marg_evmc_mw']) + " " + + '\n') + ## merge all the resulting gdx files + ## the output file will be for the next iteration + nextiter = i+1 + gdxmergedfile = os.path.join( + "ReEDS_Augur","augur_data","ReEDS_Augur_merged_" + str(nextiter)) + OPATH.writelines( + "gdxmerge "+os.path.join("ReEDS_Augur","augur_data","ReEDS_Augur*") + + " output=" + gdxmergedfile + ' \n') + ## check to make sure previous calls were successful + OPATH.writelines(writeerrorcheck(gdxmergedfile+".gdx")) + + ## restart file becomes the previous save file + restartfile = savefile + + if caseSwitches['GSw_ValStr'] != '0': + OPATH.writelines( "python valuestreams.py" + '\n') + + +def setup_window( + caseSwitches, startiter, niter, ccworkers, reeds_path, + batch_case, toLogGamsString, yearset_augur, OPATH, + ): + ### load the windows + win_in = list(csv.reader(open( + os.path.join( + reeds_path,"inputs","userinput", + "windows_{}.csv".format(caseSwitches['windows_suffix'])), + 'r'), delimiter=",")) + + restartfile = batch_case + + ### for windows indicated in the csv file + for win in win_in[1:]: + + ## beginning year is the first column (start) + begyear = win[1] + ## end year is the second column (end) + endyear = win[2] + ## for the number of iterations we have... + for i in range(startiter,niter): + big_comment(f'Window: {win}', OPATH) + comment(f'Iteration: {i}', OPATH) + + ## call the window solve + savefile = batch_case+"_"+str(i) + ## check to see if the save file exists + OPATH.writelines(writeerrorcheck(os.path.join("g00files", restartfile + ".g*"))) + ## solve via the window solve file + OPATH.writelines( + f"gams {Path('reeds','core','d_solvewindow.gms')} o=" + + os.path.join("lstfiles", batch_case + "_" + str(i) + ".lst") + +" r=" + os.path.join("g00files", restartfile) + + " gdxcompress=1 xs=g00files\\"+savefile + toLogGamsString + " --niter=" + str(i) + + " --maxiter=" + str(niter-1) + " --case=" + batch_case + " --window=" + win[0] + ' \n') + ## start threads for cc/curt + OPATH.writelines(writeerrorcheck(os.path.join("g00files",savefile + ".g*"))) + OPATH.writelines( + "python augurbatch.py " + batch_case + " " + str(ccworkers) + " " + + yearset_augur + " " + savefile + " " + str(begyear) + " " + + str(endyear) + " " + caseSwitches['distpvscen'] + " " + + str(caseSwitches['calc_csp_cc']) + " " + + str(caseSwitches['timetype']) + " " + + str(caseSwitches['GSw_WaterMain']) + " " + str(i) + " " + + str(caseSwitches['marg_vre_mw']) + " " + + str(caseSwitches['marg_stor_mw']) + " " + + str(caseSwitches['marg_evmc_mw']) + " " + + '\n') + ## merge all the resulting r2_in gdx files + ## the output file will be for the next iteration + nextiter = i+1 + ## create names for then merge the curt and cc gdx files + gdxmergedfile = os.path.join( + "ReEDS_Augur","augur_data","ReEDS_Augur_merged_" + str(nextiter)) + OPATH.writelines( + "gdxmerge " + os.path.join("ReEDS_Augur","augur_data","ReEDS_Augur*") + + " output=" + gdxmergedfile + ' \n') + ## check to make sure previous calls were successful + OPATH.writelines(writeerrorcheck(gdxmergedfile+".gdx")) + restartfile = savefile + if caseSwitches['GSw_ValStr'] != '0': + OPATH.writelines( "python valuestreams.py" + '\n') + + +#%% =========================================================================== +### --- PROCEDURE --- +### =========================================================================== + +def setupEnvironment( + BatchName=False, cases_suffix=False, single='', simult_runs=0, + forcelocal=0, skip_checks=False, + debug=False, debugnode=False, cases_per_node=1, + dryrun=False, + ): + #%% Settings for testing + # BatchName = 'v20260307_downloadM0' + # cases_suffix = 'test' + # WORKERS = 1 + # forcelocal = 0 + # single = '' + # skip_checks = False + # debug = False + # dryrun = True + + #%% Automatic inputs + reeds_path = os.path.dirname(__file__) + + #%% User inputs + print(" ") + print("------------- ") + print(" ") + print("WINDOWS USERS - This script will open multiple command prompts, the number of which") + print("is based on the number of simultaneous runs you've chosen") + print(" ") + print("MAC/LINUX USERS - Your cases will run in the background. All console output") + print("is written to the cases' appropriate gamslog.txt file in the cases' runs folders") + print(" ") + print("------------- ") + print(" ") + print(" ") + + if not BatchName: + print("-- Specify the batch prefix --") + print(" ") + print("The batch prefix is attached to the beginning of all cases' outputs files") + print("Note - it must start with a letter and not a number or symbol") + print(" ") + print("A value of 0 will assign the date and time as the batch name (e.g. v20190520_072310)") + print(" ") + + BatchName = str(input('Batch Prefix: ')) + + if BatchName == '0': + BatchName = 'v' + time.strftime("%Y%m%d_%H%M%S") + + #check for period in batchname and replace with underscore + BatchName = BatchName.replace('.', '_') + + if not cases_suffix: + print("\n\nSpecify the suffix for the cases_suffix.csv file") + print("A blank input will default to the cases.csv file\n") + + cases_suffix = str(input('Case Suffix: ')) + + #%% Check whether to submit slurm jobs (if on HPC) or run locally + hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False + hpc = False if forcelocal else hpc + + ### If on NLR HPC but NOT submitting slurm job, ask for confirmation + if ('NREL_CLUSTER' in os.environ) and (not hpc): + print( + "It looks like you're running on the NLR HPC but the REEDS_USE_SLURM environment " + "variable is not set to 1, meaning the model will run locally rather than being " + "submitted as a slurm job. Are you sure you want to run locally?" + ) + confirm_local = str(input('Run job locally? y/[n]: ') or 'n') + if confirm_local not in ['y','Y','yes','Yes','YES']: + quit() + + #%% Check whether the ReEDS conda environment is activated + if (not skip_checks) and ( + ('reeds2' not in os.environ['CONDA_DEFAULT_ENV'].lower()) + or (not pd.__version__.startswith('2')) + ): + print( + f"Your environment is {os.environ['CONDA_DEFAULT_ENV']} and your pandas " + f"version is {pd.__version__}.\nThe default environment is 'reeds2', with\n" + "pandas version 2.x, so the python parts of ReEDS are unlikely to work.\n" + "To build the environment for the first time, run:\n" + " `conda env create -f environment.yml`\n" + "To activate the created environment, run:\n" + " `conda activate reeds2` (or `activate reeds2` on Windows)\n" + "Do you want to continue without activating the environment?" + ) + confirm_env = str(input("Continue? y/[n]: ") or 'n') + if confirm_env not in ['y','Y','yes','Yes','YES']: + quit() + + #%% Load specified case file, infer other settings from cases.csv + if cases_suffix in ['', 'default']: + cases_filename = 'cases.csv' + else: + cases_filename = f'cases_{cases_suffix}.csv' + + df_cases = reeds.parse.parse_cases( + cases_filename=cases_filename, + single=single, + skip_checks=skip_checks, + ) + ## Propagate debug setting + if debug: + df_cases.loc['debug'] = str(debug) + + caseSwitches, casenames, caseList = create_case_lists( + df_cases=df_cases, + BatchName=BatchName, + single=single, + ) + + #%% Stop now if any switches are incompatible + for sw in caseSwitches: + check_compatibility(sw) + if dryrun: + quit() + + # If no --single/-s, drop the ignored cases, otherwise leave them + if not single: + casenames = [case for case in casenames + if int(df_cases.loc['ignore',case]) != 1] + df_cases.drop( + df_cases.loc['ignore'].loc[df_cases.loc['ignore']=='1'].index, + axis=1, + inplace=True + ) + # If the "single" argument is provided, only run that case + if single: + for s in single.split(','): + if s not in df_cases: + err = ( + f'Specified single={single} but available cases are:\n' + + '\n> '.join([c for c in df_cases.columns]) + ) + raise KeyError(err) + df_cases = df_cases[single.split(',')].copy() + casenames = single.split(',') + + # Make sure the run folders don't already exist + outpaths = [os.path.join(reeds_path,'runs',f'{BatchName}_{case}') for case in casenames] + existing_outpaths = [i for i in outpaths if os.path.isdir(i)] + if len(existing_outpaths): + print( + f'The following {len(existing_outpaths)} output directories already exist:\n' + + 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n' + + '\n'.join([os.path.basename(i) for i in existing_outpaths]) + + '\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + ) + overwrite = str(input('Do you want to overwrite them? y/[n]: ') or 'n') + if overwrite.lower() in ['y', 'yes']: + for outpath in existing_outpaths: + shutil.rmtree(outpath) + else: + keep = [i for (i,c) in enumerate(outpaths) if c not in existing_outpaths] + caseList = [caseList[i] for i in keep] + casenames = [casenames[i] for i in keep] + caseSwitches = [caseSwitches[i] for i in keep] + print( + f"\nThe following {(len(keep))} output directories don't exist:\n" + + 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n' + + '\n'.join([f'{BatchName}_{c}' for c in casenames]) + + '\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + ) + skip = str(input('Do you want to run them and skip the rest? [y]/n: ') or 'y') + if skip.lower() not in ['y','yes']: + raise IsADirectoryError('\n'+'\n'.join(existing_outpaths)) + + #%% User warnings + if (df_cases.loc['cleanup_level'].astype(int) > 0).any() and not skip_checks: + print( + '\nWARNING: At least one case uses cleanup_level ≥ 1, which removes files ' + 'used by R2X.\nIf you plan to run R2X, do not proceed; set cleanup_level ' + 'to 0 in your cases file and restart the run.' + ) + confirm = str(input('\nProceed? y/[n]: ') or 'n') + if confirm.lower() not in ['y', 'yes']: + quit() + + print("{} cases being run:".format(len(caseList))) + for case in casenames: + print(case) + print(" ") + + reschoice = 0 + startiter = 0 + ccworkers = 5 + niter = 5 + #%% Set number of workers, with user input if necessary + if len(caseList)==1: + print("Only one case is to be run, therefore only one thread is needed") + WORKERS = 1 + elif simult_runs < 0 or hpc: + WORKERS = min(10, len(caseList)) + elif simult_runs > 0: + WORKERS = simult_runs + else: + WORKERS = int(input('Number of simultaneous runs [integer]: ')) + + if 'int' in df_cases.loc['timetype'].tolist() or 'win' in df_cases.loc['timetype'].tolist(): + ccworkers = int(input('Number of simultaneous CC/Curt runs [integer]: ')) + print("") + print("The number of iterations defines the number of combinations of") + print(" solving the model and computing capacity credit and curtailment") + print(" Note this does not include the initial solve") + print("") + niter = int(input('How many iterations between the model and CC/Curt scripts: ')) + + if reschoice==1: + startiter = int(input('Iteration to start from (recall it starts at zero): ')) + + if hpc and cases_per_node is None: + if df_cases.shape[1] > 1: + # On HPC with multiple cases and no cases_per_node provided + cases_per_node = int(input('Number of simultaneous runs per node [integer]: ')) + else: + # On HPC with one case only + cases_per_node = 1 + elif not hpc: + # Not on HPC + cases_per_node = None + + #%% Sync remote files + print('Syncing remote files') + # If using Monte Carlo sampling, download everything (since combinations of switches + ## not listed in cases{}.csv may be used) + if df_cases.loc['MCS_runs'].astype(int).sum(): + reeds.remote.download_remote_files() + ## Otherwise, only download the files needed for the present set of runs + else: + required_files = [ + reeds.remote.identify_required_remote_files(df_cases[case]) for case in df_cases + ] + required_files = sorted(set([i for sublist in required_files for i in sublist])) + reeds.remote.download_remote_files(required_files) + + envVar = { + 'WORKERS': WORKERS, + 'ccworkers': ccworkers, + 'casenames': casenames, + 'BatchName': BatchName, + 'caseList': caseList, + 'caseSwitches': caseSwitches, + 'reeds_path' : reeds_path, + 'niter' : niter, + 'startiter' : startiter, + 'cases_filename': cases_filename, + 'hpc': hpc, + 'debugnode': debugnode, + 'cases_per_hpc_node': cases_per_node, + } + + return envVar + + +def createmodelthreads(envVar): + + q = queue.Queue() + num_worker_threads = envVar['WORKERS'] + + def worker(): + while True: + ThreadInit = q.get() + if ThreadInit is None: + break + launch_single_case_run( + options=ThreadInit['scen'], + caseSwitches=ThreadInit['caseSwitches'], + niter=ThreadInit['niter'], + reeds_path=ThreadInit['reeds_path'], + ccworkers=ThreadInit['ccworkers'], + startiter=ThreadInit['startiter'], + BatchName=ThreadInit['BatchName'], + case=ThreadInit['casename'], + cases_filename=ThreadInit['cases_filename'], + hpc=envVar['hpc'], + debugnode=envVar['debugnode'], + ) + print(ThreadInit['batch_case'] + " has finished \n") + q.task_done() + + + threads = [] + + for i in range(num_worker_threads): + t = threading.Thread(target=worker) + t.start() + threads.append(t) + + for i in range(len(envVar['caseList'])): + q.put({ + 'scen': envVar['caseList'][i], + 'caseSwitches': envVar['caseSwitches'][i], + 'batch_case':envVar['BatchName']+'_'+envVar['casenames'][i], + 'niter':envVar['niter'], + 'reeds_path':envVar['reeds_path'], + 'ccworkers':envVar['ccworkers'], + 'startiter':envVar['startiter'], + 'BatchName':envVar['BatchName'], + 'casename':envVar['casenames'][i], + 'cases_filename':envVar['cases_filename'], + }) + + # block until all tasks are done + q.join() + + # stop workers + for i in range(num_worker_threads): + q.put(None) + + for t in threads: + t.join() + + +def write_batch_script( + options, + batch_case, + reeds_path, + casedir, + caseSwitches, + cases_filename, + hpc, + startiter, + niter, + ccworkers, + BatchName, + case, +): + inputs_case = os.path.join(casedir,"inputs_case") + + if os.path.exists(os.path.join(reeds_path, 'runs', batch_case)): + print('Caution, case ' + batch_case + ' already exists in runs \n') + + #%% Set up case-specific directory structure + os.makedirs(inputs_case, exist_ok=True) + os.makedirs(os.path.join(casedir, 'g00files'), exist_ok=True) + os.makedirs(os.path.join(casedir, 'lstfiles'), exist_ok=True) + os.makedirs(os.path.join(casedir, 'outputs', 'figures'), exist_ok=True) + os.makedirs(os.path.join(casedir, 'outputs', 'tc_phaseout_data'), exist_ok=True) + + if int(caseSwitches['diagnose']): + os.makedirs(os.path.join(casedir, 'outputs', 'model_diagnose'), exist_ok=True) + + #%% Information on reV supply curves associated with this run + shutil.copytree(os.path.join(reeds_path,'inputs','supply_curve','metadata'), + os.path.join(inputs_case,'supplycurve_metadata'), dirs_exist_ok=True) + rev_paths = pd.read_csv( + os.path.join(reeds_path,'inputs','supply_curve','rev_paths.csv') + ) + + # Separate techs with no associated switch + rev_paths_none = rev_paths.loc[rev_paths.access_switch == "none",:].copy() + rev_paths = rev_paths.loc[rev_paths.access_switch != "none",:] + + # Match possible supply curves with switches from this run + revswitches = pd.DataFrame.from_dict({s:caseSwitches[s] for s in rev_paths.access_switch.unique()}, + orient='index').reset_index().rename(columns={'index':'access_switch', 0:'access_case'}) + revswitches = revswitches.merge(rev_paths, on=['access_switch', 'access_case']) + revswitches = pd.concat([revswitches[rev_paths_none.columns.tolist()], rev_paths_none]) + + # Get bin information + bins = {"wind-ons":"numbins_windons", "wind-ofs": "numbins_windofs", "upv":"numbins_upv"} + binSwitches = pd.DataFrame.from_dict({b:caseSwitches[bins[b]] for b in bins}, + orient='index').reset_index().rename(columns={'index':'tech', 0:'bins'}) + + revswitches = revswitches.merge(binSwitches, on=['tech'], how='left') + + # format rev paths + revswitches = get_rev_paths(revswitches, caseSwitches) + + # save rev paths file for run + revswitches[['tech','access_switch','access_case','rev_case','bins','sc_path', + 'sc_file','hpc_sc_file','cf_path','original_rev_folder'] + ].to_csv(os.path.join(inputs_case,'rev_paths.csv'), index=False) + + #%% Set up the meta.csv file to track repo information and runtime + logger = os.path.join(reeds_path, 'reeds', 'log.py') + loglines = ['repo,branch,commit,tag,description\n'] + ### Get some git metadata + try: + repo = git.Repo() + try: + branch = repo.active_branch.name + tag = sorted(repo.tags, key=lambda t: t.commit.committed_datetime)[-1].name + description = repo.git.describe() + except TypeError: + branch = 'DETACHED_HEAD' + tag = '' + description = '' + text = f'{repo.git_dir},{branch},{repo.head.object.hexsha},{tag},{description}\n' + loglines.append(text) + except Exception: + ## In case the user isn't in a git repo or anything else goes wrong + loglines.append('None,None,None,None,None\n') + + with open(os.path.join(casedir,'meta.csv'),'a') as METAFILE: + ### Header for timing metadata + for line in loglines: + METAFILE.writelines(line) + METAFILE.writelines('#,#,#,#,#\n') + METAFILE.writelines('year,process,starttime,stoptime,processtime\n') + ### Also write the git metadata to gamslog.txt for debugging + with open(os.path.join(casedir,'gamslog.txt'),'a') as LOGFILE: + for line in loglines: + LOGFILE.writelines(line) + + ### Write the environment info for debugging + try: + environment = subprocess.run('conda list', capture_output=True, shell=True) + pd.Series(environment.stdout.decode().split('\n')).to_csv( + os.path.join(casedir,'lstfiles','environment.csv'), + header=False, index=False, + ) + except Exception as err: + print(err) + + ### Copy over the cases file + shutil.copy2(os.path.join(reeds_path, cases_filename), casedir) + + ### Switches with values derived from other switches + ## Get hpc setting (used in Augur) + caseSwitches['hpc'] = int(hpc) + ## Get numclass from the max value in ivt + caseSwitches['numclass'] = get_ivt_numclass( + reeds_path=reeds_path, casedir=casedir, caseSwitches=caseSwitches) + + for switchname in ['hpc', 'numclass']: + options += f" --{switchname}={caseSwitches[switchname]}" + options += f' --reeds_path={reeds_path}{os.sep} --casedir={casedir}' + + #%% Record the switches for this run + reeds.io.write_gswitches(pd.Series(caseSwitches), inputs_case) + + pd.Series(caseSwitches).to_csv( + os.path.join(inputs_case,'switches.csv'), header=False) + + solveyears = reeds.parse.parse_yearset(caseSwitches['yearset']) + + # If start year is not in solveyears, start year is added into solveyears set + startyear = int(caseSwitches['startyear']) + endyear = int(caseSwitches['endyear']) + if startyear not in solveyears: + solveyears.append(startyear) + solveyears = sorted(solveyears) + + solveyears = [y for y in solveyears if (y <= endyear and y >= startyear)] + + yearset_augur = os.path.join('inputs_case','modeledyears.csv') + toLogGamsString = ' logOption=4 logFile=gamslog.txt appendLog=1 ' + + ## Copy code folders + for dirname in ['reeds']: + shutil.copytree( + os.path.join(reeds_path, dirname), + os.path.join(casedir, dirname), + ignore=shutil.ignore_patterns('test'), + ) + + #make the augur_data folder + os.makedirs(os.path.join(casedir,'ReEDS_Augur','augur_data'), exist_ok=True) + os.makedirs(os.path.join(casedir,'ReEDS_Augur','PRAS'), exist_ok=True) + + ###### Replace files according to 'file_replacements' in cases. Ignore quotes in input text. + # << is used to separate the file that is to be replaced from the file that is used + # || is used to separate multiple replacements. + if caseSwitches['file_replacements'] != 'none': + file_replacements = caseSwitches['file_replacements'].replace('"','').replace("'","").split('||') + for file_replacement in file_replacements: + replace_arr = file_replacement.split('<<') + replaced_file = replace_arr[0].strip() + replaced_file = os.path.join(casedir, replaced_file) + if not os.path.isfile(replaced_file): + raise FileNotFoundError('FILE REPLACEMENT ERROR: "' + replaced_file + '" was not found') + used_file = replace_arr[1].strip() + if not os.path.isfile(used_file): + raise FileNotFoundError('FILE REPLACEMENT ERROR: "' + used_file + '" was not found') + if os.path.isfile(replaced_file) and os.path.isfile(used_file): + shutil.copy(used_file, replaced_file) + print('FILE REPLACEMENT SUCCESS: Replaced "' + replaced_file + '" with "' + used_file + '"') + + #%% Write the call script + with open(os.path.join(casedir, 'call_' + batch_case + ext), 'w+') as OPATH: + OPATH.writelines(f"echo 'Running {batch_case}'\n") + OPATH.writelines("cd " + casedir + '\n' + '\n' + '\n') + + if hpc: + comment('Set up nodal environment for run', OPATH) + OPATH.writelines(". $HOME/.bashrc \n") + OPATH.writelines("module purge \n") + + if os.environ.get('NREL_CLUSTER') == 'kestrel': + OPATH.writelines("source /nopt/nrel/apps/env.sh \n") + OPATH.writelines("module load anaconda3 \n") + OPATH.writelines("module use /nopt/nrel/apps/software/gams/modulefiles \n") + OPATH.writelines("module load gams \n") + else: + OPATH.writelines("module load conda \n") + OPATH.writelines("module load gams \n") + + OPATH.writelines("conda activate reeds2 \n") + OPATH.writelines('export R_LIBS_USER="$HOME/rlib" \n\n\n') + + #%% Write the input processing script calls + big_comment('Input processing', OPATH) + for s in [ + 'copy_files', + 'mcs_sampler', + 'aggregate_regions', + 'hydcf', + 'h2_storage', + 'calc_financial_inputs', + 'fuelcostprep', + 'writecapdat', + 'writesupplycurves', + 'writedrshift', + 'plantcostprep', + 'climateprep', + 'hourly_load', + 'recf', + 'forecast', + 'WriteHintage', + 'transmission', + 'outage_rates', + 'hourly_repperiods', + ]: + OPATH.writelines(f"echo {'-'*12+'-'*len(s)}\n") + OPATH.writelines(f"echo 'starting {s}.py'\n") + OPATH.writelines(f"echo {'-'*12+'-'*len(s)}\n") + OPATH.writelines( + f"python {os.path.join(casedir,'reeds','input_processing',s)}.py {reeds_path} {inputs_case}\n") + OPATH.writelines(writescripterrorcheck(s)+'\n') + + OPATH.writelines( + f"python {os.path.join(reeds_path, 'postprocessing', 'cleanup_files.py')} " + f"{casedir} --force --quiet\n" + ) + + if int(caseSwitches['input_processing_only']): + OPATH.writelines( + f"python {os.path.join(reeds_path, 'postprocessing', 'cleanup_files.py')} " + f"{casedir} --force --quiet --level {caseSwitches['cleanup_level']}\n" + ) + OPATH.writelines('\n' + ('exit' if LINUXORMAC else 'goto:eof') + '\n\n') + + big_comment('Compile model', OPATH) + + OPATH.writelines( + f"\ngams {Path('reeds','core','setup','a_createmodel.gms')} " + + "gdxcompress=1 xs="+os.path.join("g00files",batch_case) + + (' license=gamslice.txt' if hpc else '') + + " o="+os.path.join("lstfiles","1_Inputs.lst") + options + " " + toLogGamsString + '\n') + OPATH.writelines(f'python {logger}\n') + restartfile = batch_case + OPATH.writelines(writeerrorcheck(os.path.join('g00files', restartfile + '.g*'))) + + ################################ + # -- CORE MODEL SETUP -- # + ################################ + if caseSwitches['timetype'] == 'seq': + setup_sequential( + caseSwitches, reeds_path, hpc, + solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, + ) + elif caseSwitches['timetype'] == 'int': + setup_intertemporal( + caseSwitches, startiter, niter, ccworkers, + solveyears, endyear, batch_case, toLogGamsString, yearset_augur, OPATH, + ) + elif caseSwitches['timetype'] == 'win': + setup_window( + caseSwitches, startiter, niter, ccworkers, reeds_path, + batch_case, toLogGamsString, yearset_augur, OPATH, + ) + + ################################# + # -- OUTPUT PROCESSING -- # + ################################# + #create reporting files + big_comment('Output processing', OPATH) + if not LINUXORMAC: + OPATH.writelines("setlocal enabledelayedexpansion\n") + ### If not iterating, run for iteration 0 + if not int(caseSwitches['GSw_PRM_StressIterateMax']): + OPATH.writelines( + f"r={os.path.join('g00files', f'{batch_case}_{max(solveyears)}i0')}\n" + if LINUXORMAC else + f'set "r={os.path.join("g00files", f"{batch_case}_{max(solveyears)}i0")}"\n' + ) + ### Otherwise, run for the last iteration (lexicographically sorted) + else: + OPATH.writelines( + f"for r in g00files/{batch_case}_*.g00; do true; done\n" + if LINUXORMAC else + f'for %%i in (g00files\{batch_case}_*.g00) do (set "r=%%i")\n' + ) + OPATH.writelines( + f"gams {Path('reeds','core','terminus','report.gms')}" + + f" o={os.path.join('lstfiles',f'report_{batch_case}.lst')}" + + (' license=gamslice.txt' if hpc else '') + + (' r=$r' if LINUXORMAC else ' r=!r!') + + ' gdxcompress=1' + + toLogGamsString + + f"--fname={batch_case}" + + f" --GSw_calc_powfrac={caseSwitches['GSw_calc_powfrac']} \n" + ) + OPATH.writelines(writescripterrorcheck("report.gms")) + if not LINUXORMAC: + OPATH.writelines("endlocal\n") + OPATH.writelines(f'python {logger}\n') + OPATH.writelines(f"python {Path('reeds','core','terminus','report_dump.py')} {casedir} -c\n\n") + if int(caseSwitches['diagnose']): + OPATH.writelines( + "python" + + f" {os.path.join(reeds_path,'postprocessing','diagnose','diagnose_process.py')}" + + f" --casepath {casedir} \n\n" + ) + + ### Run the retail rate module + OPATH.writelines( + "python" + + f" {os.path.join(reeds_path,'postprocessing','retail_rate_module','retail_rate_calculations.py')}" + + f" {batch_case} -p\n\n" + ) + + ## Run air-quality and health damages calculation script + OPATH.writelines( + "python " + f"{os.path.join(reeds_path,'postprocessing','air_quality','health_damage_calculations.py')} " + f"{casedir}\n\n" + ) + + ### Make script to unload all data to .gdx file + command = ( + 'gams dump_alldata.gms' + + ' o='+os.path.join('lstfiles','dump_alldata_{}_{}.lst'.format(BatchName,case)) + ) + command_write = ( + command + + ' r='+os.path.join('g00files','{}_{}_{}i0'.format(BatchName,case,solveyears[-1])) + ) + with open(os.path.join(casedir,'dump_alldata' + ext), 'w+') as datadumper: + datadumper.writelines('cd ' + os.path.join(reeds_path,'runs','{}_{}'.format(BatchName,case)) + '\n') + for line in [ + f"By default, this script dumps data for the first iteration of {solveyears[-1]}.", + "If more iterations were needed, increase the number at the end of the", + f"next line after 'i' (e.g. {solveyears[-1]}i0 -> {solveyears[-1]}i1)", + ]: + comment(line, datadumper) + datadumper.writelines(command_write) + if int(caseSwitches['dump_alldata']) or int(caseSwitches['debug']): + OPATH.writelines(command + (' r=$r' if LINUXORMAC else ' r=!r!') + '\n') + + ## ReEDS_to_rev processing + if caseSwitches['reeds_to_rev'] == '1': + OPATH.writelines('cd {} \n\n'.format(reeds_path)) + OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "priority" ' + '-t "wind-ons" -l "gamslog.txt" -r\n') + OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "priority" ' + '-t "wind-ofs" -l "gamslog.txt" -r\n') + OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "simultaneous" ' + '-t "upv" -l "gamslog.txt" -r\n\n') + OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "simultaneous" ' + '-t "geohydro_allkm" -l "gamslog.txt" -r\n\n') + OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "simultaneous" ' + '-t "egs_allkm" -l "gamslog.txt" -r\n\n') + + if caseSwitches['land_use_analysis'] == '1': + # Run the land-used characterization module + OPATH.writelines( + f"python {os.path.join(reeds_path,'postprocessing','land_use','land_use_analysis.py')} {casedir}\n\n" + ) + + ## Run Bokeh + bokehdir = os.path.join(reeds_path,"postprocessing","bokehpivot","reports") + OPATH.writelines( + 'python ' + os.path.join(bokehdir,"interface_report_model.py") + ' "ReEDS 2.0" ' + + os.path.join(reeds_path,"runs",batch_case) + " all No none " + + os.path.join(bokehdir,"templates","reeds2","standard_report_reduced.py") + ' "html,excel,csv" one ' + + os.path.join(reeds_path,"runs",batch_case,"outputs","reeds-report-reduced") + ' No\n') + OPATH.writelines( + 'python ' + os.path.join(bokehdir,"interface_report_model.py") + ' "ReEDS 2.0" ' + + os.path.join(reeds_path,"runs",batch_case) + " all No none " + + os.path.join(bokehdir,"templates","reeds2","standard_report_expanded.py") + ' "html,excel" one ' + + os.path.join(reeds_path,"runs",batch_case,"outputs","reeds-report") + ' No\n') + OPATH.writelines( + 'python ' + os.path.join(bokehdir,"interface_report_model.py") + ' "ReEDS 2.0" ' + + os.path.join(reeds_path,"runs",batch_case) + " all No none " + + os.path.join(bokehdir,"templates","reeds2","state_report.py") + ' "csv" one ' + + os.path.join(reeds_path,"runs",batch_case,"outputs","reeds-report-state") + ' No\n\n') + OPATH.writelines('python postprocessing/vizit/vizit_prep.py ' + '"{}"'.format(os.path.join(casedir,'outputs')) + '\n\n') + + OPATH.writelines( + f'python postprocessing/single_case_plots.py {casedir} --year {solveyears[-1]}\n\n' + ) + + ### Remove unnecessary files from case folder + OPATH.writelines( + f"python {os.path.join(reeds_path, 'postprocessing', 'cleanup_files.py')} " + f"{casedir} --force --quiet --level {caseSwitches['cleanup_level']}\n" + ) + + ### Run R2X if using debug mode + ### First install uvx: https://docs.astral.sh/uv/getting-started/installation/ + if int(caseSwitches['debug']): + r2xpath = os.path.join(casedir, 'outputs', 'r2x') + os.makedirs(r2xpath, exist_ok=True) + OPATH.writelines( + "uvx --python 3.11 --from git+https://github.com/nrel/r2x@main r2x -vv run " + f"-i {casedir} " + f"-o {r2xpath} " + "--input-model=reeds-US " + "--output-model=plexos " + f"--year={endyear} " + f"--weather-year={caseSwitches['GSw_HourlyWeatherYears'].split('_')[0]} " + "\n" + ) + + ### Check the error level + pipe = '2>&1 | tee -a' if LINUXORMAC else '>>' + tolog = f"{pipe} {os.path.join(casedir,'gamslog.txt')}" + OPATH.writelines(f"\npython postprocessing/check_error.py {casedir} {tolog}\n") + + ### Run dispatch mode if desired + if int(caseSwitches['pcm']): + OPATH.writelines(f'\npython run_pcm.py {casedir} -b\n\n') + + +def submit_slurm_parallel_jobs( + reeds_path, BatchName, casenames, cases_per_node, debugnode = False, +): + """ + Write and submit Slurm parallel run scripts for each group of cases in the batch. + """ + num_cases = len(casenames) + num_nodes = int(np.ceil(num_cases / cases_per_node)) + + batch_folder = os.path.join( + reeds_path, 'slurm_parallel_runs', + f"{BatchName}-{datetime.now().strftime('%Y%m%dT%H%M%S')}" + ) + if os.path.isdir(batch_folder): + shutil.rmtree(batch_folder) + os.makedirs(batch_folder) + + for node_index in range(num_nodes): + start_case_index = cases_per_node * node_index + stop_case_index = min(start_case_index + cases_per_node, num_cases) + casenames_print = casenames[start_case_index:stop_case_index] + run_script_fpath = os.path.join(batch_folder, f"run_{'-'.join(casenames_print)}.sh") + shutil.copy("srun_template.sh", run_script_fpath) + job_name = f"{BatchName}_({','.join(casenames_print)})" + + writelines = [] + with open(run_script_fpath, 'r') as SPATH: + for line in SPATH: + stripped = line.strip() + if debugnode and ('--time' in stripped or '--partition' in stripped): + continue + elif '--ntasks-per-node' in stripped: + writelines.append('# ' + stripped) + else: + writelines.append(stripped) + + with open(run_script_fpath, 'w') as SPATH: + for line in writelines: + SPATH.write(line + '\n') + + if debugnode: + SPATH.write("#SBATCH --time=04:00:00\n") + SPATH.write("#SBATCH --partition=debug\n") + + SPATH.write(f"#SBATCH --ntasks-per-node={cases_per_node}\n") + SPATH.write(f"#SBATCH --job-name={job_name}\n") + SPATH.write(f"#SBATCH --output={os.path.join(batch_folder, 'slurm-%j.out')}\n\n") + SPATH.write(". $HOME/.bashrc\n\n") + + for idx, case_index in enumerate(range(start_case_index, stop_case_index)): + casename = casenames[case_index] + batch_case = f"{BatchName}_{casename}" + casedir = os.path.join(reeds_path, 'runs', batch_case) + + bash_path = os.path.join(casedir, 'call_' + batch_case + ext) + task_out = os.path.join(casedir, f'slurm-${{SLURM_JOB_ID}}_{idx}.out') + resource_stats = os.path.join(casedir, 'resource_stats.log') + srun_line = ( + f"srun --ntasks=1 --overlap " + f"/usr/bin/time -a -o {resource_stats} " + f"-f 'memory_KB=%M, runtime=%E' " + f"bash {bash_path} 2>&1 | tee {task_out} &" + ) + SPATH.write(srun_line + '\n') + + # Also write the Slurm script for a single run + # just in case you need to restart a single case in the future + write_case_submission_script( + casedir, batch_case, debugnode=debugnode, + ) + + SPATH.write("wait\n") + + # Submit the job script via Slurm + try: + subprocess.Popen(["sbatch", run_script_fpath]) + except Exception as e: + raise RuntimeError(f"Failed to submit job script: {run_script_fpath}\nError: {e}") + + +def write_case_submission_script( + casedir, batch_case, debugnode = False, +): + """ + Writes a SLURM submission script in the specified case directory. + + This script (named based on the batch_case) includes SLURM resource + allocation directives and is responsible for launching the actual + ReEDS execution script (e.g., run logic). + """ + # Create a copy of the SLURM template + slurm_script_path = os.path.join(casedir, batch_case + ".sh") + shutil.copy("srun_template.sh", slurm_script_path) + + # If using debug node, comment out time and replace with short time + if debugnode: + writelines = [] + with open(slurm_script_path, 'r') as SPATH: + for line in SPATH: + writelines.append(('# ' if '--time' in line else '') + line.strip()) + with open(slurm_script_path, 'w') as SPATH: + for line in writelines: + SPATH.writelines(line + '\n') + SPATH.writelines("#SBATCH --time=01:00:00\n") + SPATH.writelines("#SBATCH --partition=debug\n") + + # Append additional settings to launch the actual run script + with open(slurm_script_path, 'a') as SPATH: + SPATH.writelines(f"#SBATCH --job-name={batch_case}\n") + SPATH.writelines(f"#SBATCH --output={os.path.join(casedir, 'slurm-%j.out')}\n\n") + SPATH.writelines("#load your default settings\n") + SPATH.writelines(". $HOME/.bashrc\n\n") + SPATH.writelines(f"sh {os.path.join(casedir, 'call_' + batch_case + ext)}\n") + + SPATH.close() + + +def generate_parallel_cases_batch_scripts(envVar): + """ + Generate batch case with ReEDS specific instructions for each case + """ + for i, case in enumerate(envVar['caseList']): + casename = envVar['casenames'][i] + batch_case = f"{envVar['BatchName']}_{casename}" + casedir = os.path.join(envVar['reeds_path'], 'runs', batch_case) + + write_batch_script( + case, + batch_case, + envVar['reeds_path'], + casedir, + envVar['caseSwitches'][i], + envVar['cases_filename'], + envVar['hpc'], + envVar['startiter'], + envVar['niter'], + envVar['ccworkers'], + envVar['BatchName'], + casename, + ) + + now = datetime.isoformat(datetime.now()) + try: + with open(os.path.join(casedir, 'meta.csv'), 'a') as METAFILE: + METAFILE.write(f'0,end,,{now},\n') + except Exception as e: + print(f"[Warning] meta.csv not found or not writeable for {casename}: {e}") + + +def launch_single_case_run( + options, caseSwitches, niter, reeds_path, ccworkers, startiter, + BatchName, case, cases_filename, hpc=False, debugnode=False +): + + ### For testing/debugging + # caseSwitches = caseSwitches[0] + # options = caseList[0] + ### Inferred inputs + batch_case = f'{BatchName}_{case}' + casedir = os.path.join(reeds_path,'runs',batch_case) + + write_batch_script( + options, + batch_case, + reeds_path, + casedir, + caseSwitches, + cases_filename, + hpc, + startiter, + niter, + ccworkers, + BatchName, + case, + ) + + ### ===================================================================================== + ### --- CALL THE CREATED BATCH FILE --- + ### ===================================================================================== + # If you're not running on eagle or AWS... + if (not hpc) & (not int(caseSwitches['AWS'])): + # Start the command prompt similar to the sequential solve + # - waiting for it to finish before starting a new thread + if LINUXORMAC: + print("Starting the run for case " + batch_case) + # Give execution rights to the shell script + os.chmod(os.path.join(casedir, 'call_' + batch_case + ext), 0o777) + # Open it up - note the in/out/err will be written to the shellscript parameter + shellscript = subprocess.Popen( + [os.path.join(casedir, 'call_' + batch_case + ext)], shell=True) + # Wait for it to finish before killing the thread + shellscript.wait() + else: + if int(caseSwitches['keep_run_terminal']) == 1: + terminal_keep_flag = ' /k ' + else: + terminal_keep_flag = ' /c ' + os.system('start /wait cmd' + terminal_keep_flag + os.path.join(casedir, 'call_' + batch_case + ext)) + + elif hpc: + write_case_submission_script( + casedir, batch_case, debugnode=debugnode, + ) + + batchcom = "sbatch " + os.path.join(casedir, batch_case + ".sh") + subprocess.Popen(batchcom.split()) + + elif int(caseSwitches['AWS']): + print("Starting the run for case " + batch_case) + # Give execution rights to the shell script + os.chmod(os.path.join(casedir, 'call_' + batch_case + ext), 0o777) + # Issue a nohup (no hangup) command and direct output to + # case-specific txt files in the root of the repository + shellscript = subprocess.Popen( + ['nohup ' + os.path.join(casedir, 'call_' + batch_case + ext) + " > " +os.path.join(casedir,batch_case+ ".txt") ], + stdin=open(os.path.join(casedir,batch_case+"_in.txt"),'w'), + stdout=open(os.path.join(casedir,batch_case+"_out.txt"),'w'), + stderr=open(os.path.join(casedir,batch_case+"_err.log"),'w'), + shell=True,preexec_fn=os.setpgrp) + # Wait for it to finish before killing the thread + shellscript.wait() + + ### Record the ending time + now = datetime.isoformat(datetime.now()) + try: + with open(os.path.join(casedir,'meta.csv'), 'a') as METAFILE: + METAFILE.writelines('0,end,,{},\n'.format(now)) + except Exception as e: + print(f"[Warning] meta.csv not found or not writeable for {batch_case}: {e}") + + +def main( + BatchName='', cases_suffix='', single='', simult_runs=0, + forcelocal=False, skip_checks=False, + debug=False, debugnode=False, cases_per_node=1, + dryrun=False, + ): + """ + Executes parallel solves based on cases in 'cases.csv' + """ + print(" ") + print(" ") + print("---------------------------------------------------------------------------------------------------------------------") + print(" ") + print(" +++++++++++++++ ") + print(" +++++++++++++++++++++++ ") + print("=+++++++++++++++++++++++++ ") + print("+++++++++++++++++++++++++++ ############ ########### ############# ######### ") + print("++++++++++++++ +++++++++++++ ############## ## ### #### ### ") + print("++++++++++++ ++++++++++++++ #### #### ## ### ### ## ") + print("+++++++++++ ++++++++++++++ #### ##### ######### ## ### ### ## ") + print("+++++++++ ++++++++ #### ##### ############# ## ### ## #### ") + print("++++++++ ++++++++++ ############# #### ##### ########### ### ## ###### ") + print("+++++++++++++ +++++++++++ ############ ############### ## ### ## #### ") + print("++++++++++++ +++++++++++ #### ##### ############### ## ### ### ### ") + print("+++++++++++ +++++++++++ #### ##### #### ## ### ### ### ") + print("++++++++++ +++++++++++++ #### #### #### ## ### ### ### ") + print("+++++++++ +++++++++++++ #### ##### ############# ## ### ##### ### ### ") + print("++++++++ +++++++++++++ #### ##### ########### ############ ########### ######### ") + print("++++++++ +++++++++++ ") + print(" +++++ ++++++++++ ") + print(" ++ ++++ ") + print(" ") + print("---------------------------------------------------------------------------------------------------------------------") + print(" ") + print(" ") + + # Gather user inputs before calling GAMS programs + envVar = setupEnvironment( + BatchName=BatchName, cases_suffix=cases_suffix, + single=single, simult_runs=simult_runs, + forcelocal=forcelocal, skip_checks=skip_checks, + debug=debug, debugnode=debugnode, cases_per_node=cases_per_node, + dryrun=dryrun, + ) + + if (envVar['hpc']) and (envVar['cases_per_hpc_node'] > 1): + # Write each .sh script for each case individually + generate_parallel_cases_batch_scripts(envVar) + + # Write the slurm scripts for parallel runs and + # submit them to the HPC + submit_slurm_parallel_jobs( + reeds_path=envVar['reeds_path'], + BatchName=envVar['BatchName'], + casenames=envVar['casenames'], + cases_per_node=envVar['cases_per_hpc_node'], + debugnode=envVar['debugnode'], + ) + + else: + # Threads are created which will handle each case individually + createmodelthreads(envVar) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--BatchName', '-b', type=str, default='', + help='Name for batch of runs') + parser.add_argument('--cases_suffix', '-c', type=str, default='', + help='Suffix for cases CSV file') + parser.add_argument('--single', '-s', type=str, default='', + help='Name of a single case to run (or comma-delimited list)') + parser.add_argument('--simult_runs', '-r', type=int, default=0, + help='Number of simultaneous runs. If negative, run all simultaneously.') + parser.add_argument('--forcelocal', '-l', action='store_true', + help='Force model to run locally instead of submitting a slurm job') + parser.add_argument('--skip_checks', '-f', action="store_true", + help="Force run, skipping checks on conda environment and switches") + parser.add_argument('--debug', '-d', action='count', default=0, + help="Run in debug mode (same behavior as debug switch in cases.csv)") + parser.add_argument('--debugnode', '-n', action="store_true", + help="Run using debug specifications for slurm on an hpc system") + parser.add_argument('--cases_per_node', '-p', type=int, default=None, + help="Number of ReEDS cases to run concurrently on a single HPC node. " + "If not provided, the user will be prompted to specify it.") + parser.add_argument('--dryrun', '-t', action='store_true', + help="Check inputs but don't start runs") + + args = parser.parse_args() + + main( + BatchName=args.BatchName, cases_suffix=args.cases_suffix, single=args.single, + simult_runs=args.simult_runs, forcelocal=args.forcelocal, skip_checks=args.skip_checks, + debug=args.debug, debugnode=args.debugnode, cases_per_node=args.cases_per_node, + dryrun=args.dryrun, + ) diff --git a/tests/objective_function_params.yaml b/tests/objective_function_params.yaml index 1764036c..ce31971b 100644 --- a/tests/objective_function_params.yaml +++ b/tests/objective_function_params.yaml @@ -1,6 +1,6 @@ ### Notes # This file holds parameters used in the objective function. These parameters -# are checked using input_processing/check_inputs.py, which will identify missing +# are checked using reeds/inputs/check_inputs.py, which will identify missing # values in the parameters if there are any. # Instructions: From 41c42969ef4a5541beab4478add96c97e816e63a Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:42:21 -0600 Subject: [PATCH 02/34] add inadvertently dropped climate heuristics inputs --- .gitignore | 4 -- .../climate/climate_heuristics_finalyear.csv | 4 ++ .../climate/climate_heuristics_yearfrac.csv | 42 +++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 inputs/climate/climate_heuristics_finalyear.csv create mode 100644 inputs/climate/climate_heuristics_yearfrac.csv diff --git a/.gitignore b/.gitignore index 5853a887..67d42ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -50,10 +50,6 @@ inputs/profiles_temperature inputs/remote inputs/shapefiles/cache -# Old climate scenarios (temporarily ignore, remove this section once ESPRS data is available) -inputs/climate/ -!inputs/climate/climate_heuristics_*.csv - # IDE stuff *.vscode diff --git a/inputs/climate/climate_heuristics_finalyear.csv b/inputs/climate/climate_heuristics_finalyear.csv new file mode 100644 index 00000000..334415c4 --- /dev/null +++ b/inputs/climate/climate_heuristics_finalyear.csv @@ -0,0 +1,4 @@ +*parameter,none,2025_2050_linear +hydro_capcredit_delta,0,-0.2 +thermal_summer_cap_delta,0,-0.15 +trans_summer_cap_delta,0,-0.05 \ No newline at end of file diff --git a/inputs/climate/climate_heuristics_yearfrac.csv b/inputs/climate/climate_heuristics_yearfrac.csv new file mode 100644 index 00000000..c833fbdf --- /dev/null +++ b/inputs/climate/climate_heuristics_yearfrac.csv @@ -0,0 +1,42 @@ +*t,none,2025_2050_linear +2010,0,0 +2011,0,0 +2012,0,0 +2013,0,0 +2014,0,0 +2015,0,0 +2016,0,0 +2017,0,0 +2018,0,0 +2019,0,0 +2020,0,0 +2021,0,0 +2022,0,0 +2023,0,0 +2024,0,0 +2025,0,0 +2026,0,0.04 +2027,0,0.08 +2028,0,0.12 +2029,0,0.16 +2030,0,0.2 +2031,0,0.24 +2032,0,0.28 +2033,0,0.32 +2034,0,0.36 +2035,0,0.4 +2036,0,0.44 +2037,0,0.48 +2038,0,0.52 +2039,0,0.56 +2040,0,0.6 +2041,0,0.64 +2042,0,0.68 +2043,0,0.72 +2044,0,0.76 +2045,0,0.8 +2046,0,0.84 +2047,0,0.88 +2048,0,0.92 +2049,0,0.96 +2050,0,1 \ No newline at end of file From 2bb1cc7a8232c8fdd45f7a533f4b65303776a4aa Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:58:01 -0600 Subject: [PATCH 03/34] delete files that have been renamed --- Augur.py | 228 - ReEDS_Augur/augur_switches.csv | 16 - ReEDS_Augur/capacity_credit.py | 862 --- ReEDS_Augur/diagnostic_plots.py | 1358 ---- ReEDS_Augur/prep_data.py | 588 -- ReEDS_Augur/run_pras.jl | 486 -- ReEDS_Augur/stress_periods.py | 615 -- aws_setup.sh | 135 - b_inputs.gms | 6774 ------------------- c_mga.gms | 77 - c_supplymodel.gms | 3976 ----------- c_supplyobjective.gms | 369 - cbc.opt | 3 - cplex.op2 | 71 - cplex.opt | 71 - d1_financials.gms | 156 - d1_temporal_params.gms | 910 --- d2_post_solve_adjustments.gms | 131 - d2_unfix_op.gms | 115 - d2_varfix.gms | 125 - d3_data_dump.gms | 424 -- d_solve_iterate.py | 179 - d_solveallyears.gms | 165 - d_solveoneyear.gms | 290 - d_solvepcm.gms | 25 - d_solveprep.gms | 254 - d_solvewindow.gms | 154 - dump_alldata.gms | 4 - e_powfrac_calc.gms | 126 - e_report.gms | 2091 ------ e_report_dump.py | 311 - e_report_params.csv | 276 - gurobi.opt | 4 - input_processing/WriteHintage.py | 774 --- input_processing/__init__.py | 0 input_processing/aggregate_regions.py | 1182 ---- input_processing/calc_financial_inputs.py | 526 -- input_processing/check_inputs.py | 155 - input_processing/climateprep.py | 222 - input_processing/copy_files.py | 1682 ----- input_processing/forecast.py | 525 -- input_processing/fuelcostprep.py | 175 - input_processing/h2_storage.py | 141 - input_processing/hourly_load.py | 783 --- input_processing/hourly_plots.py | 719 -- input_processing/hourly_repperiods.py | 976 --- input_processing/hourly_writetimeseries.py | 1583 ----- input_processing/hydcf.py | 469 -- input_processing/mcs_sampler.py | 1648 ----- input_processing/outage_rates.py | 506 -- input_processing/plantcostprep.py | 504 -- input_processing/recf.py | 508 -- input_processing/transmission.py | 641 -- input_processing/writecapdat.py | 896 --- input_processing/writedrshift.py | 86 - input_processing/writesupplycurves.py | 1138 ---- interim_report.py | 60 - interim_report_batch.py | 70 - pyproject.toml | 20 - raw_value_streams.py | 253 - reeds/inputs.py | 784 --- reeds/ra.py | 125 - reeds2pras/Project.toml | 26 - reeds2pras/R2P_Test_Summary.png | 3 - reeds2pras/README.md | 106 - reeds2pras/src/ReEDS2PRAS.jl | 40 - reeds2pras/src/main/create_pras_system.jl | 206 - reeds2pras/src/main/parse_reeds_data.jl | 133 - reeds2pras/src/models/Battery.jl | 130 - reeds2pras/src/models/Gen_Storage.jl | 166 - reeds2pras/src/models/Generator.jl | 46 - reeds2pras/src/models/Interface.jl | 150 - reeds2pras/src/models/Line.jl | 155 - reeds2pras/src/models/Region.jl | 45 - reeds2pras/src/models/Storage.jl | 96 - reeds2pras/src/models/Thermal_Gen.jl | 82 - reeds2pras/src/models/Variable_Gen.jl | 109 - reeds2pras/src/models/utils.jl | 371 - reeds2pras/src/reeds_to_pras.jl | 73 - reeds2pras/src/utils/reeds_data_parsing.jl | 1186 ---- reeds2pras/src/utils/reeds_input_parsing.jl | 497 -- reeds2pras/src/utils/runchecks.jl | 47 - reeds2pras/test/Project.toml | 8 - reeds2pras/test/runtests.jl | 38 - reeds2pras/test/test-ReEDS2PRAS.jl | 71 - reeds2pras/test/test-benchmark.jl | 35 - reeds2pras/test/utils.jl | 393 -- requirements_dev.txt | 3 - restart_runs.py | 180 - run_pcm.py | 393 -- runbatch.py | 1864 ----- runfiles.csv | 525 -- runstatus.py | 187 - sources.csv | 805 --- sources_documentation.md | 4008 ----------- srun_template.sh | 9 - tc_phaseout.py | 226 - valuestreams.py | 85 - 98 files changed, 51017 deletions(-) delete mode 100644 Augur.py delete mode 100644 ReEDS_Augur/augur_switches.csv delete mode 100644 ReEDS_Augur/capacity_credit.py delete mode 100644 ReEDS_Augur/diagnostic_plots.py delete mode 100644 ReEDS_Augur/prep_data.py delete mode 100644 ReEDS_Augur/run_pras.jl delete mode 100644 ReEDS_Augur/stress_periods.py delete mode 100644 aws_setup.sh delete mode 100644 b_inputs.gms delete mode 100644 c_mga.gms delete mode 100644 c_supplymodel.gms delete mode 100644 c_supplyobjective.gms delete mode 100644 cbc.opt delete mode 100644 cplex.op2 delete mode 100644 cplex.opt delete mode 100644 d1_financials.gms delete mode 100644 d1_temporal_params.gms delete mode 100644 d2_post_solve_adjustments.gms delete mode 100644 d2_unfix_op.gms delete mode 100644 d2_varfix.gms delete mode 100644 d3_data_dump.gms delete mode 100644 d_solve_iterate.py delete mode 100644 d_solveallyears.gms delete mode 100644 d_solveoneyear.gms delete mode 100644 d_solvepcm.gms delete mode 100644 d_solveprep.gms delete mode 100644 d_solvewindow.gms delete mode 100644 dump_alldata.gms delete mode 100644 e_powfrac_calc.gms delete mode 100644 e_report.gms delete mode 100644 e_report_dump.py delete mode 100644 e_report_params.csv delete mode 100644 gurobi.opt delete mode 100644 input_processing/WriteHintage.py delete mode 100644 input_processing/__init__.py delete mode 100644 input_processing/aggregate_regions.py delete mode 100644 input_processing/calc_financial_inputs.py delete mode 100644 input_processing/check_inputs.py delete mode 100644 input_processing/climateprep.py delete mode 100644 input_processing/copy_files.py delete mode 100644 input_processing/forecast.py delete mode 100644 input_processing/fuelcostprep.py delete mode 100644 input_processing/h2_storage.py delete mode 100644 input_processing/hourly_load.py delete mode 100644 input_processing/hourly_plots.py delete mode 100644 input_processing/hourly_repperiods.py delete mode 100644 input_processing/hourly_writetimeseries.py delete mode 100644 input_processing/hydcf.py delete mode 100644 input_processing/mcs_sampler.py delete mode 100644 input_processing/outage_rates.py delete mode 100644 input_processing/plantcostprep.py delete mode 100644 input_processing/recf.py delete mode 100644 input_processing/transmission.py delete mode 100644 input_processing/writecapdat.py delete mode 100644 input_processing/writedrshift.py delete mode 100644 input_processing/writesupplycurves.py delete mode 100644 interim_report.py delete mode 100644 interim_report_batch.py delete mode 100644 pyproject.toml delete mode 100644 raw_value_streams.py delete mode 100644 reeds/inputs.py delete mode 100644 reeds/ra.py delete mode 100644 reeds2pras/Project.toml delete mode 100644 reeds2pras/R2P_Test_Summary.png delete mode 100644 reeds2pras/README.md delete mode 100644 reeds2pras/src/ReEDS2PRAS.jl delete mode 100644 reeds2pras/src/main/create_pras_system.jl delete mode 100644 reeds2pras/src/main/parse_reeds_data.jl delete mode 100644 reeds2pras/src/models/Battery.jl delete mode 100644 reeds2pras/src/models/Gen_Storage.jl delete mode 100644 reeds2pras/src/models/Generator.jl delete mode 100644 reeds2pras/src/models/Interface.jl delete mode 100644 reeds2pras/src/models/Line.jl delete mode 100644 reeds2pras/src/models/Region.jl delete mode 100644 reeds2pras/src/models/Storage.jl delete mode 100644 reeds2pras/src/models/Thermal_Gen.jl delete mode 100644 reeds2pras/src/models/Variable_Gen.jl delete mode 100644 reeds2pras/src/models/utils.jl delete mode 100644 reeds2pras/src/reeds_to_pras.jl delete mode 100644 reeds2pras/src/utils/reeds_data_parsing.jl delete mode 100644 reeds2pras/src/utils/reeds_input_parsing.jl delete mode 100644 reeds2pras/src/utils/runchecks.jl delete mode 100644 reeds2pras/test/Project.toml delete mode 100644 reeds2pras/test/runtests.jl delete mode 100644 reeds2pras/test/test-ReEDS2PRAS.jl delete mode 100644 reeds2pras/test/test-benchmark.jl delete mode 100644 reeds2pras/test/utils.jl delete mode 100644 requirements_dev.txt delete mode 100644 restart_runs.py delete mode 100644 run_pcm.py delete mode 100644 runbatch.py delete mode 100644 runfiles.csv delete mode 100644 runstatus.py delete mode 100644 sources.csv delete mode 100644 sources_documentation.md delete mode 100644 srun_template.sh delete mode 100644 tc_phaseout.py delete mode 100644 valuestreams.py diff --git a/Augur.py b/Augur.py deleted file mode 100644 index 11c856eb..00000000 --- a/Augur.py +++ /dev/null @@ -1,228 +0,0 @@ -#%% Imports -import argparse -import os -import sys -import subprocess -import datetime -import pandas as pd -import gdxpds - -import reeds -import ReEDS_Augur.prep_data as prep_data -import ReEDS_Augur.capacity_credit as capacity_credit -import ReEDS_Augur.stress_periods as stress_periods - - -#%% Functions -def run_pras( - casedir, - t, - iteration=0, - recordtime=True, - repo=False, - overwrite=True, - include_samples=False, - write_flow=False, - write_surplus=False, - write_energy=False, - write_shortfall_samples=False, - write_availability_samples=False, - **kwargs, - ): - """ - If additional keyword arguments are provided, they override values in the switches - dictionary generated using `reeds.io.get_switches(casedir)`. - Only keys in the switches dictionary are allowed, but we do not check types or - self-consistency for the provided values, so review the allowed choices in cases.csv - and use these additional arguments at your own risk. - """ - ### Get the PRAS settings for this solve year - print('Running ReEDS2PRAS and PRAS') - sw = reeds.io.get_switches(casedir) - ## If additional kwargs are provided, use them to override the switch values - for key, val in kwargs.items(): - if key in sw: - sw[key] = val - else: - raise ValueError(f'Provided {key}={val} but {key} is not in switches.csv') - scriptpath = (sw['reeds_path'] if repo else casedir) - start_year = min(sw['resource_adequacy_years_list']) - ## ReEDS2PRAS runs at hourly resolution - timesteps = sw['num_resource_adequacy_years'] * 8760 - command = ' '.join([ - "julia", - f"--project={sw['reeds_path']}", - ### As of 20231113 there seems to be a problem with multithreading in julia on - ### mac M1 machines that causes multithreaded processes to hang - ### without resolution. So disable multithreading on those systems. - ( - '--threads=1' if (sys.platform == 'darwin') or int(sw.get('pras_singlethread', 0)) - else f"--threads={sw['threads'] if sw['threads'] > 0 else 'auto'}" - ), - f"{os.path.join(scriptpath, 'ReEDS_Augur','run_pras.jl')}", - f"--reeds_path={sw['reeds_path']}", - f"--reedscase={casedir}", - f"--solve_year={t}", - f"--weather_year={start_year}", - f"--timesteps={timesteps}", - f"--hydro_energylim={sw['pras_hydro_energylim']}", - f"--write_flow={int(write_flow)}", - f"--write_surplus={int(write_surplus)}", - f"--write_energy={int(write_energy)}", - f"--write_shortfall_samples={int(write_shortfall_samples)}", - f"--write_availability_samples={int(write_availability_samples)}", - f"--iteration={iteration}", - f"--samples={sw['pras_samples']}", - f"--overwrite={int(overwrite)}", - f"--include_samples={int(include_samples)}", - f"--scheduled_outage={sw['pras_scheduled_outage']}", - f"--pras_agg_ogs_lfillgas={int(sw['pras_agg_ogs_lfillgas'])}", - f"--pras_existing_unit_size={int(sw['pras_existing_unit_size'])}", - f"--pras_max_unitsize_prm={int(sw.get('pras_max_unitsize_prm',1))}", - f"--pras_seed={int(sw['pras_seed'])}", - ]) - print(command) - print(f'vvvvvvvvvvvvvvv run_pras.jl {t}i{iteration} vvvvvvvvvvvvvvv') - log = open(os.path.join(casedir, 'gamslog.txt'), 'a') - result = subprocess.run(command, stdout=log, stderr=log, text=True, shell=True) - log.close() - print(f'^^^^^^^^^^^^^^^ run_pras.jl {t}i{iteration} ^^^^^^^^^^^^^^^') - - if recordtime: - try: - reeds.log.write_last_pras_runtime(year=t) - except Exception as err: - print(err) - - return result - - -#%% Main function -def main(t, tnext, casedir, iteration=0): - - # #%% To debug, uncomment these lines and update the run path - # t = 2026 - # tnext = 2029 - # reeds_path = reeds.io.reeds_path - # casedir = os.path.join( - # reeds_path,'runs','v20250521_prasM0_Pacific') - # iteration = 0 - # assert tnext >= t - # os.chdir(casedir) - # ## Copy reeds2pras from repo to run folder - # import shutil - # shutil.rmtree(os.path.join(casedir, 'reeds2pras')) - # shutil.copytree( - # os.path.join(reeds_path, 'reeds2pras'), - # os.path.join(casedir, 'reeds2pras'), - # ignore=shutil.ignore_patterns('test'), - # ) - - #%% Get run settings - sw = reeds.io.get_switches(casedir) - sw['t'] = t - - #%% Prep data for resource adequacy - print('Preparing data for resource adequacy calculations') - tic = datetime.datetime.now() - augur_csv, augur_h5 = prep_data.main(t, casedir, iteration) - reeds.log.toc(tic=tic, year=t, process='ReEDS_Augur/prep_data.py') - - #%% Calculate capacity credit if necessary; otherwise bypass - print('calculating capacity credit...') - tic = datetime.datetime.now() - - if int(sw['GSw_PRM_CapCredit']): - cc_results = capacity_credit.reeds_cc(t, tnext, casedir) - else: - cc_results = { - 'cc_mar': pd.DataFrame(columns=['i','r','ccreg','szn','t','Value']), - 'cc_old': pd.DataFrame(columns=['i','r','ccreg','szn','t','Value']), - 'cc_evmc': pd.DataFrame(columns=['i','r','szn','t','Value']), - 'sdbin_size': pd.DataFrame(columns=['ccreg','szn','bin','t','Value']), - } - - reeds.log.toc(tic=tic, year=t, process='ReEDS_Augur/capacity_credit.py') - - #%% Run PRAS if necessary - solveyears = pd.read_csv( - os.path.join(casedir,'inputs_case','modeledyears.csv') - ).columns.astype(int) - pras_this_solve_year = { - 0: False, - 1: True if t == max(solveyears) else False, - 2: True, - }[int(sw['pras'])] - if pras_this_solve_year or int(sw.GSw_PRM_StressIterateMax): - result = run_pras( - casedir, t, - iteration=iteration, - write_flow=(True if t == max(solveyears) else False), - write_energy=True, - write_shortfall_samples=(True if int(sw.GSw_PRM_UpdateMethod) > 1 else False), - ) - if result.returncode: - raise Exception( - f"run_pras.jl returned code {result.returncode}. Check gamslog.txt for error trace." - ) - - #%% Identify stress periods - print('identifying new stress periods...') - tic = datetime.datetime.now() - if ( - ('user' not in sw['GSw_PRM_StressModel'].lower()) - or ((int(sw.GSw_PRM_StressIterateMax)) and int(sw['GSw_PRM_CapCredit'])) - ): - stress_periods.main(sw=sw, t=t, iteration=iteration) - reeds.log.toc(tic=tic, year=t, process='ReEDS_Augur/stress_periods.py') - - #%% Write gdx file explicitly to ensure that all entries - ### (even empty dataframes) are written as parameters, not sets - with gdxpds.gdx.GdxFile() as gdx: - for key in cc_results: - gdx.append( - gdxpds.gdx.GdxSymbol( - key, gdxpds.gdx.GamsDataType.Parameter, - dims=cc_results[key].columns[:-1].tolist(), - ) - ) - gdx[-1].dataframe = cc_results[key] - gdx.write( - os.path.join('ReEDS_Augur', 'augur_data', f'ReEDS_Augur_{t}.gdx') - ) - - # #%% Uncomment to run diagnostic_plots - # ### (typically run from call_{}.sh script for parallelization) - # try: - # import ReEDS_Augur.diagnostic_plots as diagnostic_plots - # diagnostic_plots.main(sw) - # except Exception as err: - # print('diagnostic_plots.py failed with the following exception:') - # print(err) - - -#%% Procedure -if __name__ == '__main__': - - parser = argparse.ArgumentParser(description="""Running ReEDS Augur""") - - parser.add_argument("tnext", help="Next ReEDS solve year", type=int) - parser.add_argument("t", help="Previous ReEDS solve year", type=int) - parser.add_argument("casedir", help="Path to ReEDS run") - parser.add_argument('--iteration', '-i', default=0, type=int, - help='iteration number on this solve year') - - args = parser.parse_args() - - tnext = args.tnext - t = args.t - casedir = args.casedir - iteration = args.iteration - - #%% Set up logger - reeds.log.makelog( - scriptname=f'{__file__} {t}-{tnext}', - logpath=os.path.join(casedir,'gamslog.txt'), - ) - - main(t=t, tnext=tnext, casedir=casedir, iteration=iteration) diff --git a/ReEDS_Augur/augur_switches.csv b/ReEDS_Augur/augur_switches.csv deleted file mode 100644 index bff65dbe..00000000 --- a/ReEDS_Augur/augur_switches.csv +++ /dev/null @@ -1,16 +0,0 @@ -key,value,dtype,description -cc_all_resources,FALSE,boolean,indicate whether to calculate capacity credit between all pairs of resources and regions (TRUE) or just for resources within region (FALSE) -cc_ann_hours,20,int,number of top hours considered in annual cc calculations -cc_calc_annual,FALSE,boolean,when true: annual cc values are calculated -cc_calc_seasonal,TRUE,boolean,when true: seasonal cc values are calculated -cc_default_rte,0.85,float,default efficiency value to use for assessing peaking storage potential -cc_marg_evmc_mw,100,int,step size used for marginal DR cc calculations -cc_max_stor_pen,0.9,float,max fraction of peak load considered for storage peaking capacity assessment -cc_safety_bin_size,100000,int,default value (in MW) for the safety bin size in ReEDS -cc_stor_buffer,60,int,additional duration (in minutes) that is required of storage to receive full capacity credit -cc_stor_stepsize,100,int,step size (in MW) used when determining the peaking capacity potential of storage -decimals,3,int,number of decimals to round results to for ReEDS -flex_consume_techs,"dac,electrolyzer",list,list of consume techs that are flexible -keepfiles,"dropped_load,cf",list,list of temporary files to keep -marg_vre_steps,2,int,Number of previous solve years to consider when evaluating the marginal VRE step size (default: 2). Must be at least 1; a value of 2 can help reduce oscillations. Augur will automatically drop from consideration solves that are more than 5 years from the previous solve. -storcap_cutoff,1,float,[MW and MWh] Minimum storage capacity to send to ReEDS2PRAS (applies to both power and energy capacity) diff --git a/ReEDS_Augur/capacity_credit.py b/ReEDS_Augur/capacity_credit.py deleted file mode 100644 index d778d6da..00000000 --- a/ReEDS_Augur/capacity_credit.py +++ /dev/null @@ -1,862 +0,0 @@ -#%% IMPORTS -import os -import numpy as np -import pandas as pd -import gdxpds -import reeds - - -#%% Functions -def get_relative_step_sizes(t, yearset, target_step): - ''' - Checking the relative ReEDS temporal step sizes for this solve year and - any previous solve year, specified by 'target_step' - ''' - i = yearset.index(t) - tnext = yearset[i+1] - - j = yearset.index(target_step) - targ_prev = yearset[j-1] - - relative_step_sizes = (tnext - t) / (target_step - targ_prev) - return relative_step_sizes - - -def set_marg_vre_step_size(t, sw, gdx, hierarchy): - ''' - Marginal vre step size has a default floor value of 1000 MW but - here we check to see if it needs to be higher. The function looks - back by the number of steps specified by 'marg_vre_steps' and computes - the max of the average new vre investment in those previous steps. - We take the max of that value and 1000 MW to set the set size. - The fuction also accounts for potentially varying step sizes in ReEDS. - - Inputs - * marg_vre_steps [int]: Number of previous solve years to consider when - evaluating the marginal VRE step size (default: 2). Must be at least 1; - a value of 2 can help reduce oscillations. Augur will automatically drop - from consideration solves that are more than 5 years from the previous solve. - ''' - # load yearset for getting various previous steps - yearset = gdx['tmodel_new'].allt.astype(int).tolist() - - # collect list of previous years and their relative step sizes - prev_year_list = [] - step_sizes = [] - for step in range(int(sw['marg_vre_steps'])): - - # try-except to handle cases where there aren't multiple - # steps to go back to (e.g. running Augur after 1st solve) - try: - target_last_step = yearset[yearset.index(t)-step] - - # only look at steps beyond the previous year if the step sizes - # are less than 5 years - if (t - target_last_step) < 5: - step_sizes.append(get_relative_step_sizes(t, yearset, target_last_step)) - prev_year_list.append(target_last_step) - except Exception: - print('First Augur year so no previous steps') - - relative_step_sizes = pd.DataFrame(list(zip(prev_year_list, step_sizes)), - columns=['t', 'step']) - - # load investment data for all techs - techs = gdx['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') - inv = gdx['inv_ivrt'].astype({'t':int}) - - # get investment from any previous steps under consideration - inv_last_years = inv[inv['t'].isin(prev_year_list)] - inv_vre = inv_last_years[inv_last_years['i'].isin(techs['VRE'].dropna().index)] - - # map inv_vre to ccregions - r_ccreg = hierarchy[['r','ccreg']].drop_duplicates() - inv_vre = inv_vre.merge(r_ccreg, on = 'r') - - # aggregate by tech and then and compute average across the appropriate - # geographic resolution - r for curtailment, ccreg for capacity credit - level = 'ccreg' - df = ( - inv_vre.groupby([level, 't'], as_index=False).Value.sum() - .groupby(['t'], as_index=False).Value.mean() - ) - # adjust each previous step by its relative step size - df = df.merge(relative_step_sizes, on='t') - df['Value'] *= df['step'] - - # now get max across all previous steps and set as marg_vre_mw - marg_vre_mw = round(df['Value'].max(), 0) - - marg_vre_mw_cc = int(max(int(sw['marg_vre_mw']), marg_vre_mw)) - print(f'marg_vre_mw_cc set to {marg_vre_mw_cc}') - - return marg_vre_mw_cc - -def load_evmc_data(csv_path,inputs_case,h_dt_szn, - set_h_szn_cols=['h','ccseason','hour'], - set_idx_cols=['h','hour', 'year','ccseason']): - df = pd.read_csv(os.path.join(inputs_case,csv_path)) - df = pd.merge(df,h_dt_szn[set_h_szn_cols],on='hour',how='left') - return df.set_index(set_idx_cols) - - -#%% Main function -def reeds_cc(t, tnext, casedir): - ''' - This function directs all of the capacity credit calculations for ReEDS - It writes out a gdx file which is then read back in to ReEDS during the - next iteration. - ''' - #%% Get the switches - sw = reeds.io.get_switches(casedir) - - #%% Set up log - log = reeds.log.makelog( - 'capacity_credit.py', - os.path.join(sw['casedir'], 'gamslog.txt'), - ) - - #%% Load some inputs - inputs_case = os.path.join(casedir, 'inputs_case') - hierarchy = reeds.io.get_hierarchy(casedir).reset_index() - resources = pd.read_csv(os.path.join(inputs_case, 'resources.csv')) - - augur_data = os.path.join(casedir,'ReEDS_Augur','augur_data') - cap = pd.read_csv(os.path.join(augur_data, f'max_cap_{t}.csv')) - - gdx = gdxpds.to_dataframes(os.path.join(augur_data,f'reeds_data_{t}.gdx')) - techs = gdx['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') - techs.columns = techs.columns.str.lower() - r = gdx['rfeas'] - - cap_stor = cap.loc[cap['i'].isin(gdx['storage_standalone'].i) & - ~cap['i'].isin(gdx['i_subsets'][gdx['i_subsets']['i_subtech'] == 'CONTINUOUS_BATTERY'].i)] \ - .rename(columns={'Value':'MW'}) - cap_stor['duration'] = cap_stor.i.map(gdx['storage_duration'].set_index('i').Value) - cap_stor['MWh'] = cap_stor['MW'] * cap_stor['duration'] - #Adding a check if there is no storage - populate with 0 MW and 0 MWh in each r - if cap_stor.empty: - stor_techs = gdx['storage_standalone'].i.tolist() - r_values = r['r'].tolist() - for tech_name in stor_techs: - for r_val in r_values: - cap_stor.loc[len(cap_stor)] = [tech_name,'', r_val, 0, 0, 0] - cap_stor['duration'] = cap_stor.i.map(gdx['storage_duration'].set_index('i').Value) - - cap_stor_agg = cap_stor.merge(hierarchy[['r','ccreg']], on='r') - cap_stor_agg = cap_stor_agg.groupby('ccreg', as_index=False)[['MW','MWh']].sum() - sdb = gdx['sdbin'].rename(columns={'*':'bin'})[['bin']] - - ### Get the marginal step size - marg_vre_mw_cc = set_marg_vre_step_size(t, sw, gdx, hierarchy) - - ### Get the non-duplicated profiles - resource_profiles = resources.drop_duplicates('resource') - - # Remove the "8760" safety valve bin - safety_bin = max(sdb['bin'].values) - sdb = sdb[sdb['bin'] != max(sdb['bin'])] - sdb = [int(x) for x in sdb['bin']] - - # Temporal definitions - h_dt_szn = pd.read_csv(os.path.join('inputs_case', 'rep', 'h_dt_szn.csv')) - - ccseasons = [] - if sw['cc_calc_annual']: - ccseasons += ['year'] - if sw['cc_calc_seasonal']: - ccseasons += h_dt_szn['ccseason'].drop_duplicates().tolist() - - ### Prepare the seasonal profiles - ## vre_gen needs to have tech_class_r columns - ## last version has (ccseason,year,h,hour) index - vre_gen = pd.read_hdf(os.path.join(augur_data,f'vre_gen_exist_{t}.h5')) - ## vre_cf_marg has same columns and index as vre_gen - vre_cf_marg = pd.read_hdf(os.path.join(augur_data,f'vre_cf_marg_{t}.h5')) - - if int(sw['GSw_PRM_CapCreditMulti']) == 0: - # Restrict capacity credit evaluation to use 2012 only (rather than multi-year) - vre_gen = vre_gen[vre_gen.index.get_level_values('year') == 2012].copy() - vre_cf_marg = vre_cf_marg[vre_cf_marg.index.get_level_values('year') == 2012].copy() - - vregen_ccseason = {} - vregen_marginal_ccseason = {} - for ccseason in ccseasons: - if ccseason == 'year': - vregen_ccseason[ccseason] = vre_gen - vregen_marginal_ccseason[ccseason] = vre_cf_marg * marg_vre_mw_cc - else: - vregen_ccseason[ccseason] = vre_gen.loc[ccseason] - vregen_marginal_ccseason[ccseason] = (vre_cf_marg * marg_vre_mw_cc).loc[ccseason] - - load_profiles = ( - # HOURLY_PROFILES['load'].profiles - pd.read_hdf(os.path.join(augur_data,f'load_{t}.h5')) - ### Map BA regions to ccreg's and sum over them - .rename(columns=hierarchy.set_index('r').ccreg) - .groupby(axis=1, level=0).sum() - ) - - if int(sw['GSw_PRM_CapCreditMulti']) == 0: - # Restrict capacity credit evaluation to use 2012 only (rather than multi-year) - load_profiles = load_profiles[load_profiles.index.get_level_values('year') == 2012].copy() - - # Get EVMC data if necessary - if int(sw['GSw_EVMC']): - # Get EVMC props - evmccf_shape_increase = load_evmc_data('evmc_shape_profile_increase.csv',inputs_case,h_dt_szn) - evmccf_shape_decrease = load_evmc_data('evmc_shape_profile_decrease.csv',inputs_case,h_dt_szn) - - # Initialize dataframes to store results - dict_cc_old = {} - dict_cc_mar = {} - dict_sdbin_size = {} - dict_cc_evmc = {} - dict_net_load = {} - dict_net_load_2012 = {} - - #%% Loop over CCREGs - for ccreg in hierarchy['ccreg'].drop_duplicates(): - #% CCREG DATA - # ccreg = 'cc6' # Uncomment for debugging - # ------- Get load profile, RECF profiles, VG capacity, storage - # capacity, and storage RTE for this CCREG ------- - - log.info('Calculating capacity credit for {}'.format(ccreg)) - - # Resources to be used - resources_ccreg = resource_profiles[resource_profiles['ccreg'] == ccreg] - resourcelist = ( - slice(None) if sw['cc_all_resources'] - else resources_ccreg.resource.tolist() - ) - - # Hourly profiles - load_profile_ccreg = load_profiles[ccreg] - - # EVMC profile - if int(sw['GSw_EVMC']): - evmc_shape_reg = [r for r in resources_ccreg.r.drop_duplicates() - if r in evmccf_shape_increase.columns] - evmccf_shape_increase_ccreg = evmccf_shape_increase[['i'] + evmc_shape_reg] - evmc_shape_reg = [r for r in resources_ccreg.r.drop_duplicates() - if r in evmccf_shape_decrease.columns] - evmccf_shape_decrease_ccreg = evmccf_shape_decrease[['i'] + evmc_shape_reg] - - # Storage information - # Note that we only calculate storage capacity credit for storage in the same ccreg - cap_stor_agg_ccreg = cap_stor_agg[ - cap_stor_agg['ccreg'] == ccreg].reset_index(drop=True) - - cap_stor_ccreg = cap_stor[ - cap_stor['r'].isin(hierarchy[hierarchy['ccreg'] == ccreg]['r']) - ].reset_index(drop=True) - # df = cap_stor.assign(ccreg=cap_stor.r.map(hierarchy.set_index('r').ccreg)) - # df.groupby('ccreg').MW.max() - # df.groupby('ccreg').MWh.max() - - try: - eff_charge = cap_stor_agg_ccreg['rte'].values[0] - except Exception: - eff_charge = float(sw['cc_default_rte']) - - max_demand = load_profile_ccreg.max() / (1/float(sw['cc_max_stor_pen'])) - reductions_considered = int(max_demand // float(sw['cc_stor_stepsize'])) - peak_reductions = np.linspace(0, max_demand, reductions_considered) - #Skip CC calculation if number of reductions_considered is only 1. Avoids error in interpolation within cc_storage function. - if reductions_considered == 1: - continue - - # log.debug(f'max_demand = {max_demand}') - # log.debug(f'reductions_considered = {reductions_considered}') - # log.debug(f'peak_reductions diff = {peak_reductions[1] - peak_reductions[0]}') - - # ---------------------------- CALL FUNCTIONS ------------------------- - #%% Loop over ccseasons - for ccseason in ccseasons: - #%% - # ccseason = 'winter' # Uncomment for debugging - # Get the load and CF profiles for this ccseason - if ccseason == 'year': - load_profile_ccseason = load_profile_ccreg.copy() - hours_considered = int(sw['cc_ann_hours']) - if int(sw['GSw_EVMC']): - evmc_shape_load_ccseason = evmccf_shape_increase_ccreg.copy() - evmc_shape_gen_ccseason = evmccf_shape_decrease_ccreg.copy() - - else: - load_profile_ccseason = load_profile_ccreg.xs( - ccseason, axis=0, level='ccseason').reset_index() - hours_considered = int(sw['GSw_PRM_CapCreditHours']) - - if int(sw['GSw_EVMC']): - evmc_shape_load_ccseason = evmccf_shape_increase_ccreg.xs( - ccseason, axis=0, level='ccseason').reset_index() - evmc_shape_gen_ccseason = evmccf_shape_decrease_ccreg.xs( - ccseason, axis=0, level='ccseason').reset_index() - - # log.debug(ccseason, int(len(load_profile_ccseason) / 7)) - ###### Calculate the capacity credit for each resource - cc_vg_results = cc_vg( - vg_power=vregen_ccseason[ccseason][resourcelist].values, - load=load_profile_ccseason[ccreg].values, - vg_marg_power=vregen_marginal_ccseason[ccseason][resourcelist].values, - top_hours_n=hours_considered, cap_marg=marg_vre_mw_cc) - - ###### Store the existing and marginal capacity credit results - dict_cc_old[ccreg, ccseason] = pd.DataFrame({ - 'resource': resource_profiles.set_index('resource').loc[resourcelist].index, - 'MW': cc_vg_results['cap_useful_MW'][:,0], - }) - - dict_cc_mar[ccreg, ccseason] = ( - resource_profiles.loc[resource_profiles.resource.isin(resourcelist)] - .drop('ccreg', axis=1) - .assign(CC=cc_vg_results['cc_marg']) - ) - - net_load_ccreg_ccseason = ( - load_profile_ccseason.drop(columns=ccreg) - .assign(MW=cc_vg_results['load_net']) - .sort_values(['MW'], ascending=False) - ) - #Save top n hrs of net load for ccreg and ccseason across all years, and for 2012 alone - net_load_out_numhrs = 500 - dict_net_load[ccreg, ccseason] = net_load_ccreg_ccseason.head(net_load_out_numhrs) - dict_net_load_2012[ccreg, ccseason] = ( - net_load_ccreg_ccseason[net_load_ccreg_ccseason['year']==2012].head(net_load_out_numhrs) - ) - - ###### Calculate the storage capacity credit - # The call to this function gives the MWh required for each - # peak reduction capacity. For each data year, loop through - # and get get the MWh needed for each peak reduction capacity. - # Get a "ccseason_required_MWhs" for each year. - # Get the maximum value for each position in the array. - # Make a new "ccseason_required_MWhs" array to send to the - # cc_storage function. - # Call storage cc functions for existing and marginal - # conventional storage. - net_load_profile_timestamp = pd.DataFrame( - cc_vg_results['load_net'], load_profile_ccseason.year) - years = list(net_load_profile_timestamp.index.unique()) - - for y in years: - net_load_profile_temp = net_load_profile_timestamp.iloc[ - :, 0][net_load_profile_timestamp.index == y].to_numpy() - required_MWhs_temp, batt_powers = calc_required_mwh( - load_profile=net_load_profile_temp.copy(), - peak_reductions=peak_reductions.copy(), - eff_charge=eff_charge, stor_buffer_minutes=float(sw['cc_stor_buffer'])) - - if years.index(y) == 0: - required_MWhs = required_MWhs_temp.copy() - else: - required_MWhs = np.maximum(required_MWhs, required_MWhs_temp) - - # Get the peaking storage potential by duration - peaking_stor = cc_storage( - pr=peak_reductions.copy(), - re=required_MWhs.copy(), - sdb=sdb.copy(), log=log) - - # Store the storage capacity credit along with the energy capacity. - # For the safety bin, compute MWh as safety_bin * cc_safety_bin_size. - dict_sdbin_size[ccreg, ccseason] = pd.concat([ - peaking_stor[['duration', 'MW']], - pd.DataFrame({ - 'duration': [safety_bin], - 'MW': float(sw['cc_safety_bin_size']) - }), - ], ignore_index=True) - - def pivot_melt_data(df): - return pd.pivot_table( - pd.melt(df, - id_vars=['h','year','hour','i'], - var_name='r'), - index=['year','hour','h'], - columns=['i','r'], values='value') - - if int(sw['GSw_EVMC']): - evmc_shape_inc_timestamp = pivot_melt_data(evmc_shape_load_ccseason) - evmc_shape_dec_timestamp = pivot_melt_data(evmc_shape_gen_ccseason) - evmc_years = evmc_shape_dec_timestamp.index.get_level_values('year').unique() - #do as evmc cap credit instead? - if len(evmc_years)==1: - gen_array = evmc_shape_dec_timestamp.values - evmc_shape_inc_timestamp.values - evmc_shape_marg_power = np.tile(gen_array,(7,1)) - elif len(evmc_years)==7: - evmc_shape_marg_power = evmc_shape_dec_timestamp.values - evmc_shape_inc_timestamp.values - else: - log.info("no weather year data on EVMC for any relevant regions; skipping") - continue - - ###### Calculate the capacity credit for each evmc_shape resource - results_evmc_shape = cc_evmc_shape(load=cc_vg_results['load'], - load_net=cc_vg_results['load_net'], - top_hours_net=cc_vg_results['top_hours_net'], - top_hours_n=hours_considered, - evmc_shape_marg_power=evmc_shape_marg_power*float(sw['marg_evmc_mw']), - cap_marg=float(sw['marg_evmc_mw'])) - - evmc_cc_i = pd.melt(pd.DataFrame(data=[np.round(results_evmc_shape, decimals=5), ], - columns=evmc_shape_dec_timestamp.columns)) - - if (ccreg, ccseason) in dict_cc_evmc.keys(): - dict_cc_evmc[ccreg, ccseason] = pd.concat( - [dict_cc_evmc[ccreg, ccseason], - evmc_cc_i[['r', 'i', 'value']]]) - else: - dict_cc_evmc[ccreg, ccseason] = evmc_cc_i[['r', 'i', 'value']] - - # ------ AGGREGATE OUTPUTS ------ - cc_old = ( - pd.concat(dict_cc_old, axis=0) - ### Drop the ccreg and numbered indices - .reset_index().drop(['level_2'], axis=1) - .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'MW':'value'}) - .assign(t=str(tnext)) - .merge(resources.drop('ccreg',axis=1), on='resource', how='left') - ) - ### Reorder to match ReEDS convention - cc_old = cc_old.reindex(['i','r','ccreg','ccseason','t','value'], axis=1) - - sdbin_size = ( - pd.concat(dict_sdbin_size, axis=0) - ### Keep the ccreg and ccseason indices but drop the numbered index - .reset_index().drop('level_2', axis=1) - .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'duration':'bin'}) - .astype({'bin':str}) - .assign(t=str(tnext)) - .reindex(['ccreg','ccseason','bin','t','MW'], axis=1) - ) - - cc_mar = ( - pd.concat(dict_cc_mar, axis=0) - .reset_index().drop('level_2', axis=1) - .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'CC':'value'}) - .assign(t=str(tnext)) - ) - ### Reorder to match ReEDS convention - cc_mar = cc_mar.reindex(['i','r','ccreg','ccseason','t','value'], axis=1) - - net_load = ( - pd.concat(dict_net_load, axis=0) - .reset_index().drop(['level_2'], axis=1) - .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'MW':'value'}) - ### Rename seasons to match ReEDS convention and add year index - .replace({'winter':'wint', 'spring':'spri', 'summer':'summ'}) - .assign(t=str(tnext)) - .sort_values(['ccreg','hour']) - ) - ### Reorder to match ReEDS convention - net_load = net_load.reindex(['ccreg','ccseason','year','h','hour','t','value'], axis=1) - - net_load_2012 = ( - pd.concat(dict_net_load_2012, axis=0) - .reset_index().drop(['level_2'], axis=1) - .rename(columns={'level_0':'ccreg', 'level_1':'ccseason', 'MW':'value'}) - ### Rename seasons to match ReEDS convention and add year index - .replace({'winter':'wint', 'spring':'spri', 'summer':'summ'}) - .assign(t=str(tnext)) - .sort_values(['ccreg','hour']) - ) - ### Reorder to match ReEDS convention - net_load_2012 = net_load_2012.reindex(['ccreg','ccseason','year','h','hour','t','value'], axis=1) - - if int(sw['GSw_EVMC']): - cc_evmc = ( - pd.concat(dict_cc_evmc, axis=0) - .reset_index().drop(['level_2', 'level_0'], axis=1) - .rename(columns={'level_1':'ccseason'}) - .assign(t=str(tnext)) - .reindex(['i','r','ccseason','t','value'], axis=1) - ) - else: - cc_evmc = pd.DataFrame(columns=['i', 'r', 'ccseason', 't', 'value']) - - # ---------------- RETURN A DICTIONARY WITH THE OUTPUTS FOR REEDS -------- - - cc_results = { - 'cc_mar': cc_mar, - 'cc_old': cc_old, - 'cc_evmc': cc_evmc, - 'sdbin_size': sdbin_size, - 'net_load': net_load, - 'net_load_2012': net_load_2012, - } - - return cc_results - -#%% Additional functions -# ------------------ CALC CC OF EXISTING VG RESOURCES ------------------------- -# @numba.jit(cache=True) -def cc_vg(vg_power, load, vg_marg_power, top_hours_n, cap_marg): - ''' - Calculate the capacity credit of existing and marginal variable generation - capacity using a top hour approximation. More details on the methodology - used in this approximation can be found here: - //nrelnas01/ReEDS/8760_Method_Inputs/8760 Method Documentation - Args: - vg_power: numpy matrix containing power output profiles for all - hours_n for each variable generating resource - load: numpy array containing time-synchronous load profile for all - hours_n. Units: MW - cf_marg: numpy array containing capacity factor profiles for marginal - builds of each variable generating resource - top_hours_n: number of top hours to consider for the calculation - cap_marg: marginal capacity used to calculate marginal capacity credit - Returns: - cc_marg: marginal capacity credit for each variable generating resource - load_net: net load profile. Units: MW - top_hours_net: arguments for the highest net load hours in load_net, - length top_hours_n - top_hours: argumnets for the highest load hours in load, length - top_hours_n - Notes: - Currently only built for hourly profiles. Generalize to any duration - timestep. - ''' - - # number of hours in the load and CF profiles - hours_n = len(load) - - # get the net load that must be met with conventional generation - load_net = load - np.sum(vg_power, axis=1) - - # get the indices of the top hours of net load - top_hours_net = load_net.argsort()[np.arange(hours_n-1, (hours_n-top_hours_n)-1, -1)] - - # get the indices of the top hours of load - top_hours = load.argsort()[np.arange(hours_n-1, (hours_n-top_hours_n)-1, -1)] - - # get the differences and reductions in load as well as the ratio between the two - - # load_ratio is the effective reduction in load from wind and PV for each top load - # hour, and is used to scale the contributions of wind and PV respectively - # see slide 5 of "\\nrelnas01\ReEDS\8760_Method_Inputs\8760 Method Documentation\ - # VG Capacity credit allocation documentation.pptx" for additional details - load_dif = load[top_hours] - load_net[top_hours] - load_reduct = load[top_hours] - load_net[top_hours_net] - load_ratio = np.tile( - np.divide( - load_reduct, load_dif, - out=np.zeros_like(load_reduct), - where=load_dif != 0, - ).reshape(top_hours_n, 1), - (1, vg_power.shape[1]) - ) - # get the existing cc for each resource - gen_tech = ( - vg_power[top_hours_net, :] - + np.where( - load_ratio < 1, - vg_power[top_hours, :]*load_ratio, - vg_power[top_hours, :])) - - gen_sum = np.tile( - np.sum(gen_tech, axis=1).reshape(top_hours_n, 1), - (1, vg_power.shape[1])) - - gen_frac = np.divide( - gen_tech, gen_sum, - out=np.zeros_like(gen_tech), where=gen_sum != 0) - - cap_useful_MW = ( - np.sum( - gen_frac - * np.tile(load_reduct.reshape(top_hours_n, 1), (1, vg_power.shape[1])), - axis=0) - / top_hours_n - ).reshape(vg_power.shape[1], 1) - - # get the marg net load for each VG resource [hours x resources] - load_marg = ( - np.tile(load_net.reshape(hours_n, 1), (1, vg_marg_power.shape[1])) - - vg_marg_power) - - ### Get the peak net load hours [top_hours_n x resources] - peak_net_load = np.transpose(np.array( - ### np.partition returns the max top_hours_n values, unsorted; then np.sort sorts. - ### So we only sort top_hours_n values instead of the whole array, saving time. - [np.sort( - np.partition(load_marg[:,n], -top_hours_n)[-top_hours_n:] - )[::-1] - for n in range(load_marg.shape[1])] - )) - - # get the reductions in load for each resource - load_reduct_marg = np.tile( - load_net[top_hours_net].reshape(top_hours_n, 1), - (1, vg_marg_power.shape[1]) - ) - peak_net_load - - # get the marginal CCs for each resource - cc_marg = np.sum(load_reduct_marg, axis=0) / top_hours_n / cap_marg - - # setting the lower bound for marginal CC to be 0.01 - cc_marg[cc_marg < 0.01] = 0.0 - - # round the outputs - load_net = np.around(load_net, decimals=3) - cc_marg = np.around(cc_marg, decimals=5) - cap_useful_MW = np.around(cap_useful_MW, decimals=5) - - results = { - 'load': load, - 'load_net': load_net, - 'cc_marg': cc_marg, - 'cap_useful_MW': cap_useful_MW, - 'top_hours_net': top_hours_net, - 'peak_net_load': peak_net_load - } - - return results - -def cc_evmc_shape(load,load_net,top_hours_net,top_hours_n,evmc_shape_marg_power,cap_marg): - ''' - Calculate the capacity credit of marginal evmc_shape resources - using a top hour approximation. - Args: - load: numpy array containing time-synchronous load profile for all - hours_n. Units: MW - load_net: net load profile. Units: MW - top_hours_net: arguments for the highest net load hours in load_net, - calculated in cc_vg(). Is of length top_hours_n - top_hours_n: number of top hours to consider for the calculation - evmc_shape_marg: numpy array containing capacity factor profiles for marginal - builds of each evmc_shape resource bin - cap_marg: marginal capacity used to calculate marginal capacity credit - Returns: - cc_marg: marginal capacity credit for each evmc_shape resource - Notes: - Currently only built for hourly profiles. Generalize to any duration - timestep. - ''' - hours_n = len(load) - - # get the marg net load for each evmc_shape resource [hours x resources] - load_marg = ( - np.tile(load_net.reshape(hours_n, 1), (1, evmc_shape_marg_power.shape[1])) - - evmc_shape_marg_power) - - ### Get the peak net load hours [top_hours_n x evmc_shape resources] - peak_net_load = np.transpose(np.array( - ### np.partition returns the max top_hours_n values, unsorted; then np.sort sorts. - ### So we only sort top_hours_n values instead of the whole array, saving time. - [np.sort( - np.partition(load_marg[:,n], -top_hours_n)[-top_hours_n:] - )[::-1] - for n in range(load_marg.shape[1])] - )) - - load_reduct_marg = np.tile( - load_net[top_hours_net].reshape(top_hours_n, 1), - (1, evmc_shape_marg_power.shape[1]) - ) - peak_net_load - # get the marginal CCs for each resource - cc_marg = np.sum(load_reduct_marg, axis=0) / top_hours_n / cap_marg - - # setting the lower bound for marginal CC to be 0.01 - cc_marg[cc_marg < 0.01] = 0.0 - return cc_marg - -# -------------------------CALC REQUIRED MWHS---------------------------------- -# @numba.jit(nopython=True, cache=True) -def calc_required_mwh(load_profile, peak_reductions, eff_charge, stor_buffer_minutes): - ''' - Determine the energy storage capacity required to acheive a certain peak - load reduction for a given load profile - Args: - load_profile: time-synchronous load profile - peak_reductions: set of peak reductions (in MW) to be tested - eff_charge: RTE of charging - Returns: - required_MWhs: set of energy storage capacities required for each peak - reduction size - batt_powers: corresponding peak reduction sizes for required_MWhs - ''' - - hours_n = len(load_profile) - - inc = len(peak_reductions) - max_demands = np.tile( - (np.max(load_profile) - peak_reductions).reshape(inc, 1), (1, hours_n)) - batt_powers = np.tile(peak_reductions.reshape(inc, 1), (1, hours_n)) - - poss_charges = np.minimum(batt_powers * eff_charge, - (max_demands - load_profile) * eff_charge) - necessary_discharges = (max_demands - load_profile) - - poss_batt_changes = np.where(necessary_discharges <= 0, - necessary_discharges, poss_charges) - - batt_e_level = np.zeros([inc, hours_n]) - batt_e_level[:, 0] = np.minimum(poss_batt_changes[:, 0], 0) - for n in np.arange(1, hours_n): - batt_e_level[:, n] = batt_e_level[:, n-1] + poss_batt_changes[:, n] - batt_e_level[:, n] = np.clip(batt_e_level[:, n], a_min=None, - a_max=0.0, out=batt_e_level[:, n]) - - required_MWhs = -np.min(batt_e_level, axis=1) - - # 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[:, 0] * stor_buffer_hrs) - - return required_MWhs, batt_powers - - -# --------------------- CALC CC OF MARGINAL STORAGE --------------------------- -def cc_storage(pr, re, sdb, log=None): - """ - Determine the theoretical peaking capacity (MW) for incrementally increasing - storage durations, based on the MW–MWh curve (pr, re). - - Args: - pr (array-like): - Array of storage power capacities (MW) analyzed. - re (array-like): - Array of corresponding energy capacities (MWh). - sdb (list or array-like): - Storage duration bins (in hours) to evaluate (e.g., [2,4,6,8,...]). - - Returns: - pd.DataFrame with columns: - - MW: The theoretical peaking potential for each bin. - """ - # Initializing terms - ds = sdb.copy() - min_bin = min(ds) - ds.remove(min_bin) - peak_stor = pd.DataFrame(columns=['peaking potential', 'existing power']) - - # Get the step size and make a smaller step size for interpolation - p_step = (pr[1] - pr[0]) - rel_step = 100 - p_step_small = p_step / rel_step - - # Get duration and marginal duration as a function of storage penetration - dur = np.zeros(len(pr)) - dur[1:] = re[1:] / pr[1:] - dur = dur.round(3) - dur_marg = np.zeros(len(pr)) - for i in range(1, len(pr)): - dur_marg[i] = ((re[i] - re[i-1]) / (pr[i] - pr[i-1])) - dur_marg = dur_marg.round(3) - - # ---------------------------------------------------------------- - # 1) Determine peaking potential for the smallest duration bin - # ---------------------------------------------------------------- - - # Find the limit of 2-hour storage capacity - dur_temp = dur[dur <= min_bin].copy() - dur_marg_temp = dur_marg[dur_marg < float(min(ds))].copy() - - - # Case 1: Storage potential bleeds into the next bin's marginal addition - if len(dur_marg_temp) < len(dur_temp): - peak_stor.loc[min_bin, 'peaking potential'] = pr[len(dur_marg_temp)-1] - - # Case 2: There's storage potential for the smallest bin - elif len(dur_temp) > 1: - # If the marginal duration is acceptable at the crossover point, find - # the crossover point. - lower_bound_p = pr[len(dur_temp) - 1] - upper_bound_p = pr[len(dur_temp)] - lower_bound_e = re[len(dur_temp) - 1] - upper_bound_e = re[len(dur_temp)] - min_p = np.linspace(lower_bound_p, upper_bound_p, (rel_step**2) + 1) - min_e = np.linspace(lower_bound_e, upper_bound_e, (rel_step**2) + 1) - min_dur = min_e / min_p - min_dur_temp = min_dur[min_dur <= min_bin].copy() - # If the duration is already the min duration, don't interpolate - if len(min_dur_temp) == 0: - peak_stor.loc[min_bin,'peaking potential'] = lower_bound_p - else: - # Find the max addition that could be made without exceeding the - # marginal duration limit. - dur_marg_test = min(min(ds), dur_marg[len(dur_temp)]) - max_interp = ((p_step * (min(ds) - dur_marg_test)) - / (min(ds) - min_bin)) + lower_bound_p - # Set the peaking potential for the lowest bin to be the minimum - # between the crossover point and the maximum allowed interpolated - # value (limited by the marg duration and p_step size). - peak_stor.loc[min_bin, 'peaking potential'] = min( - max_interp, min_p[len(min_dur_temp) - 1]) - - # Case 3: No storage potential for the smallest bin - elif len(dur_temp) == 1: - peak_stor.loc[min_bin, 'peaking potential'] = 0 - - # ---------------------------------------------------------------- - # 2) Determine peaking potential for the remaining duration bins - # ---------------------------------------------------------------- - # Iterate through the rest of the storage duration bins to find the - # peaking potential. - for i in range(0, len(ds)): - d = ds[i] - try: - d1 = ds[i+1] - except Exception: - d1 = d * 2 - e_base = 0 - p_base = 0 - for key in peak_stor.index: - e_base += peak_stor.loc[key, 'peaking potential'] * key - p_base += peak_stor.loc[key, 'peaking potential'] - # First check to see if this bin size will be limited by marginal - # duration. - dur_marg_temp = dur_marg[dur_marg < float(d1)].copy() - p_temp = pr[len(dur_marg_temp) - 1] - p_base - e_temp = e_base + (p_temp * d) - e_test = re[len(dur_marg_temp) - 1] - if e_test <= e_temp: - peak_stor.loc[d, 'peaking potential'] = p_temp - else: - # Now add small incremental capacity until we reach the crossover - # point - error = 0 - p = p_base - condition = True - while condition: - p_test = p + p_step_small - e_test = e_base + ((p_test - p_base) * d) - if np.interp(p_test, pr, re) >= e_test: - condition = False - else: - p += p_step_small - error += 1 - if error > 1e7: - log.info(d) - condition = False - log.info('**** Runaway while loop in capacity_credit.py') - # Find the max addition that could be made without exceeding the - # marginal duration limit - pr_temp = pr[pr <= p] - dur_marg_test = dur_marg[len(pr_temp) - 1] - max_interp = ((p_step * (d1 - dur_marg_test)) - / (d1 - d)) + pr_temp[-1] - # Set the peaking potential to be the minimum of the crossover - # point and maximum interpolation value (limited by the marg - # duration and p_step size). - peak_stor.loc[d, 'peaking potential'] = min(max_interp, p) - p_base - - peak_stor['existing power'] = 0 - - # ---------------------------------------------------------------- - # 3) Prepare final output: duration and MW - # ---------------------------------------------------------------- - peak_stor['peaking potential'] = pd.to_numeric(peak_stor['peaking potential']) - result = ( - peak_stor[['peaking potential']] - .round(decimals=2) - .reset_index() - .rename(columns={'index': 'duration', 'peaking potential': 'MW'}) - ) - - return result diff --git a/ReEDS_Augur/diagnostic_plots.py b/ReEDS_Augur/diagnostic_plots.py deleted file mode 100644 index d0f0bbec..00000000 --- a/ReEDS_Augur/diagnostic_plots.py +++ /dev/null @@ -1,1358 +0,0 @@ -#%%### Imports -import os -import sys -import pandas as pd -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib import patheffects as pe -from glob import glob -import traceback -import gdxpds -import cmocean -### Local imports -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -from reeds import plots - -plots.plotparams() - - -#%%### Fixed inputs -dpi = None -interactive = False -savefig = True - - -#%%### Functions -def delete_temporary_files(sw): - """ - Delete temporary csv, pkl, and h5 files - """ - dropfiles = ( - glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.pkl")) - + glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.h5")) - + glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.csv")) - + glob(os.path.join(sw['casedir'],'ReEDS_Augur','PRAS',f"PRAS_{sw['t']}*.pras")) - ) - - for keyword in sw['keepfiles']: - dropfiles = [f for f in dropfiles if not os.path.basename(f).startswith(keyword)] - - for f in dropfiles: - os.remove(f) - -#%% Input-loading function -def get_inputs(sw): - ### Make savepath - sw['savepath'] = os.path.join(sw['casedir'], 'outputs', 'Augur_plots') - os.makedirs(sw['savepath'], exist_ok=True) - - ##### Load shared parameters - fulltimeindex = reeds.timeseries.get_timeindex(sw.resource_adequacy_years) - - h_dt_szn = ( - pd.read_csv(os.path.join(sw['casedir'], 'inputs_case', 'rep', 'h_dt_szn.csv')) - .assign(hr=(['hr{:>03}'.format(i+1) for i in range(sw['hoursperperiod'])] - * sw['periodsperyear'] * len(sw.resource_adequacy_years_list))) - .assign(datetime=fulltimeindex) - ) - h_dt_szn['d'] = h_dt_szn.datetime.dt.strftime('sy%Yd%j') - - gdxreeds = gdxpds.to_dataframes( - os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'reeds_data_{sw["t"]}.gdx')) - - techs = gdxreeds['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') - h2dac = techs['CONSUME'].dropna().index - - tech_map = pd.read_csv( - os.path.join(sw['reeds_path'],'postprocessing','bokehpivot','in','reeds2','tech_map.csv')) - - tech_map.raw = reeds.reedsplots.simplify_techs(tech_map.raw, display_level = 'diagnostics') - tech_map = tech_map.drop_duplicates().set_index('raw').display - - tech_style = pd.read_csv( - os.path.join(sw['reeds_path'],'postprocessing','bokehpivot','in','reeds2','tech_style.csv'), - index_col='order', - ).squeeze(1) - - hierarchy = reeds.io.get_hierarchy(sw.casedir) - - resources = pd.read_csv( - os.path.join(sw['casedir'],'inputs_case','resources.csv') - ).set_index('resource') - resources['tech'] = reeds.reedsplots.simplify_techs(resources.i, display_level = 'diagnostics') - resources['rb'] = resources.r - - ##### Hourly dispatch by month - ### Load and aggregate the VRE generation profiles by tech group - try: - vre_gen = reeds.io.read_file( - os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'pras_vre_gen_{sw.t}.h5'), - parse_timestamps=True, - ) - except FileNotFoundError: - vre_gen = None - - ### Get vre_gen by tech (only resource_adequacy_years) - vre_gen_usa = ( - vre_gen - .rename(columns=dict(zip(vre_gen.columns, vre_gen.columns.map(lambda x: x.split('|')[0])))) - .groupby(axis=1, level=0).sum() - .set_index(fulltimeindex) - ) - vre_gen_usa.columns = reeds.reedsplots.simplify_techs(vre_gen_usa.columns, display_level = 'diagnostics') - - if len(sw['resource_adequacy_years_list']) == 1: - vre_gen_usa = vre_gen_usa.xs(int(sw['resource_adequacy_years_list'][0]), level='year', axis=0) - - ### Get vre_gen summed over tech by BA (full 7 years) - vre_gen_r = ( - vre_gen - .rename(columns=dict(zip(vre_gen.columns, vre_gen.columns.map(lambda x: x.split('|')[1])))) - .groupby(axis=1, level=0).sum() - ) - - ### Load hourly demand - try: - load_r = pd.read_hdf( - os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data',f'load_{sw.t}.h5') - ) - load_r.index = fulltimeindex - except FileNotFoundError: - load_r = None - - ### Load PRAS load - try: - pras_load = reeds.io.read_file( - os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'pras_load_{sw.t}.h5'), - parse_timestamps=True, - ) - except FileNotFoundError: - pras_load = None - pras_load.index = fulltimeindex - try: - pras_h2dac_load = reeds.io.read_file( - os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data', - f"pras_h2dac_load_{sw['t']}.h5"), - parse_timestamps=True, - ) - except FileNotFoundError: - pras_h2dac_load = pd.DataFrame(columns=pras_load.columns) - pras_h2dac_load.index = fulltimeindex - - ### Load input capacity to PRAS - try: - max_cap = pd.read_csv( - os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data',f"max_cap_{sw['t']}.csv")) - max_cap.i = reeds.reedsplots.simplify_techs(max_cap.i, display_level = 'diagnostics') - except FileNotFoundError: - max_cap = pd.DataFrame(columns=['i','v','r','MW']) - - ### Load LOLE/EUE/NEUE from PRAS - try: - pras = reeds.io.read_pras_results( - os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', - f"PRAS_{sw.t}i{sw.iteration}.h5") - ) - pras.index = fulltimeindex - except FileNotFoundError as err: - print(f"Failed to load PRAS outputs: {err}") - pras = pd.DataFrame({'USA_EUE':9999}, index=fulltimeindex) - - ### Load the PRAS system - try: - pras_system = reeds.io.get_pras_system( - case=sw.casedir, year=sw.t, iteration=sw.iteration) - for key in pras_system: - pras_system[key].index = fulltimeindex - for key in ['gencap', 'storcap', 'genstorcap', 'genfailrate', 'genrepairrate']: - pras_system[key].columns = pras_system[key].columns.droplevel(['unit','name']) - if 'i' in pras_system[key].columns.names: - col_vals = pras_system[key].columns.get_level_values('i').unique().tolist() - mapping = dict(zip(col_vals, reeds.reedsplots.simplify_techs(col_vals)), display_level = 'diagnostics') - pras_system[key].rename(columns = mapping, level = 'i', inplace = True) - - except FileNotFoundError as err: - print(f"Failed to load .pras system: {err}") - pras_system = dict() - - ###### Get net load profiles - ### Get net load by BA - net_load_r = load_r - vre_gen_r - ### Get net load by ccreg - net_load_ccreg = net_load_r.rename(columns=hierarchy.ccreg).groupby(axis=1, level=0).sum() - ### Get net load for the USA - net_load_usa = net_load_r.set_index(fulltimeindex).sum(axis=1) - - ###### Make combined dataframes for plotting - ### Get top load hours by ccseason - datetime2ccseason = h_dt_szn.set_index('datetime').ccseason - ### Use seasons appropriate to resolution - ccseasons = h_dt_szn.ccseason.unique() - - ccregs = sorted(net_load_ccreg.columns) - peakhours = {} - for ccseason in ccseasons: - for ccreg in ccregs: - peakhours[ccseason,ccreg] = ( - net_load_ccreg.loc[net_load_ccreg.index.map(datetime2ccseason)==ccseason][ccreg] - .nlargest(int(sw['GSw_PRM_CapCreditHours'])) - .index - ) - - dfpeak = ( - pd.DataFrame(peakhours) - .stack(level=0) - .reorder_levels([1,0], axis=0) - .stack() - .rename('datetime').to_frame() - .assign(peak=1) - .reset_index(level=2).rename(columns={'level_2':'ccreg'}) - .pivot(index='datetime', columns='ccreg', values='peak') - .reindex(fulltimeindex) - .fillna(0).astype(int) - ) - - peakcolors = plots.rainbowmapper(ccregs, plt.cm.tab20) - - ###### Store them for later - dfs = {} - dfs['dfpeak'] = dfpeak - dfs['fulltimeindex'] = fulltimeindex - dfs['gdxreeds'] = gdxreeds - dfs['h_dt_szn'] = h_dt_szn - dfs['h2dac'] = h2dac - dfs['hierarchy'] = hierarchy - dfs['load_r'] = load_r - dfs['max_cap'] = max_cap - dfs['net_load_r'] = net_load_r - dfs['net_load_usa'] = net_load_usa - dfs['peakcolors'] = peakcolors - dfs['pras_h2dac_load'] = pras_h2dac_load - dfs['pras_load'] = pras_load - dfs['pras_system'] = pras_system - dfs['pras'] = pras - dfs['resources'] = resources - dfs['tech_map'] = tech_map - dfs['tech_style'] = tech_style - dfs['vre_gen_usa'] = vre_gen_usa - dfs['vre_gen'] = vre_gen - - return dfs - - -#%%### Plotting functions -def plot_netload_profile(sw, dfs): - """ - Net load profile - """ - for y in sw['resource_adequacy_years_list']: - savename = f"A-netload-profile-w{y}-{sw['t']}.png" - - dfpos = dfs['net_load_usa'].loc[str(y)].clip(lower=0) - dfneg = dfs['net_load_usa'].loc[str(y)].clip(upper=0) - plt.close() - f,ax = plots.plotyearbymonth( - dfpos, colors=['C3'], dpi=dpi, - ) - plots.plotyearbymonth( - dfneg, colors=['C0'], - f=f, ax=ax, - ) - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_dropped_load_timeseries_full(sw, dfs): - """ - Dropped load timeseries - """ - dropped = dfs['pras']['USA_EUE'].copy() - timeindex_y = pd.date_range( - f"{sw['t']}-01-01", f"{sw['t']+1}-01-01", inclusive='left', freq='H', - tz='Etc/GMT+6')[:8760] - savename = f"dropped_load-timeseries-wfull-{sw['t']}.png" - weatheryears = sw.resource_adequacy_years_list - plt.close() - f,ax = plt.subplots(len(weatheryears), 1, sharex=True, sharey=True, figsize=(13.33,5)) - for row, y in enumerate(weatheryears): - ax[row].fill_between( - timeindex_y, dropped.loc[str(y)].values/1e3, - lw=0.2, color='C3') - ### Formatting - ax[row].annotate( - y, (0.01,1), xycoords='axes fraction', - fontsize=14, weight='bold', va='top') - ### Formatting - ax[0].set_xlim( - pd.Timestamp(f"{sw['t']}-01-01 00:00-05:00"), - pd.Timestamp(f"{sw['t']}-12-31 23:59-05:00")) - ax[0].set_ylim(0) - ax[len(weatheryears)-1].set_ylabel('Dropped load [GW]', y=0, ha='left') - plots.despine(ax) - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_h2dac_load_timeseries(sw, dfs): - """ - H2 and DAC load timeseries - """ - - dfplot = dfs['pras_h2dac_load'].sum(axis=1) - if not dfplot.sum(): - return - - for y in sw['resource_adequacy_years_list']: - savename = f"h2dac_load-timeseries-w{y}-{sw['t']}.png" - - plt.close() - ## DAC and H2 demand - f,ax = plots.plotyearbymonth( - dfplot.loc[str(y)].rename('H2/DAC\ndemand').abs().to_frame(), - colors=['C9'], dpi=dpi, - ) - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_dropped_load_duration(sw, dfs): - """ - Dropped load duration - """ - dropped = dfs['pras']['USA_EUE'].copy() - - savename = f"dropped_load-duration-{sw['t']}.png" - plt.close() - f,ax = plt.subplots(dpi=dpi) - ## Dropped load - ax.fill_between( - range(len(dropped)), - dropped.rename('Dropped').sort_values(ascending=False).values/1e3, - color='C3', lw=0, label='Dropped', - ) - ## Mark the extrema - ax.plot( - [0], [dropped.max()/1000], - lw=0, marker=1, ms=5, c='C3',) - ax.plot( - [(dropped>0).sum()], [0], - lw=0, marker=2, ms=5, c='C3',) - ## Formatting - ax.set_ylabel('Demand [GW]') - ax.set_xlabel( - f"Hours of dispatch period ({min(sw['resource_adequacy_years_list'])}–" - f"{max(sw['resource_adequacy_years_list'])}) [h]") - ax.set_xlim(0, max((dropped>0).sum(), 1)) - ax.set_ylim(0) - ax.legend( - # loc='upper left', bbox_to_anchor=(1,1.25), - fontsize=11, frameon=True, ncol=1, - columnspacing=0.5, handletextpad=0.3, handlelength=0.7, - ) - plots.despine(ax) - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def map_dropped_load(sw, dfs, level='r'): - """ - Annual EUE and NEUE by ReEDS zone - """ - ### Get the inputs - dfmap = reeds.io.get_dfmap(sw['casedir']) - dfba = dfmap['r'] - dropped = dfs['pras'][ - [c for c in dfs['pras'] if c.endswith('_EUE') and (not c.startswith('USA'))] - ].copy() - dropped.columns = dropped.columns.map(lambda x: x.split('_')[0]) - units = { - ('EUE','max'): ('MW',1), ('EUE','mean'): ('MW',1), ('EUE','sum'): ('GWh',1e-3), - ('NEUE','max'): ('%',1e2), ('NEUE','sum'): ('ppm',1e6), - } - load = dfs['pras_load'] - - ### Plot it - for metric in ['EUE','NEUE']: - ## Aggregate if necessary - if level not in ['r','rb','ba']: - dropped = ( - dropped.rename(columns=dfs['hierarchy'][level]) - .groupby(level=0, axis=1).sum().copy() - ) - load = ( - load.rename(columns=dfs['hierarchy'][level]) - .groupby(level=0, axis=1).sum().copy() - ) - for agg in ['max','sum','mean']: - if (metric,agg) not in units: - continue - savename = f"dropped_load-map-{metric}_{agg}-{level}-{sw['t']}.png" - - dfplot = dfba.copy() - if level not in ['r','rb','ba']: - dfplot[level] = dfs['hierarchy'][level] - dfplot = dfplot.dissolve(level) - dfplot['centroid_x'] = dfplot.geometry.centroid.x - dfplot['centroid_y'] = dfplot.geometry.centroid.y - if metric == 'EUE': - dfplot['val'] = dropped.agg(agg) - elif (metric == 'NEUE') and (agg == 'max'): - dfplot['val'] = (dropped / load).agg(agg) - elif (metric == 'NEUE'): - dfplot['val'] = dropped.agg(agg) / load.agg(agg) - dfplot.val = (dfplot.val * units[metric,agg][1]).replace(0, np.nan) - - plt.close() - f,ax = plt.subplots(figsize=(8,8/1.45), dpi=150) - ### Background - dfba.plot(ax=ax, facecolor='none', edgecolor='k', lw=0.2) - ### Data - dfplot.plot(ax=ax, column='val', cmap=cmocean.cm.rain) - for r, row in dfplot.iterrows(): - if row.val > 0: - ax.annotate( - f'{row.val:,.0f} {units[metric,agg][0]}', - (row.centroid_x, row.centroid_y), - color='r', ha='center', va='top', fontsize=6, weight='bold') - ### Formatting - if level in ['r','rb','ba']: - for r, row in dfba.iterrows(): - ax.annotate(r, (row.centroid_x, row.centroid_y), - ha='center', va='bottom', fontsize=6, color='C7') - ax.axis('off') - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_pras_ICAP(sw, dfs): - """ - Plot the available capacity used in PRAS without any outages - """ - if not len(dfs['pras_system']): - print('PRAS system was not loaded') - return - ### Collect the PRAS system capacities - cap = pd.concat([ - dfs['pras_system']['gencap'].groupby(axis=1, level=0).sum(), - dfs['pras_system']['storcap'].groupby(axis=1, level=0).sum(), - dfs['pras_system']['genstorcap'].groupby(axis=1, level=0).sum(), - ], axis=1) - ## Drop any empties - cap = cap.replace(0,np.nan).dropna(axis=1, how='all').fillna(0).astype(int) - ## Get the colors - tech_style = dfs['tech_style']['color'].squeeze() - ## Aggregate by type - cap = cap.groupby(axis=1, level=0).sum() - order = [c for c in tech_style.index if c in cap] - cap = cap[order] - if cap.shape[1] != len(order): - raise Exception(f"missing colors: {cap.columns}, {tech_style}") - ## Cumulate for plot - cumcap = cap.cumsum(axis=1)[order[::-1]] - load = dfs['pras_system']['load'].sum(axis=1) - - ### Plot it - for y in sw.resource_adequacy_years_list: - savename = f"PRAS-ICAP-w{y}-{sw['t']}.png" - plt.close() - f,ax = plots.plotyearbymonth( - load.loc[str(y)], - colors=['k'], style='line', lwforline=1, - ) - plots.plotyearbymonth( - cumcap.loc[str(y)], colors=[tech_style[c] for c in cumcap], - f=f, ax=ax, - ) - ## Legend - leg = ax[0].legend( - loc='upper left', bbox_to_anchor=(1,1.25), - fontsize=11, frameon=False, ncol=1, - columnspacing=0.5, handletextpad=0.3, handlelength=0.7, - ) - leg.set_title(f'ICAP {y}', prop={'size':'large'}) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_augur_pras_capacity(sw, dfs): - """ - Plot the nameplate capacity from Augur and PRAS to check consistency - """ - if not len(dfs['pras_system']): - print('PRAS system was not loaded') - return - savename = f"PRAS-Augur-capacity-{sw['t']}.png" - ### Get the colors - tech_style = dfs['tech_style']['color'].squeeze() - ### Collect the PRAS system capacities - cap = {} - cap['pras'] = pd.concat([ - dfs['pras_system']['gencap'], - dfs['pras_system']['storcap'], - dfs['pras_system']['genstorcap'], - ], axis=1) / 1e3 - ## Drop any empties - cap['pras'] = cap['pras'].replace(0,np.nan).dropna(axis=1, how='all').fillna(0) - ## Aggregate by type - cap['pras'] = (cap['pras'] - .groupby(axis=1, level=[1,0]).sum().max().rename('MW') - ) - - ### Collect the Augur capacities - cap['augur'] = dfs['max_cap'].groupby(['i','r'], as_index=False).MW.sum() - ## Convert from s to p regions - cap['augur'].r = cap['augur'].r - ## Aggregate by type - cap['augur'] = (cap['augur'] - .replace({'i':{'Hydropower Existing':'Hydropower', 'Hydropower New':'Hydropower'}}) - .groupby(['r','i']).MW.sum() / 1e3 - ) - - ### Drop VRE since its capacity is handled differently - for datum in cap: - cap[datum] = ( - cap[datum].loc[ - ~cap[datum].index.get_level_values('i').isin( - dfs['vre_gen_usa'].columns.tolist()) - ] - ) - - ### Get coordinates - zones = dfs['hierarchy'].index - ncols = int(np.around(np.sqrt(len(zones)) * 1.618, 0)) - nrows = len(zones) // ncols + int(bool(len(zones) % ncols)) - coords = dict(zip(zones, [(row,col) for row in range(nrows) for col in range(ncols)])) - - ### Plot the capacities - plt.close() - f,ax = plt.subplots( - nrows, ncols, figsize=(1.2*ncols, 1.2*nrows), sharex=True, - gridspec_kw={'wspace':0.6, 'hspace':0.5} - ) - alltechs = set() - for r in zones: - df = pd.concat({'A':cap['augur'].get(r,pd.Series()), 'P':cap['pras'].get(r,pd.Series())}, axis=1).T - order = [c for c in tech_style.index if c in df] - missing = [c for c in df if c not in order] - if len(missing): - print(f'WARNING: Missing colors for these techs: {missing}') - df = df[order].copy() - alltechs.update(df.columns.tolist()) - plots.stackbar(df=df, ax=ax[coords[r]], colors=tech_style, net=False, width=0.8) - ### Formatting - ax[coords[r]].set_title(r) - ax[coords[r]].set_xticks([0,1]) - ax[coords[r]].set_xticklabels(['A','P']) - ### Legend - if r == zones[-1]: - handles = [ - mpl.patches.Patch(facecolor=tech_style[i], edgecolor='none', label=i) - for i in [j for j in tech_style.index if j in alltechs] - ][::-1] - ax[coords[r]].legend( - handles=handles, loc='center left', bbox_to_anchor=(1,0.5), - frameon=False, columnspacing=0.5, handletextpad=0.3, handlelength=0.7, - ncol=len(alltechs)//3, - ) - ### Formatting - plots.trim_subplots(ax=ax, nrows=nrows, ncols=ncols, nsubplots=len(zones)) - ax[-1, 0].set_ylabel('Nameplate capacity [GW]', y=0, ha='left') - plots.despine(ax) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_pras_ICAP_regional(sw, dfs, numdays=5): - """ - Plot the available capacity used in PRAS without any outages - """ - if not len(dfs['pras_system']): - print('PRAS system was not loaded') - return - ### Get the peak dropped-load periods - dropped = dfs['pras']['USA_EUE'].copy() - dropped = dropped.groupby( - [dropped.index.year, dropped.index.month, dropped.index.day] - ).sum().nlargest(numdays).replace(0,np.nan).dropna() - - ### Collect the PRAS system capacities - load = dfs['pras_system']['load'] / 1e3 - cap = pd.concat([ - dfs['pras_system']['gencap'], - dfs['pras_system']['storcap'], - dfs['pras_system']['genstorcap'], - ], axis=1) / 1e3 - ## Drop any empties - cap = cap.replace(0,np.nan).dropna(axis=1, how='all').fillna(0) - ## Get the colors - tech_style = dfs['tech_style']['color'].squeeze() - ## Aggregate by type - cap = cap.groupby(axis=1, level=[1,0]).sum() - - ### Get coordinates - zones = dfs['hierarchy'].index - ncols = int(np.around(np.sqrt(len(zones)) * 1.618, 0)) - nrows = len(zones) // ncols + int(bool(len(zones) % ncols)) - coords = dict(zip(zones, [(row,col) for row in range(nrows) for col in range(ncols)])) - - ### Plot the highest-EUE days - for day in range(len(dropped)): - date = '{}-{:>02}-{:>02}'.format(*dropped.index[day]) - savename = f"PRAS-ICAP-{sw['t']}-{date.replace('-','')}.png" - plt.close() - f,ax = plt.subplots( - nrows, ncols, figsize=(1.2*ncols, 1.2*nrows), sharex=True, - gridspec_kw={'wspace':0.6, 'hspace':0.5} - ) - for r in zones: - df = cap.loc[date][r].copy() - ### Cumulate for plot - order = [c for c in tech_style.index if c in df] - df = df[order] - ### Plot it - plots.stackbar( - df=df, ax=ax[coords[r]], colors=tech_style, net=False, align='edge', - width=pd.Timedelta('1H'), - ) - ax[coords[r]].plot( - load.loc[date].index, load.loc[date][r].values, c='k', lw=1, - path_effects=[pe.withStroke(linewidth=1.7, foreground='w', alpha=0.8)], - ) - ### Formatting - ax[coords[r]].set_title(r) - ### Formatting - plots.trim_subplots(ax=ax, nrows=nrows, ncols=ncols, nsubplots=len(zones)) - ax[coords[zones[0]]].set_xlim(df.index[0], df.index[-1] + pd.Timedelta('1H')) - ax[coords[zones[0]]].set_xticks([]) - ax[-1, 0].set_xlabel(date, x=0, ha='left', labelpad=10) - ax[-1, 0].set_ylabel('ICAP [GW]', y=0, ha='left') - plots.despine(ax) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_pras_unitsize_distribution(sw, dfs): - """ - Distribution of PRAS unit sizes by tech - """ - if not len(dfs['pras_system']): - print('PRAS system was not loaded') - return - savename = f"PRAS-unitcap-{sw['t']}.png" - cap = ( - pd.concat([ - dfs['pras_system']['gencap'], - dfs['pras_system']['storcap'], - dfs['pras_system']['genstorcap'], - ], axis=1) - .max().rename('MW').reset_index() - ) - ## Get the colors - tech_style = dfs['tech_style']['color'].squeeze() - ## Aggregate by type - techs = cap.i.unique() - nondisaggtechs = ( - dfs['vre_gen_usa'].columns.tolist() - + ['Hydropower Existing'] - + ['Battery'] - + ['Canadian Imports'] - ) - order = [i for i in tech_style.index if i in techs] - others = [i for i in techs if ((i not in order) and (i not in nondisaggtechs))] - # for i in others: - # tech_style[i] = 'k' - - ylabel = {0: {'scale':1, 'units':'MW'}, 1: {'scale':1e-3, 'units':'GW'}} - plt.close() - f,ax = plt.subplots(1, 2, figsize=(7,3.75), gridspec_kw={'wspace':0.4}) - for i in (order+others)[::-1]: - df = cap.loc[cap.i==i].copy() - col = 1 if i in nondisaggtechs else 0 - df.MW = df.MW * ylabel[col]['scale'] - ax[col].plot( - range(len(df)), df.MW.sort_values().values, - c=tech_style.get(i,'k'), label=i) - ax[col].annotate( - f' {i}', (len(df)-1, df.MW.max()), - fontsize=10, color=tech_style.get(i,'k'), - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.7)], - ) - for col in range(2): - ax[col].set_ylabel(f"Unit size [{ylabel[col]['units']}]") - ax[col].set_xlabel('Number of units') - ax[col].set_xlim(0) - ax[col].set_ylim(0) - ax[col].legend( - ncol=1, columnspacing=0.5, frameon=False, - handletextpad=0.3, handlelength=0.7, - loc='upper center', bbox_to_anchor=(0.5,-0.2)) - ax[0].set_title('Disaggregated techs') - ax[1].set_title('Aggregated techs (PRAS FOR=0)') - plots.despine(ax) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_pras_unitnumber(sw, dfs, level='country'): - """Number of PRAS units by technology, grouped by region level""" - if not len(dfs['pras_system']): - print('PRAS system was not loaded') - return - savename = f"PRAS-unitnumber-{level}-{sw['t']}.png" - cap = ( - pd.concat([ - dfs['pras_system']['gencap'], - dfs['pras_system']['storcap'], - dfs['pras_system']['genstorcap'], - ], axis=1) - .max().rename('MW').reset_index() - ) - ## Get the colors - tech_style = dfs['tech_style']['color'].squeeze() - ## Aggregate by type - order = cap.i.value_counts().index - colors = order.map(lambda x: tech_style.get(x, mpl.colors.to_hex('k'))) - - ## Group by hierarchy level - regions = sorted(( - dfs['hierarchy'].index if level == 'r' else dfs['hierarchy'][level] - ).unique()) - nrows, ncols, coords = plots.get_coordinates(regions) - - plt.close() - f,ax = plt.subplots( - nrows, ncols, figsize=(max(ncols*2, 5), max(nrows*2, 3.75)), - sharex=True, sharey=True, - ) - for region in regions: - _ax = (ax if nrows == ncols == 1 else ax[coords[region]]) - rs = ( - [region] if level == 'r' - else dfs['hierarchy'].loc[dfs['hierarchy'][level]==region].index - ) - df = cap.loc[cap.r.isin(rs)].i.value_counts().reindex(order).fillna(0) - _ax.bar( - df.index, df.values, color=colors, - ) - _ax.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) - _ax.set_xticks(range(len(order))) - _ax.set_xticklabels(order, rotation=90, fontsize=9) - _ax.set_xlim(-0.5, len(order)-0.5) - _ax.annotate( - region.replace('_','\n') + f'\n{df.sum():.0f}', - (0.95,0.95), xycoords='axes fraction', ha='right', va='top', - fontsize='large', weight='bold', - ) - labelax = ( - ax if (nrows == ncols == 1) - else ax[0] if ((nrows == 1) or (ncols == 1)) - else ax[-1,0] - ) - labelax.set_ylabel('Number of units', y=0, ha='left') - plots.despine(ax) - plots.trim_subplots(ax, nrows, ncols, len(regions)) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_pras_load_units(sw, dfs): - """Histogram of net load and sorted unit sizes by model zone""" - savename = f"PRAS-load_hist-units-{sw['t']}.png" - ### Get inputs - ## Capacity - cap = ( - pd.concat([ - dfs['pras_system']['gencap'], - dfs['pras_system']['storcap'], - dfs['pras_system']['genstorcap'], - ], axis=1) - .max().rename('MW').reset_index() - ) - ## Net demand - vre_gen = dfs['vre_gen'].copy() - vre_gen.columns = pd.MultiIndex.from_tuples( - vre_gen.columns.map(lambda x: tuple(x.split('|'))), - names=['i','r'], - ) - net_demand = (dfs['pras_system']['load'] - vre_gen.groupby('r', axis=1).sum()) / 1e3 - ## Remaining unit capacity - units = cap.loc[ - ~cap.i.isin( - reeds.reedsplots.simplify_techs(vre_gen.columns.get_level_values('i').unique(), display_level = 'diagnostics')) - & (cap.MW > 0) - ] - ## Get the colors - tech_style = dfs['tech_style']['color'].squeeze() - ## Only keep the 10 regions with highest neue - regions = ( - dfs['pras'][[c for c in dfs['pras'] if c.endswith('_EUE') and not c.startswith('USA')]] - .sum() - .nlargest(10) - ).index.map(lambda x: x[:-len('_EUE')]) - - ## Maps - dfmap = reeds.io.get_dfmap(sw['casedir']) - - ### Plot it - ncols = len(regions) - nrows = 3 - plt.close() - f,ax = plt.subplots( - nrows, ncols, figsize=(ncols*1.25, 6), - gridspec_kw={'height_ratios':[1,1,3], 'hspace':0.3}, - ) - for col, r in enumerate(regions): - ## Net load - ax[1,col].hist(net_demand[r], bins=101, color='C3') - ## Units - df = units.loc[units.r==r].sort_values('MW') - df['GW'] = df.MW / 1e3 - df['GW_cumsum'] = df.GW.cumsum() - df['left'] = df['GW_cumsum'].shift(1).fillna(0) - ## Do it twice to get darker lines around the edges - ax[-1,col].bar( - x=df['left'], - height=df['MW'], - width=df['GW'], - align='edge', - color=df.i.map(lambda x: tech_style.get(x,'#000000')), - alpha=0.7, - ) - ax[-1,col].bar( - x=df['left'], - height=df['MW'], - width=df['GW'], - align='edge', - color='none', - edgecolor='k', lw=0.15, - ) - ## Peak - peak = net_demand[r].max() - for row in [1, 2]: - ax[row,col].axvline(peak, c='C3', lw=0.75, ls=':') - ## Formatting - reeds.plots.despine(ax[1,col], left=False) - ax[1,col].set_yticks([]) - ax[1,col].set_xticklabels([]) - ## Ignore battery and hydro for the y limit since they're not disaggregated - ax[-1,col].set_ylim(0, units.loc[~units.i.str.startswith(('Battery','Hydro')), 'MW'].max()) - if col > 0: - ax[-1,col].set_yticklabels([]) - ## Share x axis for histograms - xmax = max(ax[1,col].get_xlim()[1], ax[2,col].get_xlim()[1]) - for row in [1, 2]: - ax[row,col].set_xlim(0, xmax) - ## Maps - dfmap['r'].loc[[r]].plot(ax=ax[0,col], facecolor='k', edgecolor='none', zorder=3) - bounds = dfmap['r'].loc[[r]].bounds.squeeze() - ax[0,col].set_xlim(bounds.minx-50e3, bounds.maxx+50e3) - ax[0,col].set_ylim(bounds.miny-50e3, bounds.maxy+50e3) - dfmap['st'].plot(ax=ax[0,col], facecolor='none', edgecolor='k', lw=0.5, zorder=2) - dfmap['r'].plot(ax=ax[0,col], facecolor='0.9', edgecolor='w', lw=0.5, zorder=1) - ax[0,col].axis('off') - ax[1,col].set_title(r) - ## Formatting - ax[-1,0].set_xlabel('GW') - ax[1,0].set_xlabel('Net load [GW]', x=0, ha='left', color='C3') - ax[-1,0].set_ylabel('Unit size [MW]') - ax[-1,0].set_xlabel('Installed capacity [GW]', x=0, ha='left') - reeds.plots.despine(ax) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_pras_augur_load(sw, dfs): - """PRAS load against Augur load""" - dfpras = dfs['pras_system']['load'].sum(axis=1).rename('PRAS') - dfaugur = dfs['load_r'].set_axis(dfpras.index).sum(axis=1).rename('Augur') - years = dfpras.index.year.unique() - linecolors = {'Augur':'C0', 'PRAS':'C3'} - for year in years: - savename = f"demand_USA-Augur-PRAS-w{year}-{sw['t']}.png" - plt.close() - f,ax = plots.plotyearbymonth( - dfaugur.loc[str(year)], style='line', colors=linecolors['Augur']) - plots.plotyearbymonth( - dfpras.loc[str(year)], style='line', colors=linecolors['PRAS'], f=f, ax=ax) - ## Legend - handles = [ - mpl.lines.Line2D([], [], color=linecolors[i], label=i, lw=2) - for i in linecolors - ] - ax[0].legend( - handles=handles, ncol=2, frameon=False, - loc='lower right', bbox_to_anchor=(1,0.5), - handlelength=1.0, handletextpad=0.3, labelspacing=0.5, columnspacing=0.5, - ) - ax[0].annotate(year, (0.85, 1), xycoords='axes fraction', ha='right') - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_pras_load(sw, dfs): - """PRAS load over all weather years""" - dfpras = dfs['pras_system']['load'].sum(axis=1).rename('PRAS') - years = dfpras.index.year.unique() - linecolors = plots.rainbowmapper(years, categorical=True) - savename = f"demand_USA-PRAS-{sw['t']}.png" - plt.close() - f,ax = plots.plotyearbymonth( - dfpras.loc[str(years[0])], style='line', colors=linecolors[years[0]]) - for year in years[1:]: - plots.plotyearbymonth( - dfpras.loc[str(year)], style='line', colors=linecolors[year], f=f, ax=ax) - ## Legend - handles = [ - mpl.lines.Line2D([], [], color=linecolors[y], label=y, lw=2) - for y in years - ] - ax[0].legend( - handles=handles, ncol=len(years), frameon=False, - loc='lower right', bbox_to_anchor=(1,0.5), - handlelength=1.0, handletextpad=0.3, labelspacing=0.5, columnspacing=0.5, - ) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def map_pras_failure_rate(sw, dfs, aggfunc='mean', repair=False): - """ - Failure rates from PRAS, indicating the probability that an online unit will fail - (which is different from the outage rate, which gives the average fraction of units - that are on outage over time, including mean time to repair).""" - dfmap = reeds.io.get_dfmap(sw['casedir']) - dfzones = dfmap['r'] - - failrate = ( - dfs['pras_system']['genfailrate'] - ## Only keep one copy of each (i,r) - .T.reset_index().drop_duplicates().set_index(['i','r']).T - * 100 - ) - failrate.index = dfs['pras_system']['genfailrate'].index - - failsum = failrate.sum() - plottechs = failsum.loc[failsum != 0].index.get_level_values('i').unique() - - for tech in plottechs: - savename = f"hourly_failure_rate-year,month-{aggfunc}-{tech.replace('-','')}-{sw['t']}" - plt.close() - f, ax = plots.map_years_months( - dfzones=dfzones, dfdata=failrate[tech], - title=f"Monthly {aggfunc}\nhourly failure rate,\n{tech} [%]", - ) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - if repair: - repairrate = ( - dfs['pras_system']['genrepairrate'] - .T.reset_index().drop_duplicates().set_index(['i','r']).T - * 100 - ) - repairrate.index = dfs['pras_system']['genrepairrate'].index - for tech in plottechs: - savename = f"hourly_repair_rate-year,month-{aggfunc}-{tech.replace('-','')}-{sw['t']}" - plt.close() - f, ax = plots.map_years_months( - dfzones=dfzones, dfdata=repairrate[tech], - title=f"Monthly {aggfunc}\nhourly repair rate,\n{tech} [%]", - ) - ## Save it - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_cc_mar(sw, dfs): - """ - Marginal capacity credit - """ - param = 'cc_mar' - savename = f"{param}-{sw['t']}.png" - - if not int(sw['GSw_PRM_CapCredit']): - raise KeyError('No capacity credit values to plot') - cc_results = gdxpds.to_dataframes(os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data', - 'ReEDS_Augur_{}.gdx'.format(sw['t']) - )) - - dfplot = cc_results[param].drop('t',axis=1).copy() - dfplot['tech'] = dfplot.i.map(lambda x: x.split('_')[0]) - techs = sorted(dfplot.tech.unique()) - numcols = len(techs) - bootstrap = 5 - squeeze = 0.7 - ### Use seasons appropriate to resolution - ccseasons = ['cold', 'hot'] - histcolor = ['C0', 'C1'] - xticklabels = ccseasons - - plt.close() - f,ax = plt.subplots( - 1, numcols, figsize=(len(techs)*1.2, 3.75), sharex=True, sharey=True) - for row, tech in enumerate(techs): - df = dfplot.loc[(dfplot.tech==tech)].copy() - ### Each observation in the histogram is a (i,r) pair - df['i_r'] = df.i + '_' + df.r - df = df.pivot(columns='ccseason',values='Value',index='i_r')[ccseasons] - - plots.plotquarthist( - ax[row], df, histcolor=histcolor, - bootstrap=bootstrap, density=True, squeeze=squeeze, - pad=0.03, - ) - - ### Formatting - ax[row].set_title(tech) - ax[row].yaxis.set_minor_locator(mpl.ticker.MultipleLocator(0.1)) - ax[row].yaxis.set_major_locator(mpl.ticker.MultipleLocator(0.2)) - ax[row].set_ylim(0,1) - ax[row].set_xticklabels(xticklabels, rotation=90) - - ax[0].set_ylabel( - f'{sw.t} {param} [fraction]', weight='bold', fontsize='x-large') - - plots.despine(ax) - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_netloadhours_timeseries(sw, dfs): - """ - Peak net load hours by ccreg - """ - savename = f"netloadhours-timeseries-{sw['t']}.png" - - ### Plot it - years = sw.resource_adequacy_years_list - ccregs = sorted(dfs['dfpeak'].columns) - plt.close() - f,ax = plt.subplots(len(years),1,sharex=False,sharey=True,figsize=(12,6)) - for row, year in enumerate(years): - df = dfs['dfpeak'].loc[str(year)].cumsum(axis=1)[ccregs[::-1]] - df.plot.area( - ax=ax[row], color=dfs['peakcolors'], stacked=False, alpha=1, - legend=False, - ) - ax[row].annotate( - year,(0.005,0.95),xycoords='axes fraction',ha='left',va='top', - weight='bold',fontsize='large') - if row < (len(years) - 1): - ax[row].set_xticklabels([]) - handles, labels = ax[0].get_legend_handles_labels() - ax[0].legend( - handles[::-1], labels[::-1], - columnspacing=0.5, handletextpad=0.3, handlelength=0.7, - loc='upper left', bbox_to_anchor=(1,1), frameon=False, title='ccreg', - ) - ax[3].set_ylabel('Net load peak instances [#]') - # ax[-1].xaxis.set_major_locator(mpl.dates.MonthLocator()) - # ax[-1].xaxis.set_major_formatter(mpl.dates.DateFormatter('%b')) - plots.despine(ax) - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_netloadhours_histogram(sw, dfs): - """ - histograms of peak net load occurrence - """ - savename = f"netloadhours-histogram-{sw['t']}.png" - - ### Plot it - years = sw.resource_adequacy_years_list - ccregs = sorted(dfs['dfpeak'].columns) - plt.close() - f,ax = plt.subplots(1,3,figsize=(12,3.75)) - ### hour - dfs['dfpeak'].groupby(dfs['dfpeak'].index.hour).sum()[ccregs[::-1]].plot.bar( - ax=ax[0], color=dfs['peakcolors'], stacked=True, alpha=1, - width=0.95, legend=False, - ) - ax[0].set_xlabel('Hour [CST]') - ax[0].xaxis.set_major_locator(mpl.ticker.MultipleLocator(4)) - ax[0].xaxis.set_minor_locator(mpl.ticker.MultipleLocator(1)) - ax[0].tick_params(labelrotation=0) - ### Month - dfs['dfpeak'].groupby(dfs['dfpeak'].index.month).sum()[ccregs[::-1]].plot.bar( - ax=ax[1], color=dfs['peakcolors'], stacked=True, alpha=1, - width=0.95, legend=False, - ) - ax[1].set_xlabel('Month') - ax[1].tick_params(labelrotation=0) - ### Year - dfs['dfpeak'].groupby(dfs['dfpeak'].index.year).sum()[ccregs[::-1]].plot.bar( - ax=ax[2], color=dfs['peakcolors'], stacked=True, alpha=1, - width=0.95, legend=False, - ) - ax[2].set_xlabel('Year') - ax[2].set_xticks(range(len(years))) - ax[2].set_xticklabels( - years, - rotation=35, ha='right', rotation_mode='anchor') - # ax[2].tick_params(labelrotation=45) - ### Formatting - handles, labels = ax[2].get_legend_handles_labels() - ax[2].legend( - handles[::-1], labels[::-1], - loc='center left', bbox_to_anchor=(1,0.5), frameon=False, - title=sw['capcredit_hierarchy_level'], - ncol=1, columnspacing=0.5, handletextpad=0.3, handlelength=0.7, - ) - ax[0].set_ylabel('Net peak load instances [#]') - plots.despine(ax) - if savefig: - plt.savefig(os.path.join(sw['savepath'],savename)) - if interactive: - plt.show() - plt.close() - - -def plot_stressors(sw, dfs): - """ - Map demand/CF/FOR (organized differently to allow use outside of Augur) - """ - for iteration in range(sw['iteration']): - plot_generator = reeds.reedsplots.map_stressors( - case=sw['casedir'], t=sw['t'], iteration=iteration, - seed=(True if t == int(sw['endyear']) else False), - ) - while True: - try: - f, ax, df, plotlabel = next(plot_generator) - savename = ( - f"stress{t}i{iteration}-" - + plotlabel.split(':')[0].replace('-','') - ) - if savefig: - plt.savefig(os.path.join(sw.casedir, 'outputs', 'figures', f'{savename}.png')) - if interactive: - plt.show() - except StopIteration: - break - - -#%%### Main function -def main(sw, debug=False): - """ - debug: Make more plots for debugging if set to True - """ - #%% Get the inputs - dfs = get_inputs(sw) - - #%% Make the plots - if (not int(sw.GSw_PRM_CapCredit)) or (int(sw.pras == 2)): - try: - plot_pras_ICAP_regional(sw, dfs) - except Exception: - print('plot_pras_ICAP_regional() failed:', traceback.format_exc()) - - try: - plot_pras_load_units(sw, dfs) - except Exception: - print('plot_pras_load_units() failed:', traceback.format_exc()) - - try: - plot_pras_unitsize_distribution(sw, dfs) - except Exception: - print('plot_pras_unitsize_distribution() failed:', traceback.format_exc()) - - try: - for level in ['country', 'transgrp', 'r']: - plot_pras_unitnumber(sw, dfs, level) - except Exception: - print('plot_pras_unitnumber() failed:', traceback.format_exc()) - - try: - plot_augur_pras_capacity(sw, dfs) - except Exception: - print('plot_augur_pras_capacity() failed:', traceback.format_exc()) - - try: - plot_pras_load(sw, dfs) - except Exception: - print('plot_pras_load() failed:', traceback.format_exc()) - - try: - plot_stressors(sw, dfs) - except Exception: - print('plot_stressors() failed:', traceback.format_exc()) - - try: - plot_dropped_load_timeseries_full(sw, dfs) - except Exception: - print('plot_dropped_load_timeseries_full() failed:', traceback.format_exc()) - - try: - plot_dropped_load_duration(sw, dfs) - except Exception: - print('plot_dropped_load_duration() failed:', traceback.format_exc()) - - try: - for level in ['r', 'transgrp']: - map_dropped_load(sw, dfs, level=level) - except Exception: - print('map_dropped_load() failed:', traceback.format_exc()) - - if int(sw['GSw_PRM_CapCredit']): - try: - plot_cc_mar(sw, dfs) - except Exception: - print('plot_cc_mar() failed:', traceback.format_exc()) - - try: - plot_netloadhours_timeseries(sw, dfs) - except Exception: - print('plot_netloadhours_timeseries() failed:', traceback.format_exc()) - - try: - plot_netloadhours_histogram(sw, dfs) - except Exception: - print('plot_netloadhours_histogram() failed:', traceback.format_exc()) - - if debug: - try: - plot_pras_augur_load(sw, dfs) - except Exception: - print('plot_pras_augur_load() failed:', traceback.format_exc()) - - try: - plot_pras_ICAP(sw, dfs) - except Exception: - print('plot_pras_ICAP() failed:', traceback.format_exc()) - - try: - plot_netload_profile(sw, dfs) - except Exception: - print('plot_netload_profile() failed:', traceback.format_exc()) - - try: - map_pras_failure_rate(sw, dfs) - except Exception: - print('map_pras_failure_rate() failed:', traceback.format_exc()) - - -#%%### PROCEDURE -if __name__ == '__main__': - #%%### ARGUMENT INPUTS - import argparse - parser = argparse.ArgumentParser( - description='Create the necessary 8760 and capacity factor data for hourly resolution') - parser.add_argument('--reeds_path', help='ReEDS-2.0 directory') - parser.add_argument('--casedir', help='ReEDS-2.0/runs/{case} directory') - parser.add_argument('--t', type=int, default=2050, help='solve year to plot') - parser.add_argument('--iteration', '-i', type=int, default=-1, - help='iteration to plot (default of -1 means latest iteration)') - parser.add_argument('--debug', '-d', action='store_true', help='Make more plots') - - args = parser.parse_args() - reeds_path = args.reeds_path - casedir = args.casedir - t = args.t - iteration = args.iteration - debug = args.debug - - # #%%### Inputs for debugging - # reeds_path = reeds.io.reeds_path - # casedir = os.path.join(reeds_path, 'runs', 'v20251111_15M0_Pacific') - # t = 2026 - # interactive = True - # iteration = 0 - # debug = True - - #%%### INPUTS - ### Switches - sw = reeds.io.get_switches(casedir) - sw['t'] = t - ## Debugging - # sw['reeds_path'] = reeds_path - # sw['casedir'] = casedir - - ### Run for the latest iteration - if iteration < 0: - sw['iteration'] = int( - os.path.splitext( - sorted(glob(os.path.join(sw.casedir,'lstfiles',f'*{sw.t}i*.lst')))[-1] - )[0] - .split(f'{sw.t}i')[-1] - ) - else: - sw['iteration'] = iteration - - ### Make the plots - print('plotting intermediate Augur results...') - try: - main(sw, debug) - except Exception as _err: - print('diagnostic_plots.py failed with the following exception:') - print(traceback.format_exc()) - - ### Remove intermediate csv files to save drive space - if (not int(sw['keep_augur_files'])) and (not int(sw['debug'])): - delete_temporary_files(sw) diff --git a/ReEDS_Augur/prep_data.py b/ReEDS_Augur/prep_data.py deleted file mode 100644 index 8f4febfc..00000000 --- a/ReEDS_Augur/prep_data.py +++ /dev/null @@ -1,588 +0,0 @@ -#%% Notes -""" -This script writes inputs for resource adequacy calculations: -* capacity_credit.py (existing and marginal capacity credit) -* run_pras.jl -> ReEDS2PRAS.jl -> PRAS.jl (probabilistic resource adequacy) - -The files used by PRAS are: -* In {case}/ReEDS_Augur/augur_data: - * cap_converter_{year}.csv - * energy_cap_{year}.csv - * max_cap_{year}.csv - * pras_load_{year}.h5 - * pras_vre_gen_{year}.h5 - * tran_cap_{year}.csv -* In {case}/inputs_case: - * hydcf.csv - * outage_forced_hourly.h5 - * outage_forced_static.csv - * resources.csv - * tech-subset-table.csv - * unitdata.csv - * unitsize.csv -""" - -#%% General imports -import os -import re -import pandas as pd -import numpy as np -import gdxpds -### Local imports -import reeds - - -### Functions -def errorcheck_reeds2pras(casedir, csvout, h5out): - ### In ReEDS2PRAS, two classes of technologies are handled separately: - ### - Technologies in pras_vre_gen: - ### - Capacity is defined as the maximum of the hourly generation profile - ### - Capacity is not disaggregated into units and no outages are applied - ### - Technologies in max_cap: - ### - Capacity is taken from max_cap and disaggregated into units - ### - Unit outages are applied - ### - (except for batteries and dispatchable hydro, which are not disaggregated and - ### for which outages are not applied) - ### So here, to avoid double-counting, make sure the techs in pras_vre_gen and max_cap - ### do not overlap - profile_techs = h5out['pras_vre_gen'].columns.map(lambda x: x.split('|')[0]).unique() - max_cap_techs = csvout['max_cap'].index.get_level_values('i').unique() - for check in [ - [i for i in profile_techs if i in max_cap_techs], - [i for i in max_cap_techs if i in profile_techs], - ]: - if len(check): - raise ValueError(f'{check} overlap between pras_vre_gen and max_cap') - - ### ReEDS2PRAS takes the region list from the load data, so make sure all regions - ### with generation/transmission capacity show up in the load data - load_regions = h5out['pras_load'].columns - profile_regions = h5out['pras_vre_gen'].columns.map(lambda x: x.split('|')[1]).unique() - max_cap_regions = csvout['max_cap'].index.get_level_values('r').unique() - tran_regions = ( - list(csvout['tran_cap'].index.get_level_values('r').unique()) - + list(csvout['tran_cap'].index.get_level_values('rr').unique()) - ) - energy_regions = csvout['energy_cap'].index.get_level_values('r').unique() - converter_regions = csvout['cap_converter'].index - for check in [ - [r for r in profile_regions if r not in load_regions], - [r for r in max_cap_regions if r not in load_regions], - [r for r in tran_regions if r not in load_regions], - [r for r in energy_regions if r not in load_regions], - [r for r in converter_regions if r not in load_regions], - ]: - if len(check): - raise ValueError(f'{check} are not in pras_load') - - ### Make sure there are no missing values in data sent to ReEDS2PRAS - for key in ['pras_load', 'pras_vre_gen']: - if h5out[key].isnull().sum().sum() > 0: - missing_data_cols = [i for i in h5out[key] if h5out[key].isnull().sum(axis=0) > 0] - print(missing_data_cols) - raise ValueError(f'{key} has NaN values in {len(missing_data_cols)} columns') - - ### Make sure disaggregated techs have unit sizes, forced outage rates, and MTTRs - unitsize = pd.read_csv( - os.path.join(casedir, 'inputs_case', 'unitsize.csv'), - index_col='tech', - )['MW'] - mttr = pd.read_csv(os.path.join(casedir, 'inputs_case', 'mttr.csv'), index_col='tech') - outage_forced = reeds.io.get_outage_hourly(casedir, 'forced') - outage_techs = outage_forced.columns.get_level_values('i').unique() - ## ReEDS2PRAS does not disaggregate batteries - battery_techs = reeds.techs.get_tech_subset_table(casedir).loc[['BATTERY']].tolist() - for check, infile in [ - ([i for i in max_cap_techs if i not in unitsize.index.tolist() + battery_techs], 'unitsize.csv'), - ([i for i in max_cap_techs if i not in mttr.index], 'mttr.csv'), - ([i for i in max_cap_techs if i not in outage_techs], 'outage_forced_hourly.h5'), - ]: - if len(check): - raise ValueError(f'{check} are missing from {infile}') - - -#%%### Procedure -def main(t, casedir, iteration=0): - #%%### DEBUGGING: Inputs - # t = 2020 - # reeds_path = os.path.expanduser('~/github2/ReEDS-2.0') - # casedir = os.path.join(reeds_path,'runs','v20230214_PRMaugurM0_Pacific_d7fIrh4_CC_y2012') - - #%%### Get inputs from ReEDS - gdx_file = os.path.join(casedir,'ReEDS_Augur','augur_data',f'reeds_data_{t}.gdx') - gdxreeds = gdxpds.to_dataframes(gdx_file) - ### Use indices as multiindex - for key in gdxreeds: - # try: - if 'i' in gdxreeds[key]: - gdxreeds[key].i = gdxreeds[key].i.str.lower() - if 'ii' in gdxreeds[key]: - gdxreeds[key].ii = gdxreeds[key].ii.str.lower() - if 't' in gdxreeds[key]: - gdxreeds[key].t = gdxreeds[key].t.astype(int) - - #%% Load other inputs from ReEDS - inputs_case = os.path.join(casedir, 'inputs_case') - - sw = reeds.io.get_switches(casedir) - sw['t'] = t - - h_dt_szn = pd.read_csv(os.path.join(inputs_case, 'rep', 'h_dt_szn.csv')) - hmap_allyrs = pd.read_csv( - os.path.join(inputs_case, 'rep', 'hmap_allyrs.csv'), - low_memory=False, - ) - hmap_allyrs['szn'] = h_dt_szn['season'].copy() - - hmap_myr_stress = pd.read_csv( - os.path.join(inputs_case, f'stress{t}i{iteration}', 'hmap_myr.csv'), - low_memory=False, - index_col='*timestamp', - parse_dates=True, - ).rename_axis('timestamp') - - h_dt_szn = h_dt_szn.set_index(['year', 'hour']) - # Add explicit timestamp index - h_dt_szn['timestamp'] = pd.to_datetime( - h_dt_szn.index.map(hmap_allyrs.set_index(['year', 'hour'])['*timestamp'])) - h_dt_szn = h_dt_szn.reset_index().set_index('timestamp') - - load = reeds.io.read_file(os.path.join(inputs_case, 'load.h5'), parse_timestamps=True) - - resources = pd.read_csv(os.path.join(inputs_case, 'resources.csv')) - recf = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) - recf.columns = pd.MultiIndex.from_tuples([tuple(x.split('|')) for x in recf.columns], - names=('i','r')) - - tech_subset_table = reeds.techs.expand_GAMS_tech_groups( - reeds.techs.get_tech_subset_table(casedir).reset_index() - ).set_index('tech_group').i - - techs_vre = tech_subset_table.loc[['VRE', 'CSP', 'PVB']].unique() - if int(sw.pras_vre_combine): - ## Group all into a single "vre" tech - techs_vre_simplify = dict(zip( - techs_vre, - ['vre']*len(techs_vre) - )) - else: - ## Strip the resource class but keep resource type; - ## e.g. "upv_5" -> "upv", "csp2_3" -> "csp" - techs_vre_simplify = dict(zip( - techs_vre, - [re.sub('\d?_\d+$', '', i) for i in techs_vre] - )) - - try: - offshore = pd.read_csv( - os.path.join(casedir, 'inputs_case', 'offshore.csv'), - header=None, - ).squeeze(1).tolist() - except pd.errors.EmptyDataError: - offshore = [] - - #%%### Set up the output containers and a few other inputs - csvout, h5out = {}, {} - - #%%### Transmission routes, capacity, and losses - if int(sw.pras_trans_contingency): - trancap_reeds = gdxreeds['cap_trans_prm'] - else: - trancap_reeds = gdxreeds['cap_trans_energy'] - - #%%### Efficiencies and storage parameters - duration = gdxreeds['storage_duration'].loc[ - gdxreeds['storage_duration'].i.isin(gdxreeds['storage_standalone'].i) - ].set_index('i').Value - - - #%%### Nameplate capacity - cap_ivr_realvint = ( - gdxreeds['cap_ivrt'].loc[gdxreeds['cap_ivrt'].t==t].drop('t', axis=1) - .groupby(['i','v','r'], as_index=False).Value.sum() - ) - ### Reset the vintages of all storage units to 'new1' to reduce model size - cap_storage_devint = cap_ivr_realvint.loc[ - cap_ivr_realvint.i.isin(gdxreeds['storage_standalone'].i)].copy() - cap_storage_devint['v'] = 'new1' - cap_storage_devint = ( - cap_storage_devint.groupby(['i','v','r'], as_index=False).Value.sum()) - - def _devint_storage(dfin): - dfout = pd.concat([ - dfin.loc[~dfin.i.isin(gdxreeds['storage_standalone'].i)], - cap_storage_devint - ], axis=0) - return dfout - - cap_ivr = _devint_storage(cap_ivr_realvint) - - #%%### Nameplate energy capacity - cap_energy_ivr = ( - gdxreeds['cap_energy_ivrt'].loc[gdxreeds['cap_energy_ivrt'].t==t].drop('t', axis=1) - .groupby(['i','v','r'], as_index=False).Value.sum() - ) - ### Reset the vintages of all storage energy capacity units to 'new1' as well - cap_energy_ivr_devint = cap_energy_ivr.loc[ - cap_energy_ivr.i.isin(gdxreeds['storage_standalone'].i)].copy() - cap_energy_ivr_devint['v'] = 'new1' - cap_energy_ivr_devint = ( - cap_energy_ivr_devint.groupby(['i','v','r'], as_index=False).Value.sum()) - cap_energy_ivr = pd.concat([ - cap_energy_ivr.loc[~cap_energy_ivr.i.isin(gdxreeds['storage_standalone'].i)], - cap_energy_ivr_devint - ], axis=0) - - #%% Remove VRE and demand-modifying techs (H2 production, DAC, DR) - demand_techs = tech_subset_table[['CONSUME', 'DR_SHED']].values - cap_nonloadtechs = cap_ivr.loc[~cap_ivr.i.isin(demand_techs)].copy() - - vretechs_i = resources.i.str.lower().unique() - cap_vre = ( - cap_ivr.loc[cap_ivr.i.str.lower().isin(vretechs_i)] - .set_index(['i','v','r']).Value.copy() - ) - - #%%### VRE generation, accounting for generation - ### Apply CF adjustment to capacity (the resulting df is not meaningful but it's only a step - ### toward the generation df). - ### Some RE techs, like CSP, don't have cf_adj_t defined in all years, - ### so fill missing values with 1, then drop rows with missing region (indicating no capacity) - cf_adj_iv = ( - gdxreeds['cf_adj_t_filt'].loc[gdxreeds['cf_adj_t_filt'].t==t].drop('t', axis=1) - .set_index(['i','v']).Value - ) - cap_vre_derated = cap_vre.multiply(cf_adj_iv, fill_value=1).reset_index().dropna() - if len(cap_vre) != len(cap_vre_derated): - raise Exception( - "CF adjustment didn't work; probably missing values in cf_adj_t_filt. " - f"len(cap_vre) = {len(cap_vre)}; len(cap_vre_derated) = {(len(cap_vre_derated))}." - ) - cap_vre_derated = cap_vre_derated.groupby(['i','r']).Value.sum() - - ### Multiply derated capacity by CF to get generation - gen_vre_ir = recf.multiply(cap_vre_derated, axis=1).dropna(axis=1) - ### Check for missing data - if gen_vre_ir.shape[1] != cap_vre_derated.shape[0]: - missing_in_gen_vre_ir = set(cap_vre_derated.index) - set(gen_vre_ir.columns) - missing_in_cap_vre_derated = set(gen_vre_ir.columns) - set(cap_vre_derated.index) - raise Exception( - f"Mismatch between VRE capacity and available CF data. " - f"Missing in gen_vre_ir: {missing_in_gen_vre_ir or 'None'}, " - f"Missing in cap_vre_derated: {missing_in_cap_vre_derated or 'None'}" - ) - ### Aggregate by model zone - gen_vre_r = gen_vre_ir.copy() - gen_vre_r = gen_vre_r.groupby(axis=1, level='r').sum() - - ### Store generation by (i,r) for capacity_credit.py - gen_vre_resources = gen_vre_ir.reindex(resources[['i','r']], axis=1).fillna(0).clip(lower=0) - - vre_gen_exist = gen_vre_resources.copy() - vre_gen_exist.columns = ['|'.join(c) for c in vre_gen_exist.columns] - vre_gen_exist.index = h_dt_szn.set_index(['ccseason','year','h','hour']).index - h5out['vre_gen_exist'] = vre_gen_exist - - ### Store generation by (i,r) for PRAS, after aggregating i if necessary - pras_vre_gen = gen_vre_resources.copy() - pras_vre_gen.columns = pd.MultiIndex.from_arrays([ - pras_vre_gen.columns.get_level_values('i').map(lambda x: techs_vre_simplify.get(x,x)), - pras_vre_gen.columns.get_level_values('r') - ]) - pras_vre_gen = pras_vre_gen.groupby(['i','r'], axis=1).sum() - - pras_vre_gen.columns = ['|'.join(c) for c in pras_vre_gen.columns] - h5out['pras_vre_gen'] = pras_vre_gen - - - ###### Store marginal CF by (i,r) for capacity_credit.py - ## Use the cf_adj_iv for the latest available vintage - cf_adj_i = cf_adj_iv.reset_index() - ## Temporarily reformat the vintage so we can select the last one - def intify(v): - try: - return int(v) - except ValueError: - return v - cf_adj_i.v = ( - cf_adj_i.v.str.replace('new','') - .map(intify) - .map(lambda x: x if str(x).startswith('init') else f'new{x:>03}') - ) - cf_adj_i = ( - cf_adj_i.sort_values(['i','v']).drop_duplicates('i', keep='last') - .set_index('i').Value - .reindex(recf.columns.get_level_values('i').unique()).fillna(1) - ) - - ### Multiply [CF] * [CF adjustment] to get marginal CF - vre_cf_marg = ( - recf.multiply(cf_adj_i, level='i', axis=1) - .reindex(resources[['i','r']], axis=1) - ) - vre_cf_marg.columns = ['|'.join(c) for c in vre_cf_marg.columns] - vre_cf_marg.index = h_dt_szn.set_index(['ccseason','year','h','hour']).index - h5out['vre_cf_marg'] = vre_cf_marg - - h_dt_szn_load_years = h_dt_szn.loc[h_dt_szn.index.isin(load.index.get_level_values('datetime'))] - - #%%### Flexible load - ### H2 and DAC: Make it all inflexible (necessary for PRAS) - load_h2dac_all_hourly = ( - gdxreeds['prod_filt'] - .groupby(['r', 'allh']).Value.sum().reset_index() - .merge(h_dt_szn_load_years[['h']].reset_index(), left_on='allh', right_on='h') - .pivot(index='timestamp', columns='r', values='Value') - .fillna(0) - .reindex(h_dt_szn_load_years.index) - ) - - #%% Load shedding - ## Get the DR shed load for all weather years - gen_h_stress = gdxreeds['gen_h_stress_filt'] - gen_shed = gen_h_stress.loc[ - (gen_h_stress['t'] == t) - & gen_h_stress['i'].isin(tech_subset_table['DR_SHED'].values) - ].groupby(['r','allh']).Value.sum().unstack('r') - ## First assign values to all timestamps in GSw_HourlyChunkLengthStress - gen_shed_combined = gen_shed.reindex(hmap_myr_stress.h.values) - gen_shed_combined.index = hmap_myr_stress.index - ## Now fill other hours with zero - gen_shed_combined = gen_shed_combined.reindex(h_dt_szn.index).fillna(0) - - #%% Flexibly sited load -> pd.Series with index = regions and missing values 0-filled - ra_cap_loadsite = ( - gdxreeds['ra_cap_loadsite'] - .loc[gdxreeds['ra_cap_loadsite']['t'] == t] - .drop(columns='t') - .set_index('r') - .squeeze(1) - .reindex(load.columns).fillna(0) - ) - - #%%### Total load and net load - ### Get Candian exports and add to this solve year's load - can_exports = ( - gdxreeds['can_exports_h_filt'] - .merge(h_dt_szn_load_years[['h']].reset_index(), left_on='allh', right_on='h') - .pivot(index='timestamp', columns='r', values='Value') - ) - load_year = load.loc[t].add(can_exports, fill_value=0) - - ### PRAS doesn't yet handle flexible load, so include all H2/DAC load in the - ### version we write for PRAS - if int(sw['pras_include_h2dac']): - print(f'Added H2/DAC to PRAS load since pras_include_h2dac = {sw.pras_include_h2dac}') - pras_load = load_year.add(load_h2dac_all_hourly, fill_value=0) - else: - pras_load = load_year.copy() - - ### Add zeros for offshore zones - for r in offshore: - pras_load[r] = 0 - ra_cap_loadsite[r] = 0 - - ### Subtract dr-shed load - if int(sw.GSw_DRShed) and not gen_shed_combined.empty: - print(f'Subtracted shed load from PRAS load since GSw_DRShed = {sw.GSw_DRShed}') - pras_load = pras_load.subtract(gen_shed_combined, fill_value=0).clip(lower=0) - - ### Add flexibly sited load if its profile is inflexible (GSw_LoadSiteCF = 1) - if ( - np.isclose(float(sw.GSw_LoadSiteCF), 1) - and len(ra_cap_loadsite) - and int(sw.GSw_LoadSiteRA) - ): - print( - f'Added CAP_LOADSITE to PRAS load since GSw_LoadSiteCF = {sw.GSw_LoadSiteCF} ' - f'and GSw_LoadSiteRA = {sw.GSw_LoadSiteRA}' - ) - pras_load += ra_cap_loadsite - - h5out['pras_load'] = pras_load - ## Include the hourly H2/DAC load for debugging - h5out['pras_h2dac_load'] = load_h2dac_all_hourly - - ### Store load with the appropriate index for capacity_credit.py - h5out['load'] = ( - load_year.merge( - h_dt_szn[['ccseason', 'year', 'h', 'hour']], left_index=True, right_index=True - ) - .set_index(['ccseason', 'year', 'h', 'hour']) - ) - - #%%### Collect some csv's for ReEDS2PRAS - ### Transmission capacity: Subset for RA according to GSw_PRMTRADE_level switch - tran_cap = ( - trancap_reeds.set_index(['r','rr','trtype']).rename(columns={'Value':'MW'})) - if sw.GSw_PRMTRADE_level != 'country': - hierarchy = reeds.io.get_hierarchy(casedir) - if sw.GSw_PRMTRADE_level == 'r': - rmap = dict(zip(hierarchy.index, hierarchy.index)) - else: - rmap = hierarchy[sw.GSw_PRMTRADE_level] - tran_cap['level'] = tran_cap.index.get_level_values('r').map(rmap) - tran_cap['levell'] = tran_cap.index.get_level_values('rr').map(rmap) - tran_cap = ( - tran_cap.loc[tran_cap.level==tran_cap.levell] - .drop(['level','levell'], axis=1) - ) - csvout['tran_cap'] = tran_cap - - ### Converter capacity: Offshore zones don't need converters since offshore generation - ## exports are assumed to be in DC, so add converter capacity to each offshore zone - ## equal to the VSC transmission capacity into / out of the zone. - ## Transmission capacity is defined in both directions and VSC is the same in both, - ## so we can just keep offshore transmission in one direction. - offshore_with_transmission = [ - r for r in offshore if r in tran_cap.index.get_level_values('r').unique() - ] - csvout['cap_converter'] = pd.concat([ - gdxreeds['cap_converter_filt'].set_index('r').rename(columns={'Value':'MW'}), - tran_cap.loc[offshore_with_transmission].groupby('r').sum(), - ]) - - ### Nameplate capacity - max_cap = cap_nonloadtechs.set_index(['i','v','r']).Value.rename('MW') - ## Drop VRE since it is handled through pras_vre_gen - max_cap = max_cap.loc[ - ~max_cap.index.get_level_values('i').str.startswith( - tuple( - list(techs_vre_simplify.keys()) + list(techs_vre_simplify.values()) - ) - ) - ].copy() - ## Aggregate geothermal - simplify_geo = dict(zip( - tech_subset_table['GEO'].values, - ['geothermal']*len(tech_subset_table['GEO']) - )) - max_cap = max_cap.rename(index=simplify_geo, level='i').groupby(['i','v','r']).sum() - - ## Storage energy capacity [MWh] = power capacity [MW] * duration [h] - energy_cap = ( - cap_storage_devint - .set_index(['i','v','r']).Value - .multiply(duration) - .rename('MWh') - ) - ## Append batteries energy capacity - energy_cap = pd.concat([ - energy_cap, - cap_energy_ivr.set_index(['i','v','r'])['Value'].rename('MWh') - ]) - energy_cap = energy_cap.round(2) - energy_cap = energy_cap[energy_cap > 0] - - # Add to max_cap any index in energy_cap that's missing, setting them to 0 - missing_index = energy_cap.index.difference(max_cap.index) - max_cap = pd.concat([max_cap, pd.Series(0, index=missing_index, name=max_cap.name)]) - - ## Drop storage with energy or power capacity below the PRAS cutoff - too_small_storage = list(set( - energy_cap.loc[energy_cap < sw['storcap_cutoff']].index.tolist() - + max_cap.loc[energy_cap.index].loc[ - max_cap.loc[energy_cap.index] < sw['storcap_cutoff']].index.tolist() - + max_cap.loc[max_cap < sw['storcap_cutoff']/2].index.tolist() - )) - - csvout['energy_cap'] = energy_cap.drop(too_small_storage, errors='ignore') - csvout['max_cap'] = max_cap.drop(too_small_storage, errors='ignore') - - ### Storage efficiency - storage_hybrid = reeds.techs.expand_GAMS_tech_groups( - reeds.techs.get_tech_subset_table(casedir).loc[['STORAGE_HYBRID']].reset_index() - ).i - storage_eff = ( - gdxreeds['storage_eff'] - .loc[gdxreeds['storage_eff'].t==t] - .set_index('i') - .Value - .rename('fraction') - ## Only keep standalone storage - .drop(storage_hybrid, errors='ignore') - ) - ## As in ReEDS LP, storage losses are applied to charging side (none for discharging) - csvout['charge_eff'] = storage_eff - csvout['discharge_eff'] = pd.Series(index=storage_eff.index, data=1., name='fraction') - - #%% Planning reserve margin in MW (sometimes used during unit disaggregation) - csvout['max_unitsize'] = ( - gdxreeds['prm'].loc[gdxreeds['prm'].t == t].set_index('r').Value - * h5out['pras_load'].max() - ).round().astype(int).rename_axis('r').rename('mw') - ## Turn off for counties by setting to zero (zeros in this file mean the max unit - ## size is not enforced for that region in ReEDS2PRAS) - agglevel_variables = reeds.spatial.get_agglevel_variables( - reeds.io.reeds_path, os.path.join(casedir, 'inputs_case') - ) - counties = agglevel_variables['county_regions'] - if len(counties): - csvout['max_unitsize'].loc[counties] = 0 - - #%% Strip water tech suffixes from water-dependent technologies - ### and change upgrade techs to the tech they are upgraded FROM. - ### It would be more natural to use the tech they are upgraded TO but in most cases - ### (e.g. for CCS and H2) we don't have empirical outage rates for the upgraded-TO tech. - watertech2tech = pd.concat([ - pd.read_csv( - os.path.join(casedir,'inputs_case','i_coolingtech_watersource_link.csv'), - usecols=['*i','ii'], - ), - pd.read_csv( - os.path.join(casedir,'inputs_case','i_coolingtech_watersource_upgrades_link.csv'), - usecols=['*i','ii'], - ), - ]).apply(lambda x: x.str.lower()).set_index('*i').squeeze(1) - - upgrade2from = ( - pd.concat([ - pd.read_csv( - os.path.join(casedir, 'inputs_case', 'upgrade_link.csv') - ).rename(columns={'*TO':'TO'}), - pd.read_csv( - os.path.join(casedir, 'inputs_case', 'upgradelink_water.csv') - ).rename(columns={'*TO-WATER':'TO', 'FROM-WATER':'FROM', 'DELTA-WATER':'DELTA'}), - ]) - .apply(lambda x: x.str.lower()) - .set_index('TO').FROM - .map(lambda x: watertech2tech.get(x,x)) - ) - watertech2tech = watertech2tech.map(lambda x: upgrade2from.get(x,x)) - - techmap = pd.concat([upgrade2from, watertech2tech]).to_dict() - - ### Simplify all the techs in output csv files and sum the capacities - for key in csvout: - indices = csvout[key].index.names - if ('i' in indices) and ('cap' in key): - csvout[key] = csvout[key].rename(index=techmap, level='i').groupby(indices).sum() - - - #%% Check for errors before sending to ReEDS2PRAS - errorcheck_reeds2pras(casedir, csvout, h5out) - - - #%%### Write it - #%% .csv files - for key in csvout: - csvout[key].round(int(sw['decimals'])).to_csv( - os.path.join(casedir,'ReEDS_Augur','augur_data',f'{key}_{t}.csv'), - ) - - #%% .h5 files - for key in h5out: - if key.startswith('pras'): - reeds.io.write_profile_to_h5( - df=h5out[key].astype(np.float32), - filename=f'{key}_{t}.h5', - outfolder=os.path.join(casedir,'ReEDS_Augur','augur_data'), - ) - else: - h5out[key].astype(np.float32).to_hdf( - os.path.join(casedir,'ReEDS_Augur','augur_data',f'{key}_{t}.h5'), - key='data', complevel=4, mode='w', - ) - - #%%### Return outputs for debugging - return csvout, h5out diff --git a/ReEDS_Augur/run_pras.jl b/ReEDS_Augur/run_pras.jl deleted file mode 100644 index a44e86f6..00000000 --- a/ReEDS_Augur/run_pras.jl +++ /dev/null @@ -1,486 +0,0 @@ -#%% Imports -import ArgParse -import DataFrames -import Logging -import LoggingExtras -import Dates -import PRAS -import HDF5 - -const DF = DataFrames - -#%% Functions -""" - Parse command line arguments for use with ReEDS2PRAS and PRAS -""" -function parse_commandline() - s = ArgParse.ArgParseSettings() - - @ArgParse.add_arg_table s begin - "--reeds_path" - help = "Path to ReEDS-2.0 folder" - arg_type = String - required = true - "--reedscase" - help = "Path to ReEDS run (usually .../ReEDS-2.0/runs/{casename})" - arg_type = String - required = true - "--solve_year" - help = "ReEDS solve year (usually in [2020..2050])" - arg_type = Int - required = true - "--weather_year" - help = "The weather year to start from, in [2007..2013,2016..2023]" - arg_type = Int - default = 2007 - required = true - "--samples" - help = "Number of Monte Carlo samples to run in PRAS" - arg_type = Int - default = 10 - required = false - "--timesteps" - help = "Number of hourly timesteps to use" - arg_type = Int - default = 61320 - required = false - "--hydro_energylim" - help = "Model hydropower as an energy-limited resource" - arg_type = Int - default = 0 - required = false - "--scheduled_outage" - help = "Include monthly scheduled outage" - arg_type = Int - default = 0 - required = false - "--write_flow" - help = "Write the hourly interface flows" - arg_type = Int - default = 0 - required = false - "--write_surplus" - help = "Write the hourly surplus" - arg_type = Int - default = 0 - required = false - "--write_energy" - help = "Write the hourly storage energy" - arg_type = Int - default = 0 - required = false - "--write_shortfall_samples" - help = "Write the sample-level shortfall" - arg_type = Int - default = 0 - required = false - "--write_availability_samples" - help = "Write the sample-level generator and storage availability" - arg_type = Int - default = 0 - required = false - "--iteration" - help = "Solve-year iteration number (only used in file label)" - arg_type = Int - default = 0 - required = false - "--overwrite" - help = "Overwrite an existing .pras file" - arg_type = Int - default = 1 - required = false - "--include_samples" - help = "Include the number of samples in the output .csv filename" - arg_type = Int - default = 0 - required = false - "--pras_agg_ogs_lfillgas" - help = "Aggregate existing o-g-s and landfill gas using size for new units" - arg_type = Int - default = 0 - required = false - "--pras_existing_unit_size" - help = "Use average existing unit size by (tech,region) when disaggregating new units" - arg_type = Int - default = 1 - required = false - "--pras_max_unitsize_prm" - help = "Cap the upper bound of disaggregated unit size by zone at the zonal PRM in MW" - arg_type = Int - default = 1 - required = false - "--pras_seed" - help = "Random seed for PRAS (positive integer; ignored and set randomly if 0)" - arg_type = Int - default = 1 - required = false - "--debug" - help = "Log debug-level messages" - arg_type = Int - default = 0 - required = false - end - return ArgParse.parse_args(s) -end - -""" - Set up logging to file and console -""" -function setup_logger(pras_system_path::String, args::Dict) - if ~isnothing(pras_system_path) - logfile = replace(pras_system_path, ".pras"=>".log") - - if args["debug"] == 1 - logfilehandle = LoggingExtras.MinLevelLogger( - LoggingExtras.FileLogger(logfile; append=true), - Logging.Debug) - else - logfilehandle = LoggingExtras.MinLevelLogger( - LoggingExtras.FileLogger(logfile; append=true), - Logging.Info) - end - - logger = LoggingExtras.TeeLogger( - Logging.global_logger(), - logfilehandle - ) - - ### https://github.com/JuliaLogging/LoggingExtras.jl#add-timestamp-to-all-logging - timestamp_logger(logger) = LoggingExtras.TransformerLogger(logger) do log - merge( - log, - (; message = "$(Dates.format(Dates.now(), "yyyy-mm-dd HH:MM:SS")) | $(log.message)") - ) - end - - Logging.global_logger(timestamp_logger(logger)) - end -end - -""" - Simple PRAS analysis. - - Parameters - ---------- - - Returns - ------- -""" -function run_pras(pras_system_path::String, args::Dict) - #%% Load the system model - @info "Parsing PRAS System ..." - sys = PRAS.SystemModel(pras_system_path); - - #%% Specify the results to save - resultspec = Dict{String,Any}("short" => PRAS.Shortfall()) - if args["write_flow"] == 1 - resultspec["flow"] = PRAS.Flow() - end - if args["write_surplus"] == 1 - resultspec["surplus"] = PRAS.Surplus() - end - if args["write_energy"] == 1 - resultspec["energy"] = PRAS.StorageEnergy() - end - if args["write_shortfall_samples"] == 1 - resultspec["short_samples"] = PRAS.ShortfallSamples() - end - if args["write_availability_samples"] == 1 - resultspec["avail_gen"] = PRAS.GeneratorAvailability() - resultspec["avail_stor"] = PRAS.StorageAvailability() - resultspec["avail_genstor"] = PRAS.GeneratorStorageAvailability() - resultspec["energy_samples"] = PRAS.StorageEnergySamples() - end - - #%% Run PRAS - if args["pras_seed"] > 0 - method = PRAS.SequentialMonteCarlo( - samples=args["samples"], threaded=true, verbose=true, seed=args["pras_seed"]) - else - method = PRAS.SequentialMonteCarlo( - samples=args["samples"], threaded=true, verbose=true) - end - results_tuple = PRAS.assess(sys, method, values(resultspec)...) - results = Dict{String,Any}(zip(keys(resultspec), results_tuple)) - - #%% Print some results for the entire modeled region to show it worked - @info "$(PRAS.LOLE(results["short"])) event-h" - @info "$(PRAS.EUE(results["short"])) MWh" - @info "NEUE = $(1e6 * PRAS.EUE(results["short"]).eue.estimate / sum(sys.regions.load)) ppm" - - ## Filter out DC regions used for VSC HVDC transmission - regions = [r for r in sys.regions.names if !(occursin("|", r))] - - #%% Print some more detailed results if debugging - for (i, reg) in enumerate(regions) - @debug "$reg: $(round(PRAS.LOLE(results["short"],reg).lole.estimate)) event-h" - @debug "$reg: $(round(PRAS.EUE(results["short"],reg).eue.estimate)) MWh" - @debug "$reg: NEUE = $(round( - 1e6 * PRAS.EUE(results["short"],reg).eue.estimate - / sum(sys.regions.load[i,:]) - )) ppm\n\n" - end - - #%% Record the EUE and LOLE outputs by region and timestep - ## Units are: - ## * LOLE: event-h - ## * EUE: MWh - ## First for the whole modeled area (labeled as "USA" but if modeling a smaller - ## region (speicified by GSw_Region) it will be for that modeled region) - dfout = DF.DataFrame( - USA_LOLE=[PRAS.LOLE(results["short"],h).lole.estimate for h in sys.timestamps], - USA_EUE=[PRAS.EUE(results["short"],h).eue.estimate for h in sys.timestamps], - ) - ## Now for each constituent region - for (i,r) in enumerate(regions) - dfout[!, "$(r)_LOLE"] = [PRAS.LOLE(results["short"],r,h).lole.estimate for h in sys.timestamps] - dfout[!, "$(r)_EUE"] = [PRAS.EUE(results["short"],r,h).eue.estimate for h in sys.timestamps] - end - - #%% Write it - if args["include_samples"] == 1 - outfile = replace(pras_system_path, ".pras"=>"-$(args["samples"]).h5") - else - outfile = replace(pras_system_path, ".pras"=>".h5") - end - HDF5.h5open(outfile, "w") do f - for column in DF.names(dfout) - f[column, compress=4] = convert(Array, dfout[!, column]) - end - end - @info("Wrote PRAS EUE and LOLE to $(outfile)") - - #%%### Record more operational details if desired - - ### Flow - if args["write_flow"] == 1 - dfflow = DF.DataFrame() - for i in results["flow"].interfaces - ## Flow results are tuples of (mean, standard deviation). Keep the mean. - dfflow[!, "$(i)"] = [results["flow"][i,h][1] for h in sys.timestamps] - end - ## Write it - flowfile = replace(outfile, ".h5"=>"-flow.h5") - HDF5.h5open(flowfile, "w") do f - for column in DF._names(dfflow) - f["$column", compress=4] = convert(Array, dfflow[!, column]) - end - end - @info("Wrote PRAS flow to $(flowfile)") - end - - ### Surplus - if args["write_surplus"] == 1 - dfsurplus = DF.DataFrame() - for r in regions - ## Surplus results are tuples of (mean, standard deviation). Keep the mean. - dfsurplus[!, "$(r)"] = [results["surplus"][r,h][1] for h in sys.timestamps] - end - ## Write it - surplusfile = replace(outfile, ".h5"=>"-surplus.h5") - HDF5.h5open(surplusfile, "w") do f - for column in DF._names(dfsurplus) - f["$column", compress=4] = convert(Array, dfsurplus[!, column]) - end - end - @info("Wrote PRAS surplus to $(surplusfile)") - end - ### Storage energy - if args["write_energy"] == 1 - dfenergy = DF.DataFrame() - for i in sys.storages.names - ## Energy results are tuples of (mean, standard deviation). Keep the mean. - dfenergy[!, strip("$(i)", '_')] = [results["energy"][i,h][1] for h in sys.timestamps] - end - ## Write it - energyfile = replace(outfile, ".h5"=>"-energy.h5") - HDF5.h5open(energyfile, "w") do f - for column in DF._names(dfenergy) - f["$column", compress=4] = convert(Array, dfenergy[!, column]) - end - end - @info("Wrote PRAS storage energy to $(energyfile)") - end - - ### Sample-level shortfall - if args["write_shortfall_samples"] == 1 - dictshort = Dict(s => DF.DataFrame() for s = 1:args["samples"]) - for s in range(1, args["samples"]) - dictshort[s] = DF.DataFrame( - transpose(getindex.(results["short_samples"][:, :], s)), - sys.regions.names - ) - # subset to regions (filter out DC regions) - dictshort[s] = dictshort[s][:,findall(regions .∈ Ref(sys.regions.names))] - end - ## Write it - shortfile = replace(outfile, ".h5"=>"-shortfall_samples.h5") - HDF5.h5open(shortfile, "w") do f - ## Create a group for each sample. Within each group, write an array for each region. - for s in range(1, args["samples"]) - HDF5.create_group(f, "$s") - for column in DF._names(dictshort[s]) - f["$s"]["$column", compress=4] = convert(Array, dictshort[s][!, column]) - end - end - end - @info("Wrote PRAS shortfall by sample to $(shortfile)") - end - - ### Sample-level generator and storage availability - if args["write_availability_samples"] == 1 - dictavail = Dict(s => DF.DataFrame() for s = 1:args["samples"]) - for s in range(1, args["samples"]) - dictavail[s] = hcat( - DF.DataFrame( - transpose(getindex.(results["avail_gen"][:, :], s)), - strip.(results["avail_gen"].generators, '_') - ), - DF.DataFrame( - transpose(getindex.(results["avail_stor"][:, :], s)), - strip.(results["avail_stor"].storages, '_') - ), - DF.DataFrame( - transpose(getindex.(results["avail_genstor"][:, :], s)), - strip.(results["avail_genstor"].generatorstorages, '_') - ), - ) - end - ## Write it - availabilityfile = replace(outfile, ".h5"=>"-avail.h5") - HDF5.h5open(availabilityfile, "w") do f - ## Create a group for each sample. Within each group, write an array for each unit. - for s in range(1, args["samples"]) - HDF5.create_group(f, "$s") - for column in DF._names(dictavail[s]) - f["$s"]["$column", compress=4] = convert(Array, dictavail[s][!, column]) - end - end - end - @info("Wrote PRAS unit availability to $(availabilityfile)") - ### Same for storage energy by sample - dictstoravail = Dict(s => DF.DataFrame() for s = 1:args["samples"]) - for s in range(1, args["samples"]) - dictstoravail[s] = DF.DataFrame( - transpose(getindex.(results["energy_samples"][:, :], s)), - strip.(results["energy_samples"].storages, '_') - ) - end - ## Write it - energysamplesfile = replace(outfile, ".h5"=>"-energy_samples.h5") - HDF5.h5open(energysamplesfile, "w") do f - for s in range(1, args["samples"]) - HDF5.create_group(f, "$s") - for column in DF._names(dictstoravail[s]) - f["$s"]["$column", compress=4] = convert(Array, dictstoravail[s][!, column]) - end - end - end - @info("Wrote PRAS storage energy by sample to $(energysamplesfile)") - end - - #%% - return dfout -end - - -#%% Main function -""" - Run ReEDS2PRAS and PRAS -""" -function main(args::Dict) - #%% Define some intermediate filenames - pras_system_path = joinpath( - args["reedscase"],"ReEDS_Augur","PRAS", - "PRAS_$(args["solve_year"])i$(args["iteration"]).pras" - ) - - #%% Set up the logger - setup_logger(pras_system_path, args) - @info "Julia version: $(VERSION)" - @info "Julia executable: $(joinpath(Sys.BINDIR, "julia"))" - @info "Running ReEDS2PRAS with the following inputs:" - for (arg, val) in args - @info "$arg => $val" - end - - #%% Run ReEDS2PRAS - if (args["overwrite"] == 1) | ~isfile(pras_system_path) - ### Create and save the PRAS system - ## Could use compression_level={integer} here but it doesn't really help - PRAS.savemodel( - ReEDS2PRAS.reeds_to_pras( - args["reedscase"], - args["solve_year"], - args["timesteps"], - args["weather_year"], - # Boolean switches: == to convert from integer to boolean - args["scheduled_outage"] == 1, - args["hydro_energylim"] == 1, - args["pras_agg_ogs_lfillgas"] == 1, - args["pras_existing_unit_size"] == 1, - args["pras_max_unitsize_prm"] == 1, - ), - pras_system_path, - verbose=true, - ) - @info "Finished ReEDS2PRAS" - end - - #%% Run PRAS - if args["samples"] > 0 - @info "Running PRAS" - dfout = run_pras(pras_system_path, args) - @info "Finished PRAS" - #%% - return dfout - end -end - - -#%% Procedure -if abspath(PROGRAM_FILE) == @__FILE__ - #%% Inputs for debugging - # julia --project=/path/to/ReEDS-2.0 --threads=1 - # args = Dict( - # "reeds_path" => "/path/to/ReEDS-2.0", - # "reedscase" => ( - # "/path/to/ReEDS-2.0/runs/" - # *"runname"), - # "solve_year" => 2035, - # "weather_year" => 2007, - # "samples" => 10, - # "iteration" => 0, - # "timesteps" => 131400, - # "hydro_energylim" => 1, - # "write_flow" => 0, - # "write_surplus" => 0, - # "write_energy" => 0, - # "write_shortfall_samples" => 1, - # "write_availability_samples" => 0, - # "overwrite" => 1, - # "debug" => 0, - # "include_samples" => 0, - # "scheduled_outage" => 0, - # "pras_agg_ogs_lfillgas" => 0, - # "pras_existing_unit_size" => 1, - # "pras_max_unitsize_prm" => 1, - # "pras_seed" => 1, - # ) - # reedscase = args["reedscase"] - # solve_year = args["solve_year"] - # timesteps = args["timesteps"] - # weather_year = args["weather_year"] - # include(joinpath(args["reeds_path"], "reeds2pras", "src", "ReEDS2PRAS.jl")) - - #%% Parse the command line arguments - args = parse_commandline() - - #%% Include ReEDS2PRAS - include(joinpath(args["reedscase"], "reeds2pras", "src", "ReEDS2PRAS.jl")) - - #%% Run it - main(args) - - #%% -end diff --git a/ReEDS_Augur/stress_periods.py b/ReEDS_Augur/stress_periods.py deleted file mode 100644 index 905a710b..00000000 --- a/ReEDS_Augur/stress_periods.py +++ /dev/null @@ -1,615 +0,0 @@ -#%%### General imports -import os -import site -import traceback -import pandas as pd -import numpy as np -from glob import glob -import re -import matplotlib.pyplot as plt -### Local imports - -## use this to import reeds when running locally for debugging -# import site -# this_dir_path = os.path.dirname(os.path.realpath(__file__)) -# site.addsitedir(os.path.join(this_dir_path, "..")) - -import reeds - -# #%% Debugging -# sw['reeds_path'] = os.path.expanduser('~/github/ReEDS-2.0/') -# sw['casedir'] = os.path.join(sw['reeds_path'],'runs','v20230123_prmM3_Pacific_d7sIrh4sh2_y2') -# import importlib -# importlib.reload(functions) - - -#%%### Functions -def plot_eue_diagnostics(sw, t, iteration, high_eue_periods): - try: - dates = ( - pd.concat(high_eue_periods) - .reset_index().actual_period.map(reeds.timeseries.h2timestamp) - .dt.strftime('%Y-%m-%d') - .tolist() - ) - vmax = {'forced': 40, 'scheduled': 25, 'both': 50} - aggfunc = 'max' - for outage_type in vmax: - savename = f'map-outage_{outage_type}_{aggfunc}-{t}i{iteration}.png' - plt.close() - f, ax, _ = reeds.reedsplots.map_outage_days( - sw.casedir, - dates=dates, - outage_type=outage_type, - aggfunc=aggfunc, - vmax=vmax[outage_type], - ) - plt.savefig( - os.path.join(sw.casedir, 'outputs', 'Augur_plots', savename) - ) - plt.close() - except Exception as err: - print(err) - - -def get_and_write_neue(sw, write=True): - """ - Write dropped load across all completed years to outputs - so it can be plotted alongside other ReEDS outputs. - - Notes - ----- - * The denominator of NEUE is exogenous electricity demand; it does not include - endogenous load from losses or H2 production or exogenous H2 demand. - """ - infiles = [ - i for i in sorted(glob( - os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', 'PRAS_*.h5'))) - if re.match(r"PRAS_[0-9]+i[0-9]+.h5", os.path.basename(i)) - ] - eue = {} - for infile in infiles: - year_iteration = os.path.basename(infile)[len('PRAS_'):-len('.h5')].split('i') - year = int(year_iteration[0]) - iteration = int(year_iteration[1]) - eue[year,iteration] = reeds.io.read_pras_results(infile)['USA_EUE'].sum() - eue = pd.Series(eue).rename('MWh') - eue.index = eue.index.rename(['year','iteration']) - - load = reeds.io.read_file(os.path.join(sw['casedir'],'inputs_case','load.h5')) - loadyear = load.sum(axis=1).groupby('year').sum() - - neue = ( - (eue / loadyear * 1e6).rename('NEUE [ppm]') - .rename_axis(['t','iteration']).sort_index() - ) - - if write: - neue.to_csv(os.path.join(sw['casedir'],'outputs','neue.csv')) - eue.to_csv(os.path.join(sw['casedir'],'outputs','eue.csv')) - return neue - - -def get_annual_neue(case, t, iteration=0): - """ - """ - ### Get EUE from PRAS - dfeue = reeds.ra.get_pras_eue(case=case, t=t, iteration=iteration) - - ### Get load (for calculating NEUE) - dfload = reeds.io.read_h5py_file( - os.path.join( - case,'ReEDS_Augur','augur_data',f'pras_load_{t}.h5') - ) - dfload.index = dfeue.index - - levels = ['country','interconnect','nercr','transreg','transgrp','st','r'] - _neue = {} - for hierarchy_level in levels: - ### Get the region aggregator - rmap = reeds.io.get_rmap(case=case, hierarchy_level=hierarchy_level) - ### Get NEUE summed over year - _neue[hierarchy_level,'sum'] = ( - dfeue.rename(columns=rmap).groupby(axis=1, level=0).sum().sum() - / dfload.rename(columns=rmap).groupby(axis=1, level=0).sum().sum() - ) * 1e6 - ### Get max NEUE hour - _neue[hierarchy_level,'max'] = ( - dfeue.rename(columns=rmap).groupby(axis=1, level=0).sum() - / dfload.rename(columns=rmap).groupby(axis=1, level=0).sum() - ).max() * 1e6 - - ### Combine it - neue = pd.concat(_neue, names=['level','metric','region']).rename('NEUE_ppm') - - return neue - - -def get_shoulder_periods(sw, criterion, dfenergy_r, high_eue_periods): - ## Stop if not needed - if sw.GSw_PRM_StressStorageCutoff.lower() in ['off', '0', 'false']: - print( - f"GSw_PRM_StressStorageCutoff={sw.GSw_PRM_StressStorageCutoff} " - "so not adding shoulder stress periods based on storage level" - ) - return {} - if dfenergy_r.empty: - print( - "No storage capacity, so no shoulder stress periods will be added " - "based on storage level" - ) - return {} - - ## Parse inputs - hierarchy = reeds.io.get_hierarchy(sw.casedir) - timeindex = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) - cutofftype, cutoff = sw.GSw_PRM_StressStorageCutoff.lower().split('_') - periodhours = {'day':24, 'wek':24*5, 'year':24}[sw.GSw_HourlyType] - (hierarchy_level, ppm, stress_metric, period_agg_method) = criterion.split('_') - - ## Aggregate storage energy to hierarchy_level - dfenergy_agg = ( - dfenergy_r.rename(columns=hierarchy[hierarchy_level]) - .groupby(axis=1, level=0).sum() - ) - dfheadspace_MWh = dfenergy_agg.max() - dfenergy_agg - dfheadspace_frac = dfheadspace_MWh / dfenergy_agg.max() - - shoulder_periods = {} - for i, row in high_eue_periods[criterion, f'high_{stress_metric}'].iterrows(): - if row.r not in dfheadspace_MWh: - continue - - day = pd.Timestamp('-'.join(row[['y','m','d']].astype(str).tolist())) - - start_headspace_MWh = dfheadspace_MWh.loc[day.strftime('%Y-%m-%d'),row.r].iloc[0] - end_headspace_MWh = dfheadspace_MWh.loc[day.strftime('%Y-%m-%d'),row.r].iloc[-1] - - start_headspace_frac = dfheadspace_frac.loc[day.strftime('%Y-%m-%d'),row.r].iloc[0] - end_headspace_frac = dfheadspace_frac.loc[day.strftime('%Y-%m-%d'),row.r].iloc[-1] - - day_eue = high_eue_periods[criterion, f'high_{stress_metric}'].loc[i,'EUE'] - day_index = np.where( - timeindex == dfenergy_agg.loc[day.strftime('%Y-%m-%d')].iloc[0].name - )[0][0] - - day_before = timeindex[day_index - periodhours] - day_after = timeindex[(day_index + periodhours) % len(timeindex)] - - if ( - ((cutofftype == 'eue') and (end_headspace_MWh / day_eue >= float(cutoff))) - or ((cutofftype[:3] == 'cap') and (end_headspace_frac >= float(cutoff))) - or (cutofftype[:3] == 'abs') - ): - shoulder_periods[criterion, f'after_{row.name}'] = pd.Series({ - 'actual_period':day_after.strftime('y%Yd%j'), - 'y':day_after.year, 'm':day_after.month, 'd':day_after.day, 'r':row.r, - }).to_frame().T.set_index('actual_period') - print(f"Added {day_after} as shoulder stress period after {day}") - - if ( - ((cutofftype == 'eue') and (start_headspace_MWh / day_eue >= float(cutoff))) - or ((cutofftype[:3] == 'cap') and (start_headspace_frac >= float(cutoff))) - or (cutofftype[:3] == 'abs') - ): - shoulder_periods[criterion, f'before_{row.name}'] = pd.Series({ - 'actual_period':day_before.strftime('y%Yd%j'), - 'y':day_before.year, 'm':day_before.month, 'd':day_before.day, 'r':row.r, - }).to_frame().T.set_index('actual_period') - print(f"Added {day_before} as shoulder stress period before {day}") - - return shoulder_periods - - -def get_eue_sorted_periods(sw, t, iteration): - ### Get storage state of charge (SOC) to use in selection of "shoulder" stress periods - dfenergy = reeds.io.read_pras_results( - os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', f"PRAS_{t}i{iteration}-energy.h5") - ) - timeindex = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) - dfenergy.index = timeindex - ## Sum by region - dfenergy_r = ( - dfenergy - .rename(columns={c: c.split('|')[1] for c in dfenergy.columns}) - .groupby(axis=1, level=0).sum() - ) - - ### Get NEUE - neue = pd.read_csv( - os.path.join(sw.casedir, 'outputs', f'neue_{t}i{iteration}.csv'), - index_col=['level', 'metric', 'region'], - ).squeeze(1) - - ### Load this year's stress periods so we don't duplicate - stressperiods_this_iteration = pd.read_csv( - os.path.join( - sw['casedir'], 'inputs_case', f'stress{t}i{iteration}', 'period_szn.csv') - ) - - ### Check all stress criteria; for regions that fail, add new stress periods - _eue_sorted_periods = {} - failed = {} - high_eue_periods = {} - shoulder_periods = {} - for criterion in sw.GSw_PRM_StressThreshold.split('/'): - ## Example: criterion = 'transgrp_10_EUE_sum' - (hierarchy_level, ppm, stress_metric, period_agg_method) = criterion.split('_') - - eue_periods = reeds.ra.get_eue_periods( - case=sw.casedir, t=t, iteration=iteration, - hierarchy_level=hierarchy_level, - stress_metric=stress_metric, - period_agg_method=period_agg_method, - ) - - ### Sort in descending stress_metric order - _eue_sorted_periods[criterion] = ( - eue_periods - .sort_values(stress_metric, ascending=False) - .reset_index().set_index('actual_period') - ) - - ### Get the threshold(s) and see if any of them failed - this_test = neue[hierarchy_level][period_agg_method] - - if (this_test > float(ppm)).any(): - failed[criterion] = this_test.loc[this_test > float(ppm)] - print(f"GSw_PRM_StressThreshold = {criterion} failed for:") - print(failed[criterion]) - ###### Add GSw_PRM_StressIncrement periods to the list for the next iteration - high_eue_periods[criterion, f'high_{stress_metric}'] = ( - _eue_sorted_periods[criterion].loc[ - ## Only include new stress periods for the region(s) that failed - _eue_sorted_periods[criterion].r.isin(failed[criterion].index) - ## Don't repeat existing stress periods - & ~(_eue_sorted_periods[criterion].index.isin( - stressperiods_this_iteration.actual_period)) - ] - ## Don't add dates more than once - .drop_duplicates(subset=['y','m','d']) - ## Keep the GSw_PRM_StressIncrement worst periods for each region. - ## If you instead want to keep the GSw_PRM_StressIncrement worst periods - ## overall, use .nlargest(int(sw.GSw_PRM_StressIncrement), stress_metric) - .groupby('r').head(int(sw.GSw_PRM_StressIncrement)) - ) - for period, row in high_eue_periods[criterion, f'high_{stress_metric}'].iterrows(): - print( - f"Added {period} " - f"({reeds.timeseries.h2timestamp(period).strftime('%Y-%m-%d')}) " - f"as stress period for {row.r} " - f"({stress_metric} = {row[stress_metric]})" - ) - - ### Include "shoulder periods" before or after each period - ### if the storage state of charge is low - shoulder_periods = { - **shoulder_periods, - **get_shoulder_periods(sw, criterion, dfenergy_r, high_eue_periods) - } - - ### Dealing with earlier criteria may also address later criteria, so stop here - break - - else: - print(f"GSw_PRM_StressThreshold = {criterion} passed") - - eue_sorted_periods = pd.concat(_eue_sorted_periods, names=['criterion']) - - ### Get lists of stress periods: new (added this iteration) and all - if len(failed): - new_stress_periods = pd.concat( - {**high_eue_periods, **shoulder_periods}, names=['criterion','periodtype'], - ).reset_index().drop_duplicates(subset='actual_period', keep='first') - else: - return failed, None, None - - ## Reproduce the format of inputs_case/stress_period_szn.csv - p = 'w' if sw.GSw_HourlyType == 'wek' else 'd' - new_stressperiods_write = pd.DataFrame({ - 'rep_period': new_stress_periods.actual_period, - 'year': new_stress_periods.actual_period.map( - lambda x: int(x.strip('sy').split(p)[0])), - 'yperiod': new_stress_periods.actual_period.map( - lambda x: int(x.strip('sy').split(p)[1])), - 'actual_period': new_stress_periods.actual_period, - }) - - ### Add new stress periods to the stress periods used for this year/iteration, then write - newstresspath = f'stress{t}i{iteration+1}' - os.makedirs(os.path.join(sw['casedir'], 'inputs_case', newstresspath), exist_ok=True) - outpath = os.path.join(sw['casedir'], 'inputs_case', newstresspath, 'period_szn.csv') - - combined_periods_write = pd.concat( - [stressperiods_this_iteration, new_stressperiods_write], - axis=0, - ).drop_duplicates(keep='first') - - if int(sw.GSw_PRM_CapCredit): - pd.DataFrame(columns=['rep_period','year','yperiod','actual_period']).to_csv( - outpath, - index=False, - ) - else: - combined_periods_write.to_csv(outpath, index=False) - - ### Tables and plots for debugging - eue_sorted_periods.round(2).rename(columns={'EUE':'EUE_MWh','NEUE':'NEUE_ppm'}).to_csv( - os.path.join(sw.casedir, 'inputs_case', newstresspath, 'eue_sorted_periods.csv') - ) - new_stress_periods.round(2).rename(columns={'EUE':'EUE_MWh','NEUE':'NEUE_ppm'}).to_csv( - os.path.join(sw.casedir, 'inputs_case', newstresspath, 'new_stress_periods.csv'), - index=False, - ) - plot_eue_diagnostics(sw, t, iteration, high_eue_periods) - - return failed, new_stressperiods_write, combined_periods_write - - -def prm_increment_pras(sw, t, iteration, combined_periods_write, failed_regions): - try: - hmap = pd.read_csv( - os.path.join(sw.casedir, 'inputs_case', f'stress{t}i{iteration+1}', 'hmap_allyrs.csv') - ) - stress_hours = hmap.loc[ - hmap.actual_period.str.contains('|'.join(combined_periods_write.actual_period)) - ] - except FileNotFoundError: - # if there are no stress periods being modeled, use dispatch year to - # fill in for stress hours - stress_hours = pd.read_csv( - os.path.join(sw.casedir, 'inputs_case', 'rep', 'hmap_myr.csv') - ) - - ## shortfall data - # read the net shortfall (positive) and net surplus (negative) results - # by sample from PRAS run (MWh) - filepath = os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', - f'PRAS_{sw["t"]}i{iteration}-shortfall_samples.h5') - net_short = reeds.io.read_pras_results(filepath) - # get number of samples - n_samples = len(net_short) - # collapse dict of dataframes by sample in 1 dataframe (keep index to preserve hours) - net_short = pd.concat( - (df.assign(**{"sample": k}) for k, df in net_short.items()), ignore_index=False) - # convert to long format with shortfall by sample, hour, and r - net_short.index.names=['hour'] - net_short = net_short.reset_index().set_index(['sample','hour']) - net_short = net_short.sort_index(level=['sample', 'hour'], ascending=[True, True]) - net_short = net_short.melt( - ignore_index=False, var_name='r', value_name='net_short_mwh').reset_index() - - # zero-out negative values (net surplus) for determining regional unserved energy totals - net_short['net_short_mwh'] = net_short['net_short_mwh'].clip(lower=0) - # calaculate total regional net shortfall for all hours by sample - net_short_crit = net_short.groupby(['r','sample'], as_index=False)['net_short_mwh'].sum() - - ## get load data - dfload = reeds.io.read_file( - os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data',f'pras_load_{t}.h5'), - parse_timestamps=True - ) - - # add an index to represent each hour - dfload = dfload.reset_index().reset_index().rename(columns={"index":"hour"}) - - # melt to long - dfload = dfload.melt(id_vars=['datetime', 'hour'], var_name='r', value_name='load_mwh') - - ## get regional load for (1) all hours (2) just the stress periods - ## total load is used to translate the ppm target to EUE, whereas - ## the stress period load is used to back-calculate the incremental prm - ## needed to get to the target - - # total load by r - dfload_all = dfload.groupby(['r'], as_index=False)['load_mwh'].sum() - - # total stress period load by r - # note: use hour0 to subset to stress periods here since load data starts with hour index 0 - dfload_stress = dfload.loc[dfload.hour.isin(stress_hours.hour0)] - dfload_stress = dfload_stress.groupby(['r'], as_index=False)['load_mwh'].sum() - dfload_stress = dfload_stress.rename(columns={'load_mwh':'stress_load_mwh'}) - - # combine - dfload_all = dfload_all.merge(dfload_stress) - - # transform the reliability target criteria by region from ppm into - # unserved energy (MWh) - dfload_all = dfload_all.merge(failed_regions, on='r') - dfload_all['target_eue_mwh'] = ( - dfload_all['ppm'] / 1e6 * dfload_all['load_mwh'] - ) - - ## calculate piece-wise linear function (plf) that estimates the change in EUE - ## across the samples as a function of the amount of surplus added added to address - ## unserved energy in each sample each segment of the plf is defined by a slope and - ## two points: (x1, y1) and (x2, y2) - plfs = net_short_crit.loc[net_short_crit.net_short_mwh > 0].copy() - ## y-intercept: initial EUE - plfs['intercept'] = plfs.groupby('r')['net_short_mwh'].transform('sum') / n_samples - ## slope: computed from the lolp based on the remaining periods with unserved energy - ## as surplus is added sort unserved by descending first to calculate slopes - plfs = plfs.sort_values(['r', 'net_short_mwh'], ascending=False) - plfs['slope'] = -1 - plfs['slope'] = plfs.groupby(['r'])['slope'].transform('cumsum') / n_samples - # resort in ascending order for later calculations - plfs = plfs.sort_values(['r', 'net_short_mwh'], ascending=True) - ## x1: surplus to add to eliminate unserved energy from previous sample - plfs['x1'] = plfs.groupby('r')['net_short_mwh'].shift(1, fill_value=0) - ## x2: surplus to add to eliminate unserved energy from this sample - plfs['x2'] = plfs['net_short_mwh'] - # compute change in y value over each segment - plfs['Dy'] = plfs['slope'] * (plfs['x2']-plfs['x1']) - # check: Dy should never be positive - assert plfs['Dy'].max() <= 0, "Error in Dy calculation" - ## y1: intercept + cumulative change in unserved (Dy) - plfs['y1'] = plfs['intercept'] + plfs.groupby('r')['Dy'].transform( - lambda x: x.cumsum().shift(1, fill_value=0)) - ## y2: y1 + change over that segment (next y1 value) - plfs['y2'] = plfs.groupby('r')['y1'].shift(-1, fill_value=0) - - # now merge load merge with plf functions to find the segment that captures the target - plfs = plfs.merge(dfload_all, on='r') - plfs['seg'] = 0 - plfs.loc[(plfs['target_eue_mwh']<=plfs['y1']) & ( - plfs['target_eue_mwh']>=plfs['y2']), 'seg'] = 1 - # calculate the energy surplus to add by backtracking from the target_eue on the - # relevant segment(y): y=a+b*x => x=(y-a)/b - prm_increment = plfs.loc[plfs['seg']==1].copy() - prm_increment['surplus_mwh'] = prm_increment['x1'] + ( - prm_increment['target_eue_mwh'] - prm_increment['y1']) * (1 / prm_increment['slope']) - # calculate the prm increase as the required surplus as a fraction of - # load during stress periods - prm_increment['fraction'] = ( - prm_increment['surplus_mwh'] / prm_increment['stress_load_mwh'] - ) - prm_increment = prm_increment[['r','fraction']].reset_index(drop=True) - return prm_increment - - -def update_prm(sw, t, iteration, failed, combined_periods_write): - """Update the energy reserve margin by region r for stress periods, either using a - static increment (GSw_PRM_UpdateMethod=1) or based on the estimated surplus needed by PRAS - to recover the desired reliabiliaty criteria (GSw_PRM_UpdateMethod>1). - - Args: - sw (pd.series): ReEDS switches for this run. - t (int): Model solve year. - iteration (int): ReEDS-PRAS iteration - failed (dict): Dictionary of regions with unserved energy at the hierarchy_level - and their criterion evaluations - stress_hours (pd.DataFrame): data frame of stress periods - - Returns: - pd.DataFrame: Table of prm levels for the next PRAS iteration - """ - # Get regions that failed criteria - _failed_regions = [] - for criterion in failed: - # Example: criterion = 'transgrp_10_EUE_sum' - (hierarchy_level, ppm, __, __) = criterion.split('_') - # Recover regions where the PRM criterion failed - rmap = reeds.io.get_rmap(sw['casedir'], hierarchy_level=hierarchy_level).reset_index() - df = rmap.loc[ - rmap[hierarchy_level].isin(failed[criterion].index) - ].rename(columns={hierarchy_level:'region'}) - df['hierarchy_level'] = hierarchy_level - df['ppm'] = float(ppm) - _failed_regions.append(df) - # For zones that failed multiple criteria, use the most stringent (lowest EUE target) - failed_regions = ( - pd.concat(_failed_regions) - .sort_values(by=['ppm']) - .drop_duplicates(subset='r', keep='first') - ) - - ## Fixed-increment update - if int(sw.GSw_PRM_UpdateMethod) == 1: - prm_increment = failed_regions.copy() - prm_increment['fraction'] = float(sw['GSw_PRM_UpdateFraction']) - ## PRAS-informed PRM update - else: - prm_increment = prm_increment_pras( - sw, - t, - iteration, - combined_periods_write, - failed_regions, - ) - prm_increment = ( - prm_increment.rename(columns={'r':'*r'}) - .set_index('*r').fraction - ) - - ## Add the PRM increment to last iteration's PRM - prm = pd.read_csv( - os.path.join(sw['casedir'], 'inputs_case', f'stress{t}i{iteration}', 'prm.csv'), - index_col='*r', - ).fraction - prm_next_iteration = prm.add(prm_increment, fill_value=0).round(3) - - return prm_next_iteration - - -#%%### Procedure -def main(sw, t, iteration=0, logging=True): - """ - """ - #%% More imports and settings - site.addsitedir(os.path.join(sw['casedir'],'input_processing')) - import hourly_writetimeseries - newstresspath = f'stress{t}i{iteration+1}' - - #%% Write consolidated NEUE so far - try: - _neue_simple = get_and_write_neue(sw, write=True) - neue = get_annual_neue(sw.casedir, t, iteration=iteration) - neue.round(2).to_csv( - os.path.join(sw.casedir, 'outputs', f"neue_{t}i{iteration}.csv") - ) - - except Exception as err: - if int(sw['pras']) == 2: - print(traceback.format_exc()) - if int(sw.GSw_PRM_StressIterateMax): - raise Exception(err) - - #%% Stop here if not iterating or if before ReEDS can build new capacity - if (not int(sw.GSw_PRM_StressIterateMax)) or (t < int(sw['GSw_StartMarkets'])): - return - - #%% Identify and write new stress periods - failed, new_stressperiods_write, combined_periods_write = get_eue_sorted_periods( - sw=sw, t=t, iteration=iteration, - ) - - #%% Stop here if all thresholds pass or if there are no new stress periods - if ( - (not len(failed)) - or ((len(new_stressperiods_write) == 0) and (int(sw.GSw_PRM_UpdateMethod) == 0)) - ): - print('No new stress periods and no PRM update, so stopping here') - return - - #%% Write timeseries data for stress periods for the next iteration of ReEDS - hourly_writetimeseries.main( - sw=sw, reeds_path=sw['reeds_path'], - inputs_case=os.path.join(sw['casedir'], 'inputs_case'), - periodtype=newstresspath, - make_plots=0, - logging=logging - ) - - #%% Write updated PRM values - if ( - (int(sw.GSw_PRM_UpdateMethod) == 0) - or (len(new_stressperiods_write) and (int(sw.GSw_PRM_UpdateMethod) == 3)) - ): - ## Not updating PRM, so copy last year's - prm_next_iteration = pd.read_csv( - os.path.join(sw.casedir, 'inputs_case', f'stress{t}i{iteration}', 'prm.csv'), - index_col='*r', - ) - else: - prm_next_iteration = update_prm(sw, t, iteration, failed, combined_periods_write) - - prm_next_iteration.to_csv( - os.path.join(sw.casedir, 'inputs_case', newstresspath, 'prm.csv'), - ) - - #%% Done - return - - -# if __name__ == '__main__': -# #%%### option to run script directly for debugging -# casedir = "/path/to/ReEDS-2.0/runs/runname" -# t = 2030 # previous solve year -# iteration = 0 -# # load switches -# sw = reeds.io.get_switches(casedir) -# sw['t'] = t -# sw['GSw_PRM_UpdateMethod'] = 2 -# #%%### -# main(sw, t, iteration, logging=False) diff --git a/aws_setup.sh b/aws_setup.sh deleted file mode 100644 index fcc250f4..00000000 --- a/aws_setup.sh +++ /dev/null @@ -1,135 +0,0 @@ -# Some good instances -# m5a.12xlarge 48 x86_64 192 - - 10 Gigabit 2.064 USD per Hour -# r5a.24xlarge 96 x86_64 768 - - 20 Gigabit 5.424 USD per Hour - -#general process -#GIT installation -sudo yum -y install git -sudo amazon-linux-extras install epel -sudo yum -y install git-lfs -git lfs install - -#Append two export commands for GAMS and conda to .bashrc: -echo "export GAMSDIR=/opt/gams/gams35.1_linux_x64_64_sfx" >> ~/.bashrc -echo "export PATH=/home/ec2-user/anaconda3/bin/:$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/" >> ~/.bashrc - -#if this is a new disk, you need to establish a file system -sudo mkfs -t xfs /dev/xvdo -# copy over the gams license file from your local machine to the gams system directory -# this directory was created in the last step and varies with the gams installation version -#!!!! this needs to be changed for each user - both the directory names and gams license file -#!!!! following command need to be run from local terminal any time you mount or create a new file system on a drive -#scp -i MB_R2.pem -r /Users/mbrown1/Desktop/gamslice.txt ec2-user@172.18.32.113:~/r2/gamslice.txt - -#need to make a directory to mount that volume.. here just setting it up as ~/r2 -sudo mkdir ~/r2 -#then mount the directory to that folder: -#!!!! depends on what drive letter you assigned in ec2 web interface -#!!!! here i defined my drive as /dev/sdo -sudo mount /dev/sdo ~/r2 - -#make sure you have ownership of the drive and the opt directory (where we'll install GAMS): -sudo chown -R ec2-user ~/r2 -sudo chown -R ec2-user /opt -# make a directory for gams -mkdir /opt/gams -# change to that directory and download the gams installer -cd /opt/gams -#!!!! alternatively, this could be stored on your EBS drive and copied over -wget "https://d37drm4t2jghv5.cloudfront.net/distributions/35.1.0/linux/linux_x64_64_sfx.exe" - -# change permissions for the installation file -chmod 755 linux_x64_64_sfx.exe -# unpack the installation file -# not sure why the entire directory needs to spelled out here but it does... -/opt/gams/linux_x64_64_sfx.exe - -#copy over the gams license stored on your drive -cd gams35.1_linux_x64_64_sfx -nano ~/r2/gamslice.txt - -#add license file contents - -cp ~/r2/gamslice.txt gamslice.txt - -#export GAMSDIR for GDXPDS -export GAMSDIR=/opt/gams/gams35.1_linux_x64_64_sfx - -#installing anaconda -#following step only needs to be done if the installation file is not on your drive -cd ~/r2 -#!!!! alternatively, this could be stored on your EBS drive -wget "https://repo.anaconda.com/archive/Anaconda3-2020.11-Linux-x86_64.sh" - -#create a temporary directory given need to read/write from non-write-protected directory -mkdir ~/tmp -chown -R ec2-user ~/tmp -#actual installation call -#note setting up the temporary directory for this call and putting installation files -# in the /home/ec2-user/anaconda3 directory - the 'b' and 'p' arguments make it silent -# and indicate that the directory specified is the installation path -TMPDIR=~/tmp sh /home/ec2-user/r2/Anaconda3-2020.11-Linux-x86_64.sh -b -p /home/ec2-user/anaconda3 - -#ordering matters here - we want to be certain that -#the system sees the conda version of python before -#any other - both GAMS and otherwise -export PATH=/home/ec2-user/anaconda3/bin/:$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/ - -#make sure you have the appropriate packages installed -conda install pandas numpy scipy scikit-learn matplotlib networkx numba -pip install gdxpds - -#------------------------ -# -- git instructions -- -#------------------------ - -ssh-keygen -t rsa -b 4096 -C "youremailaddress@nlr.gov" -eval "$(ssh-agent -s)" -ssh-add id_rsa -#[copy key and add to your github.nrel.gov account] -ssh -T git@github.nrel.gov -#type yes -# should say Hi [username]! ... -git clone https://github.nrel.gov/ReEDS/ReEDS-2.0.git reeds - -git clone git@github.nrel.gov:ReEDS/ReEDS-2.0 - - -# Run ReEDS! -# (using nohup to keep the process from dying when you end your ssh session) -#nohup python runbatch_aws.py -c weekendcentroid -r 4 -b centwknd > myout.txt & - - -#======================================== -# -- old but potentially useful lines -- -#======================================== - -#Following lines needed if using the gams version of python... -#these export path lines could all be wrapped together -#but it helps me to break them out to avoid one big line -#export PATH=$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/ -#following instructs to use the gams version of python -#allows us to avoid installing/configuring conda -#ordering matters here! - we want the system to -#see the GAMS version of python first -#export PATH=/opt/gams/gams35.1_linux_x64_64_sfx/GMSPython:$PATH -#add python package directory for GAMS python to path: -#export PATH=$PATH:/opt/gams/gams35.1_linux_x64_64_sfx/GMSPython/bin -#can test to see if the following worked by typing: -#which python -#and should get something like: -#/opt/gams/gams35.1_linux_x64_64_sfx/GMSPython/python - -#install necessary python packages -#sudo yum install git -#need to manually install a base package for python -#that is not included with the gams version for some reason -#and is not available via pip ----- such a headache -#i've contacted GAMS on this.. working on a solution -#git clone https://github.com/python/cpython.git -#cp -r cpython/Lib/unittest/ /opt/gams/gams35.1_linux_x64_64_sfx/GMSPython/lib/python3.8/site-packages/unittest/ - -#move to your git directory -#!!! could be different for different users -#cd ~/r2/r2_aws - diff --git a/b_inputs.gms b/b_inputs.gms deleted file mode 100644 index 0d59e562..00000000 --- a/b_inputs.gms +++ /dev/null @@ -1,6774 +0,0 @@ -$title 'ReEDS 2.0' - -* Note - all dollar values are in 2004$ unless otherwise indicated -* It is our intention that there are no hard-coded values in b_inputs.gms -* but you will note that we still have some work to do to make that happen... - -*Setting the default directory separator -$setglobal ds \ - -$eolcom \\ - -*Change the default slash if in UNIX -$ifthen.unix %system.filesys% == UNIX -$setglobal ds / -$endif.unix - -*need to convert the 'unit' numhintage to some large value -$ifthen.unithintage %numhintage%=="unit" -$eval numhintage 300 -$endif.unithintage - -* Under the Clean Air Act, all coal plants are regulated individually. Therefore, we need a large value of hintages to represent these plants -$include inputs_case%ds%max_hintage_number.txt - -$ifthen.unithintage %GSw_Clean_Air_Act%==1 -$eval numhintage max_hintage_number -$endif.unithintage - -*need to convert the 'group' numhintage to some large value -$ifthen.unithintage %numhintage%=="group" -$eval numhintage 300 -$endif.unithintage - -* there are numeraire hintages on either sides of the outer breaks -* when using calcmethod = 1, here adding two for safety -* NB this will not increase model size given conditions -* dictating valcap and valgen for initial classes -$eval numhintage %numhintage% + 2 - -*$ontext -* --- print timing profile --- -option profile = 3 -option profiletol = 0 - -* --- suppress .lst file printing --- -* equations listed per block -option limrow = %debug% ; -* variables listed per block -option limcol = %debug% ; -* solver's solution output printed -option solprint = off ; -* solver's system output printed -option sysout = off ; -*$offtext - -set dummy "set used for initialization of numerical sets" / 0*10000 / ; -alias(dummy,adummy) ; - - -*====================== -* -- Local Switches -- -*====================== - -* Following are scalars used to turn on or off various components of the model. -* For binary switches, [0] is off and [1] is on. -* These switches are generated from the cases file in runbatch.py. -$include inputs_case%ds%gswitches.txt - -* Extra switches that are defined based on other switches -scalar Sw_Prod "Scalar value for whether Sw_H2 or Sw_DAC are enabled" ; -Sw_Prod$[Sw_H2 or Sw_DAC or Sw_DAC_Gas] = 1 ; - -set timetype "Type of time method used in the model" -/ seq, win, int / ; - -parameter Sw_Timetype(timetype) "Switch that specifies the type of time method used in the model" ; - -Sw_Timetype("%timetype%") = 1 ; - -* Sw_PCM is always 0 except when running d_solvepcm.gms, where it's set to 1 -scalar Sw_PCM "Internal switch used when running PCM mode" / 0 / ; -* Sw_MGA is always 0 except when running the optimization a second time for MGA, where it's set to 1 -scalar Sw_MGA "Internal switch used when running MGA mode" / 0 / ; - -*============================ -* --- Scalar Declarations --- -*============================ - -*year-related switches that define retirement and upgrade start dates -scalar retireyear "first year to allow capacity to start retiring" /%GSw_Retireyear%/ - upgradeyear "first year to allow capacity to upgrade" /%GSw_Upgradeyear%/ - climateyear "first year to apply climate impacts" /%GSw_ClimateStartYear%/ ; - -*** Scalars: copied from inputs/scalars.csv to inputs_case/scalars.txt in runbatch.py -$include inputs_case%ds%scalars.txt - -*========================== -* --- Set Declarations --- -*========================== - -*## Spatial sets (define first so case stays consistent) -* written by copy_files.py -$onOrder -set r "regions" -/ -$offlisting -$include inputs_case%ds%val_r.csv -$onlisting -/ ; -$offOrder - -$onempty -set offshore(r) "offshore zones" -/ -$offlisting -$include inputs_case%ds%offshore.csv -$onlisting -/ ; -$offempty - -set land(r) "land-based (not offshore) zones" ; -land(r)$[not offshore(r)] = yes ; - -* written by copy_files.py -$onempty -set cs(*) "carbon storage sites" -/ -$offlisting -$include inputs_case%ds%val_cs.csv -$onlisting -/ ; -$offempty - -* created in and mapped to hierarchy in recf.py -set ccreg "capacity credit regions" -/ -$offlisting -$include inputs_case%ds%ccreg.csv -$onlisting -/ ; - -set eall "emission categories used in reporting" -/ -$offlisting -$include inputs_case%ds%eall.csv -$onlisting -/ ; - -set e(eall) "emission categories used in model" -/ -$offlisting -$include inputs_case%ds%e.csv -$onlisting -/ ; - -set etype "emission types used in model (upstream and process)" -/ -$offlisting -$include inputs_case%ds%etype.csv -$onlisting -/ ; - -Sets -nercr "NERC regions" -* https://www.nerc.com/pa/RAPA/ra/Reliability%20Assessments%20DL/NERC_LTRA_2021.pdf -/ -* written by copy_files.py -$include inputs_case%ds%val_nercr.csv -/ - -transreg "Transmission Planning Regions from FERC order 1000" -* (https://www.ferc.gov/sites/default/files/industries/electric/indus-act/trans-plan/trans-plan-map.pdf) -/ -* written by copy_files.py -$include inputs_case%ds%val_transreg.csv -/, - -transgrp "sub-FERC-1000 regions" -/ -* written by copy_files.py -$include inputs_case%ds%val_transgrp.csv -/, - -itlgrp "ReEDS zones for additional ITL constraints when doing a run that includes county resolution" -/ -* written by copy_files.py -$include inputs_case%ds%val_itlgrp.csv -/, - -cendiv "census divisions" -/ -* written by copy_files.py -$include inputs_case%ds%val_cendiv.csv -/, - -interconnect "interconnection regions" -/ -* written by copy_files.py -$include inputs_case%ds%val_interconnect.csv -/, - -country "country regions" -/ -* written by copy_files.py -$include inputs_case%ds%val_country.csv -/, - -st "US, Mexico, and/or Canadian States/Provinces" -/ -* written by copy_files.py -$include inputs_case%ds%val_st.csv -/, - -* biomass supply curves defined by USDA region -usda_region "Biomass supply curve regions" -/ -* written by copy_files.py -$include inputs_case%ds%val_usda_region.csv -/, - -h2ptcreg "Regions which enforce the H2 production incentive regulations, for the US these are the National Transmission Needs Study regions" -* https://www.energy.gov/sites/default/files/2023-12/National%20Transmission%20Needs%20Study%20Supplemental%20Material%20-%20Final_2023.12.1.pdf -/ -* written by copy_files.py -$include inputs_case%ds%val_h2ptcreg.csv -/ - -* Hurdle rate regions -hurdlereg "Hurdle regions" -/ -$include inputs_case%ds%val_hurdlereg.csv -/ -; - -* Written by copy_files.py -$include b_sets.gms - -sets -*The following two sets: -*ban - will remove the technology from being considered, anywhere -*bannew - will remove the ability to invest in that technology - ban(i) "ban from existing, prescribed, and new generation -- usually indicative of missing data or operational constraints" - / - upv_10 -* csp-ns is "CSP, no storage". There is ~1.3 GW existing capacity but we group it with UPV and -* don't allow new builds of csp-ns. - csp-ns - other - unknown - geothermal - hydro - csp3_1*csp3_12 - csp4_1*csp4_12 - pumped-hydro-flex - hydED_pumped-hydro-flex - CoalOldUns_CoalOldScr - CoalOldUns_CofireOld - CoalOldScr_CofireOld -$ifthene.hydup not %GSw_HydroCapEnerUpgradeType% == 1 - hydUD - hydUND -$endif.hydup -$ifthene.hydup2 %GSw_HydroAddPumpDispUpgSwitch% == 0 - hydEND_hydED - hydED_pumped-hydro -$endif.hydup2 - /, - - bannew(i) "banned from creating new capacity, usually due to lacking data or representation" - / - can-imports - hydro - distpv - geothermal - cofireold - CoalOldScr - CoalOldUns - csp-ns -*you cannot build existing hydro... - HydEND - HydED - /, - -*Technologies with certain combinations of power technology, cooling technology, and -*water source are also banned from new capacity below after defining -*linking sets between i, ctt, and wst. - -*Data is insufficient to characterize new pond cooling systems, regulations effectively -*prohibit new once-through cooling - bannew_ctt(ctt) "banned ctt from creating new non-numeraire techs, usually due to lacking data or representation" - / - o - p - /, - - bannew_wst(wst) "banned wst from creating new non-numeraire techs, usually due to lacking data or representation" - / - fsl - ss - / ; - -alias(i,ii,iii) ; - -set i_water_nocooling(i) "technologies that use water, but are not differentiated by cooling tech and water source" -/ -$offlisting -$ondelim -$include inputs_case%ds%i_water_nocooling.csv -$offdelim -$onlisting -/ ; - -set i_water_cooling(i) "derived technologies from original technologies with cooling technologies other than just none", -*Hereafter numeraire techs in cooling-water context mean original technologies, -*like gas-CC, and non-numeraire techs mean techs that are derived from numeraire techs -*with cooling technology type and water source data appended to them, like gas-CC_r_fsa -*-- it is gas-CC with recirculating cooling and fresh surface appropriated water source. - i_water(i) "set of all technologies that use water for any purpose", - i_ii_ctt_wst(i,ii,ctt,wst) "linking set between non-numeraire techs, numeraire techs, cooling technology types, and water source types", -*linking sets extracted from i_ii_ctt_wst(i,ii,ctt,wst) that allow one-one mapping among dimensions - i_ctt(i,ctt) "linking set between non-numeraire techs and cooling technology types", - i_wst(i,wst) "linking set between non-numeraire techs and water source types", - wst_i_ii(i,ii) "linking set between non-numeraire techs and numeraire techs", - ctt_i_ii(i,ii) "linking set between non-numeraire techs and numeraire techs"; - -*input parameters for non-numeraire techs and linking set only if Sw_WaterMain is ON and start with a blank slate -i_water_cooling(i) = no ; -i_ii_ctt_wst(i,ii,ctt,wst) = no ; - -$ifthen.coolingwatersets %GSw_WaterMain% == 1 -set i_water_cooling_temp(i) -/ -$offlisting -$include inputs_case%ds%i_coolingtech_watersource.csv -$include inputs_case%ds%i_coolingtech_watersource_upgrades.csv -$onlisting -/, - - i_ii_ctt_wst_temp(i,ii,ctt,wst) -/ -$offlisting -$ondelim -$include inputs_case%ds%i_coolingtech_watersource_link.csv -$include inputs_case%ds%i_coolingtech_watersource_upgrades_link.csv -$offdelim -$onlisting -/ ; - -i_water_cooling(i)$i_water_cooling_temp(i) = yes ; -i_ii_ctt_wst(i,ii,ctt,wst)$i_ii_ctt_wst_temp(i,ii,ctt,wst) = yes ; -$endif.coolingwatersets - -i_water(i)$[i_water_cooling(i) or i_water_nocooling(i)] = yes ; - -*linking sets between non-numeraire techs, numeraire techs, cooling tech, and water source -i_ctt(i,ctt)$[sum{(ii,wst)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; -i_wst(i,wst)$[sum{(ii,ctt)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; -*wst_i_ii(i,ii) and ctt_i_ii(i,ii) are identical linking set between non-numeraire and numeraire techs, -*kept both for clarity of use in cooling technology and water source related formulations -wst_i_ii(i,ii)$[sum{(wst,ctt)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; -ctt_i_ii(i,ii)$[sum{(wst,ctt)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = YES ; - -set i_numeraire(i) "numeraire techs that need cooling" ; -*i_numeraire(i) will be removed from valcap set as these technologies are ultimately -*expanded to non-numeraire techs. valcap will have non-numeraire techs if Sw_WaterMain=1 -*or will have numeraire techs otherwise. -i_numeraire(ii)$sum{(wst,ctt,i)$i_ii_ctt_wst(i,ii,ctt,wst), 1 } = yes ; - -table ctt_hr_mult(i,ctt) "heatrate multipliers to differentiate cooling technology types" -$offlisting -$ondelim -$include inputs_case%ds%heat_rate_mult.csv -$offdelim -$onlisting -; - -table ctt_cc_mult(i,ctt) "capital cost multipliers to differentiate cooling technology types" -$offlisting -$ondelim -$include inputs_case%ds%cost_cap_mult.csv -$offdelim -$onlisting -; - -table ctt_cost_vom_mult(i,ctt) "VOM cost multipliers to differentiate cooling technology types" -$offlisting -$ondelim -$include inputs_case%ds%cost_vom_mult.csv -$offdelim -$onlisting -; - -set allt "all potential years" -/ -$offlisting -$include inputs_case%ds%allt.csv -$onlisting -/ ; - -set i_geotech(i,geotech) "crosswalk between an individual geothermal technology and its category" -/ -$offlisting -$ondelim -$include inputs_case%ds%i_geotech.csv -$offdelim -$onlisting -/ ; - -set -*technology-specific subsets - battery(i) "battery storage technologies", - beccs(i) "Bio with CCS", - bio(i) "technologies that use only biofuel", - boiler(i) "technologies that use steam boilers" - canada(i) "Canadian imports", - ccs(i) "CCS technologies", - ccs_mod(i) "CCS technologies with moderate capture rate", - ccs_max(i) "CCS technologies with maximum capture rate", - ccsflex_byp(i) "Flexible CCS technologies with bypass", - ccsflex_dac(i) "Flexible CCS technologies with direct air capture", - ccsflex_sto(i) "Flexible CCS technologies with storage", - ccsflex(i) "Flexible CCS technologies", - cf_tech(i) "technologies that have a specified capacity factor" - coal_ccs(i) "technologies that use coal and have CCS", - coal(i) "technologies that use coal", - cofire(i) "cofire technologies", - combined_cycle(i) "combined cycle technologies", - combustion_turbine(i)"combustion turbine technologies", - consume(i) "technologies that consume electricity and add to load", - conv(i) "conventional generation technologies", - csp_storage(i) "csp generation technologies with thermal storage", - csp(i) "csp generation technologies", - csp1(i) "csp-tes generation technologies 1", - csp2(i) "csp-tes generation technologies 2", - csp3(i) "csp-tes generation technologies 3", - csp4(i) "csp-tes generation technologies 4", - dac(i) "direct air capture technologies", - distpv(i) "distpv (i.e., rooftop PV) generation technologies", - demand_flex(i) "demand flexibility technologies (includes DR and EVMC)", - dr_shed(i) "DR shed technologies" - evmc(i) "ev flexibility technologies", - evmc_storage(i) "ev flexibility as direct load control", - evmc_shape(i) "ev flexibility as adoptable change to load from response to pricing", - fossil(i) "fossil technologies" - fuel_cell(i) "fuel cell technologies", - gas_cc_ccs(i) "techs that are gas combined cycle and have CCS", - gas_cc(i) "techs that are gas combined cycle", - gas_ct(i) "techs that are gas combustion turbine", - gas(i) "techs that use gas (but not o-g-s)", - geo(i) "geothermal technologies", - geo_base(i) "geothermal technologies typically considered in model runs", - geo_hydro(i) "geothermal hydrothermal technologies", - geo_egs(i) "geothermal enhanced geothermal systems technologies", - geo_extra(i) "geothermal technologies not typically considered in model runs", - geo_egs_allkm(i) "egs (covering deep egs depths of all km) technologies", - geo_egs_nf(i) "egs (near-field) technologies", - h2_combustion(i) "h2-ct and h2-cc technologies", - h2_cc(i) "h2-cc technologies" - h2_ct(i) "h2-ct technologies", - h2(i) "hydrogen-producing technologies", - hyd_add_pump(i) "hydro techs with an added pump", - hydro_d(i) "dispatchable hydro technologies", - hydro_nd(i) "non-dispatchable hydro technologies", - hydro(i) "hydro technologies", - lfill(i) "land-fill gas technologies", - nondispatch(i) "technologies that are not dispatchable" - nuclear(i) "nuclear technologies", - ofswind(i) "offshore wind technologies", - ogs(i) "oil-gas-steam technologies", - onswind(i) "onshore wind technologies", - psh(i) "pumped hydro storage technologies", - pv(i) "all PV generation technologies", - pvb(i) "hybrid pv+battery technologies", - pvb1(i) "pvb generation technologies 1", - pvb2(i) "pvb generation technologies 2", - pvb3(i) "pvb generation technologies 3", - re(i) "renewable energy technologies", - refurbtech(i) "technologies that can be refurbished", - rsc_i(i) "technologies based on Resource supply curves", - smr(i) "steam methane reforming technologies", - storage_hybrid(i) "hybrid VRE-storage technologies", - storage_standalone(i) "stand alone storage technologies", - storage(i) "storage technologies", - storage_interday(i) "interday storage", - thermal_storage(i) "thermal storage technologies", - upgrade(i) "technologies that are upgrades from other technologies", - upv(i) "upv generation technologies", - vre_distributed(i) "distributed PV technologies", - vre_no_csp(i) "variable renewable energy technologies that are not csp", - vre_utility(i) "utility scale wind and PV technologies", - vre(i) "variable renewable energy technologies", - wind(i) "wind generation technologies", - -t(allt) "full set of years" /%startyear%*%endyear%/, - -* Each generation technology is broken out by class: -* 1. initial capacity: init-1, init-2, ..., init-n -* 2. prescribed capacity: prescribed -* 3. new capacity: new -* This allows us to distinguish between existing, prescribed, and model-chosen builds -* The number of classes is set by numhintage for initial capacity and numclass for new capacity -v "technology class" - / - init-1*init-%numhintage%, - new1*new%numclass% - /, - -initv(v) "initial technologies" /init-1*init-%numhintage%/, - -newv(v) "new tech set" /new1*new%numclass%/ - -; - -* DAC == direct air capture -* H2 == hydrogen -* Note: no longer tracking H2 by color. This means ReEDS internalizes -* emissions for any H2 produced for non-power sector demands -set p "products produced" -/ -$offlisting -$include inputs_case%ds%p.csv -$onlisting -/ ; - - -hyd_add_pump('hydED_pumped-hydro') = yes ; -hyd_add_pump('hydED_pumped-hydro-flex') = yes ; - -* Sets involved with resource supply curve definitions -set sc_cat "supply curve categories (capacity and cost)" -/ -$offlisting -$include inputs_case%ds%sc_cat.csv -$onlisting -/ ; - -set rscbin "Resource supply curves bins" /bin1*bin%numbins%/, - rscfeas(i,r,rscbin) "feasibility set for technologies that have resource supply curves" ; - -alias(r,rr,n,nn) ; -alias(v,vv) ; -alias(t,tt,ttt) ; -alias(st,ast) ; -alias(allt,alltt) ; -alias(cendiv,cendiv2) ; -alias(rscbin,arscbin) ; -alias(nercr,nercrr) ; -alias(transgrp,transgrpp) ; -alias(itlgrp,itlgrpp) ; - -parameter yeart(t) "numeric value for year", - year(allt) "numeric year value for allt" ; - -yeart(t) = t.val ; -year(allt) = allt.val ; - -set att(allt,t) "mapping set between allt and t" ; -att(allt,t)$[year(allt) = yeart(t)] = yes ; - -*the end year is defined dynamically -*if %end_year% < annual data, we are gonna get into trouble... -*if you aint first you're last -set tfirst(t) "first year", - tlast(t) "last year" ; - -*aint first you're last -tfirst(t)$[ord(t) = 1] = yes ; -tlast(t)$[ord(t) = smax(tt,ord(tt))] = yes ; - - -parameter deflator(allt) "Deflator values (for inflation) calculated from http://www.usinflationcalculator.com/inflation/consumer-price-index-and-annual-percent-changes-from-1913-to-2008/ using the Avg-Avg values" -/ -$offlisting -$ondelim -$include inputs_case%ds%deflator.csv -$offdelim -$onlisting -/ ; - -*various parameters needed for Present Value Factor (PVF) calculations before solving -*specifically these are used in the aggregating of the PVF of -*onm and capital when years are skipped -set yearafter "set to loop over for the final year calculation" -/ -$offlisting -$include inputs_case%ds%yearafter.csv -$onlisting -/ ; - -* --- Upgrade link definitions --- -Set upgrade_to(i,ii) "mapping set that allows for i to be upgraded to ii" - upgrade_from(i,ii) "mapping set that allows for i to be upgraded from ii" - upgrade_link(i,ii,iii) "indicates that tech i is upgradeable from ii with a delta base of iii" -/ -$offlisting -$ondelim -$include inputs_case%ds%upgrade_link.csv -$ifthen.ctech %GSw_WaterMain% == 1 -$include inputs_case%ds%upgradelink_water.csv -$endif.ctech -$offdelim -$onlisting -/ ; - -upgrade(i)$[sum{(ii,iii), upgrade_link(i,ii,iii) }] = yes ; -upgrade_to(i,ii)$[sum{iii, upgrade_link(i,iii,ii) }] = yes ; -upgrade_from(i,ii)$[sum{iii, upgrade_link(i,ii,iii) }] = yes ; - -set unitspec_upgrades(i) "upgraded technologies that get unit-specific characteristics" -/ -$offlisting -$ondelim -$include inputs_case%ds%unitspec_upgrades.csv -$offdelim -$onlisting -/ ; - -unitspec_upgrades(i)$[sum{ii$ctt_i_ii(i,ii), unitspec_upgrades(ii) }$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), unitspec_upgrades(ii) } ; - -* Ban upgrades if upgrades are turned off -ban(i)$[upgrade(i)$(not Sw_Upgrades)] = yes ; -bannew(i)$[upgrade(i)$(not Sw_Upgrades)] = yes ; - -* --- Read technology subset lookup table --- -Table i_subsets(i,i_subtech) "technology subset lookup table" -$offlisting -$ondelim -$include inputs_case%ds%tech-subset-table.csv -$offdelim -$onlisting -; - -*approach in cooling water formulation is populating parameters of numeraire tech (e.g. gas-CC) -*for non-numeraire techs (e.g. gas-CC_r_fsa; r = recirculating cooling, fsa=fresh surface appropriated water source) -*e.g. populate i_subsets for non-numeraire techs from numeraire tech using a linking set ctt_i_ii(i,ii) -i_subsets(i,i_subtech)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), i_subsets(ii,i_subtech) } ; - -*assign subtechs to each upgrade tech -*based on what they will be upgraded to -i_subsets(i,i_subtech)$[upgrade(i)$Sw_Upgrades] = - sum{ii$upgrade_to(i,ii), i_subsets(ii,i_subtech) } ; - -** define tech bans so that they are not defined in the technology subsets below ** -* switch based gen tech bans (see cases file for details) -if(Sw_BECCS = 0, - ban('beccs_mod') = yes ; - ban('beccs_max') = yes ; -) ; - -if(Sw_Biopower = 0, - ban('biopower') = yes ; -) ; - -if(Sw_Canada <> 1, - ban('can-imports') = yes ; -) ; - -if(Sw_CCS = 0, - ban(i)$i_subsets(i,'ccs') = yes ; -) ; - -if(Sw_CCSFLEX_BYP = 0, - ban('Gas-CC-CCS-F1') = yes ; - ban('coal-CCS-F1') = yes ; -) ; - -if(Sw_CCSFLEX_STO = 0, - ban('Gas-CC-CCS-F2') = yes ; - ban('coal-CCS-F2') = yes ; -) ; - -if(Sw_CCSFLEX_DAC = 0, - ban('Gas-CC-CCS-F3') = yes ; - ban('coal-CCS-F3') = yes ; -) ; - -if(Sw_CSP = 0, - ban(i)$i_subsets(i,'csp') = yes ; -) ; - -if(Sw_CSP = 1, - ban(i)$i_subsets(i,'csp2') = yes ; -) ; - -if(Sw_CoalIGCC = 0, - ban('Coal-IGCC') = yes ; -) ; - -if(Sw_CoalNew = 0, - ban('coal-new') = yes ; -) ; - -if(Sw_CofireNew = 0, - ban('CofireNew') = yes ; -) ; - -if(Sw_DAC = 0, - ban(i)$i_subsets(i,'dac') = yes ; -) ; - -if(Sw_DAC_Gas = 0, - ban("dac_gas") = yes ; -); - -if(Sw_EVMC = 0, - ban(i)$i_subsets(i,'evmc') = yes ; -) ; - -if(Sw_GasCT_Aero = 0, - ban('Gas-CT_aero') = yes ; -) ; - -if(Sw_GasCC_H_1x1 = 0, - ban('Gas-CC_H_1x1') = yes ; - ban('Gas-CC_H_1x1-CCS_mod') = yes ; - ban('Gas-CC_H_1x1-CCS_max') = yes ; -) ; - -if(Sw_GasCC_H_2x1 = 0, - ban('Gas-CC_H_2x1') = yes ; - ban('Gas-CC_H_2x1-CCS_mod') = yes ; - ban('Gas-CC_H_2x1-CCS_max') = yes ; -) ; - -if(Sw_Geothermal = 0, - ban(i)$i_subsets(i,'geo') = yes ; -) ; - -if(Sw_Geothermal = 1, - ban(i)$i_subsets(i,'geo_extra') = yes ; -) ; - -if(Sw_H2 = 0, - ban(i)$i_subsets(i,'h2') = yes ; -) ; - -if(Sw_H2_SMR = 0, - ban(i)$i_subsets(i,'smr') = yes ; -) ; - -if(Sw_H2Combustion = 0, - ban(i)$i_subsets(i,'h2_combustion') = yes ; -) ; - -if(Sw_H2CombinedCycle = 0, - ban(i)$i_subsets(i,'h2_cc') = yes ; -) ; - -if(Sw_H2Combustionupgrade = 0, - ban(i)$[i_subsets(i,'h2_combustion')$upgrade(i)] = yes ; -) ; - -if(Sw_FuelCell = 0, - ban(i)$i_subsets(i,'fuel_cell') = yes ; -) ; - -if(Sw_LfillGas = 0, - ban('lfill-gas') = yes ; -) ; - -if(Sw_MaxCaptureCCSTechs = 0, - ban(i)$[i_subsets(i,'ccs_max')] = yes ; -) ; - -if(Sw_Nuclear = 0, - bannew(i)$i_subsets(i,'nuclear') = yes ; -) ; - -if(Sw_NuclearSMR = 0, - ban("Nuclear-SMR") = yes ; -) ; - -if(Sw_OfsWind = 0, - ban(i)$i_subsets(i,'ofswind') = yes ; -) ; - -if(Sw_OnsWind6to10 = 0, - bannew('wind-ons_6') = yes ; - bannew('wind-ons_7') = yes ; - bannew('wind-ons_8') = yes ; - bannew('wind-ons_9') = yes ; - bannew('wind-ons_10') = yes ; -) ; - -if(Sw_DRShed = 0, - ban(i)$i_subsets(i,'DR_SHED') = yes ; -) ; - -* always allow PSH to use fresh surface water (fsa, fsu) -* do not allow new PSH to use saline surface water -bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'ss') }] = YES ; -$ifthen.pshwat %GSw_PSHwatertypes% == 0 -* do not allow saline ground water or wastewater effluent -bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'sg') }] = YES ; -bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'ww') }] = YES ; -$elseif.pshwat %GSw_PSHwatertypes% == 1 -* option to also prohibit fresh groundwater -bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'sg') }] = YES ; -bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'ww') }] = YES ; -bannew(i)$[sum{wst_i_ii(i,ii)$i_subsets(i,'psh'), i_wst(i,'fg') }] = YES ; -$elseif.pshwat %GSw_PSHwatertypes% == 2 -* option 2 allows fresh/saline ground water and wastewater -$else.pshwat -$endif.pshwat - -*** Ban hybrid storage techs based on Sw_HybridPlant switch -* 0: Ban all storage, including CSP -if(Sw_HybridPlant = 0, - ban(i)$i_subsets(i,'storage_hybrid') = yes ; -) ; -* 1: Allow CSP, ban all other storage -if(Sw_HybridPlant = 1, - ban(i)$[i_subsets(i,'storage_hybrid')$(not sameas(i,'csp_storage'))] = yes ; - ban(i)$i_subsets(i,'csp_storage') = no ; -) ; -* 2: Allow hybrid plants, excluding CSP -if(Sw_HybridPlant = 2, - ban(i)$[i_subsets(i,'storage_hybrid')$(not sameas(i,'csp_storage'))] = no ; - ban(i)$i_subsets(i,'csp_storage') = yes ; -) ; -* 3: Allow CSP and all other hybrid plants (note csp_storage bans are controlled by Sw_CSP) -if(Sw_HybridPlant = 3, - ban(i)$[i_subsets(i,'storage_hybrid')$(not sameas(i,'csp_storage'))] = no ; -) ; - -*ban techs in hybrid PV+battery if the switch calls for it -if(Sw_PVB=0, - ban(i)$i_subsets(i,'pvb') = yes ; - bannew(i)$i_subsets(i,'pvb') = yes ; -) ; - -* Ban PVB_Types that aren't included in the model -$ifthen.pvb123 %GSw_PVB_Types% == '1_2' - ban(i)$i_subsets(i,'pvb3') = yes ; -$endif.pvb123 -$ifthen.pvb12 %GSw_PVB_Types% == '1' - ban(i)$i_subsets(i,'pvb2') = yes ; - ban(i)$i_subsets(i,'pvb3') = yes ; -$endif.pvb12 - -*** Ban storage techs based on Sw_Storage switch -* 0: Ban all storage -if(Sw_Storage = 0, - ban(i)$i_subsets(i,'storage_standalone') = yes ; - Sw_BatteryMandate = 0 ; -) ; -* 1: Keep all storage - -if(Sw_WaterMain = 1, -*By default, ban new builds with bannew_ctt cooling techs for all i, -bannew(i)$[sum{ctt$bannew_ctt(ctt), i_ctt(i,ctt) }] = YES ; - -* ban new builds of Nuclear and coal-CCS with dry cooling techs as cooling requirements -* of nuclear and coal-CCS make dry cooling impractical -bannew(i)$[sum{ctt_i_ii(i,'Nuclear'), i_ctt(i,'d') }] = YES ; -bannew(i)$[sum{ctt_i_ii(i,'coal-CCS_mod'), i_ctt(i,'d') }] = YES ; -bannew(i)$[sum{ctt_i_ii(i,'coal-CCS_max'), i_ctt(i,'d') }] = YES ; -bannew(i)$[sum{ctt_i_ii(i,'Nuclear-SMR'), i_ctt(i,'d') }] = YES ; - -*ban and bannew all non-numeraire techs that are derived from ban numeraire techs -ban(i)$sum{ii$ban(ii), ctt_i_ii(i,ii) } = YES ; -bannew(i)$sum{ii$bannew(ii), ctt_i_ii(i,ii) } = YES ; - -* ban new builds of water sources included in bannew_wst for all i -bannew(i)$[sum{wst$bannew_wst(wst), i_wst(i,wst) }] = YES ; -* end parentheses for Sw_WaterMain = 1 -) ; - -* Turn off canadian imports as an option when running NARIS -$ifthen.naris %GSw_Region% == "naris" - ban(i)$i_subsets(i,'canada') = yes ; -$endif.naris - -* Ban DUPV, CSP, and Geothermal resources that do not remain after aggregation -set resourceclass "renewable resource classes" -/ -$offlisting -$include inputs_case%ds%resourceclass.csv -$onlisting -/ ; -parameter resourceclassnum(resourceclass) "numeric value for resource class" ; -resourceclassnum(resourceclass) = resourceclass.val ; -set tech_resourceclass(i,resourceclass) "map from CSP/DUPV techs to resource classes" -/ -$offlisting -$ondelim -$include inputs_case%ds%tech_resourceclass.csv -$offdelim -$onlisting -/ ; -* There are 12 CSP resource classes by default. If Sw_NumCSPclasses < 12, we ban the -* CSP techs with resource class > Sw_NumCSPclasses -if(Sw_NumCSPclasses < 12, -ban(i)$[i_subsets(i,'csp') - $sum{resourceclass$tech_resourceclass(i,resourceclass), - resourceclassnum(resourceclass)>Sw_NumCSPclasses }] = yes ; -) ; -* If Sw_CSPRemoveLow is turned on, remove the last (worst) CSP class (which will be -* equal to Sw_NumCSPclasses) -if(Sw_CSPRemoveLow = 1, -ban(i)$[i_subsets(i,'csp') - $sum{resourceclass$tech_resourceclass(i,resourceclass), - resourceclassnum(resourceclass)=Sw_NumCSPclasses }] = yes ; -) ; - -*Ban Geothermal resources that do not remain after aggregation -if(Sw_NumGeoclasses < 10, -ban(i)$[i_subsets(i,'geo') - $sum{resourceclass$tech_resourceclass(i,resourceclass), - resourceclassnum(resourceclass)>Sw_NumGeoclasses }] = yes ; -) ; - -*Ingest list of new nuclear restricted BAs ('p' regions), ba list is consistent with NCSL restrictions. -*https://www.ncsl.org/research/environment-and-natural-resources/states-restrictions-on-new-nuclear-power-facility.aspx -$onempty -set nuclear_ba_ban(r) "List of BAs where new nuclear builds are restricted" -/ -$offlisting -$include inputs_case%ds%nuclear_ba_ban_list.csv -$onlisting -/ ; -$offempty - -* techs banned by state (note that this only applies to valinv later) -$onempty -table tech_banned(i,r) "Banned technologies by model region" -$offlisting -$ondelim -$include inputs_case%ds%techs_banned.csv -$offdelim -$onlisting -; -$offempty - -* --- Remove banned technologies from upgrade links --- -upgrade_link(i,ii,iii)$[ban(i) or ban(ii) or ban(iii)] = no ; -upgrade(i)$[not sum{(ii,iii), upgrade_link(i,ii,iii) }] = no ; -upgrade_to(i,ii)$[not sum{iii, upgrade_link(i,iii,ii) }] = no ; -upgrade_from(i,ii)$[not sum{iii, upgrade_link(i,ii,iii) }] = no ; - -* --- define technology subsets --- -battery(i)$(not ban(i)) = yes$i_subsets(i,'battery') ; -beccs(i)$(not ban(i)) = yes$i_subsets(i,'beccs') ; -bio(i)$(not ban(i)) = yes$i_subsets(i,'bio') ; -boiler(i)$(not ban(i)) = yes$i_subsets(i,'boiler') ; -canada(i)$(not ban(i)) = yes$i_subsets(i,'canada') ; -ccs(i)$(not ban(i)) = yes$i_subsets(i,'ccs') ; -ccs_mod(i)$(not ban(i)) = yes$i_subsets(i,'ccs_mod') ; -ccs_max(i)$(not ban(i)) = yes$i_subsets(i,'ccs_max') ; -ccsflex_byp(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex_byp') ; -ccsflex_dac(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex_dac') ; -ccsflex_sto(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex_sto') ; -ccsflex(i)$(not ban(i)) = yes$i_subsets(i,'ccsflex') ; -cf_tech(i)$(not ban(i)) = yes$i_subsets(i,'cf_tech') ; -coal_ccs(i)$(not ban(i)) = yes$i_subsets(i,'coal_ccs') ; -coal(i)$(not ban(i)) = yes$i_subsets(i,'coal') ; -cofire(i)$(not ban(i)) = yes$i_subsets(i,'cofire') ; -combined_cycle(i)$(not ban(i)) = yes$i_subsets(i,'combined_cycle') ; -combustion_turbine(i)$(not ban(i)) = yes$i_subsets(i,'combustion_turbine') ; -consume(i)$(not ban(i)) = yes$i_subsets(i,'consume') ; -conv(i)$(not ban(i)) = yes$i_subsets(i,'conv') ; -csp_storage(i)$(not ban(i)) = yes$i_subsets(i,'csp_storage') ; -csp(i)$(not ban(i)) = yes$i_subsets(i,'csp') ; -csp1(i)$(not ban(i)) = yes$i_subsets(i,'csp1') ; -csp2(i)$(not ban(i)) = yes$i_subsets(i,'csp2') ; -csp3(i)$(not ban(i)) = yes$i_subsets(i,'csp3') ; -csp4(i)$(not ban(i)) = yes$i_subsets(i,'csp4') ; -dac(i)$(not ban(i)) = yes$i_subsets(i,'dac') ; -distpv(i)$(not ban(i)) = yes$i_subsets(i,'distpv') ; -dr_shed(i)$(not ban(i)) = yes$i_subsets(i,'dr_shed') ; -demand_flex(i)$(not ban(i)) = yes$i_subsets(i,'demand_flex') ; -evmc(i)$(not ban(i)) = yes$i_subsets(i,'evmc') ; -evmc_storage(i)$(not ban(i)) = yes$i_subsets(i,'evmc_storage') ; -evmc_shape(i)$(not ban(i)) = yes$i_subsets(i,'evmc_shape') ; -fossil(i)$(not ban(i)) = yes$i_subsets(i,'fossil') ; -fuel_cell(i)$(not ban(i)) = yes$i_subsets(i,'fuel_cell') ; -gas_cc_ccs(i)$(not ban(i)) = yes$i_subsets(i,'gas_cc_ccs') ; -gas_cc(i)$(not ban(i)) = yes$i_subsets(i,'gas_cc') ; -gas_ct(i)$(not ban(i)) = yes$i_subsets(i,'gas_ct') ; -gas(i)$(not ban(i)) = yes$i_subsets(i,'gas') ; -geo(i)$(not ban(i)) = yes$i_subsets(i,'geo') ; -geo_base(i)$(not ban(i)) = yes$i_subsets(i,'geo_base') ; -geo_hydro(i)$(not ban(i)) = yes$i_subsets(i,'geo_hydro') ; -geo_egs(i)$(not ban(i)) = yes$i_subsets(i,'geo_egs') ; -geo_extra(i)$(not ban(i)) = yes$i_subsets(i,'geo_extra') ; -geo_egs_allkm(i)$(not ban(i)) = yes$i_subsets(i,'geo_egs_allkm') ; -geo_egs_nf(i)$(not ban(i)) = yes$i_subsets(i,'geo_egs_nf') ; -h2_combustion(i)$(not ban(i)) = yes$i_subsets(i,'h2_combustion') ; -h2_cc(i)$(not ban(i)) = yes$i_subsets(i,'h2_cc') ; -h2_ct(i)$(not ban(i)) = yes$i_subsets(i,'h2_ct') ; -h2(i)$(not ban(i)) = yes$i_subsets(i,'h2') ; -hydro_d(i)$(not ban(i)) = yes$i_subsets(i,'hydro_d') ; -hydro_nd(i)$(not ban(i)) = yes$i_subsets(i,'hydro_nd') ; -hydro(i)$(not ban(i)) = yes$i_subsets(i,'hydro') ; -lfill(i)$(not ban(i)) = yes$i_subsets(i,'lfill') ; -nondispatch(i)$(not ban(i)) = yes$i_subsets(i,'nondispatch') ; -nuclear(i)$(not ban(i)) = yes$i_subsets(i,'nuclear') ; -ofswind(i)$(not ban(i)) = yes$i_subsets(i,'ofswind') ; -ogs(i)$(not ban(i)) = yes$i_subsets(i,'ogs') ; -onswind(i)$(not ban(i)) = yes$i_subsets(i,'onswind') ; -psh(i)$(not ban(i)) = yes$i_subsets(i,'psh') ; -pv(i)$(not ban(i)) = yes$i_subsets(i,'pv') ; -pvb(i)$(not ban(i)) = yes$i_subsets(i,'pvb') ; -pvb1(i)$(not ban(i)) = yes$i_subsets(i,'pvb1') ; -pvb2(i)$(not ban(i)) = yes$i_subsets(i,'pvb2') ; -pvb3(i)$(not ban(i)) = yes$i_subsets(i,'pvb3') ; -re(i)$(not ban(i)) = yes$i_subsets(i,'re') ; -refurbtech(i)$(not ban(i)) = yes$i_subsets(i,'refurbtech') ; -rsc_i(i)$(not ban(i)) = yes$i_subsets(i,'rsc') ; -smr(i)$(not ban(i)) = yes$i_subsets(i,'smr') ; -storage_hybrid(i)$(not ban(i)) = yes$i_subsets(i,'storage_hybrid') ; -storage_interday(i)$(not ban(i)) = yes$i_subsets(i,'storage_interday') ; -storage_standalone(i)$(not ban(i)) = yes$i_subsets(i,'storage_standalone') ; -storage(i)$(not ban(i)) = yes$i_subsets(i,'storage') ; -thermal_storage(i)$(not ban(i)) = yes$i_subsets(i,'thermal_storage') ; -upv(i)$(not ban(i)) = yes$i_subsets(i,'upv') ; -vre_distributed(i)$(not ban(i)) = yes$i_subsets(i,'vre_distributed') ; -vre_no_csp(i)$(not ban(i)) = yes$i_subsets(i,'vre_no_csp') ; -vre_utility(i)$(not ban(i)) = yes$i_subsets(i,'vre_utility') ; -vre(i)$(not ban(i)) = yes$i_subsets(i,'vre') ; -wind(i)$(not ban(i)) = yes$i_subsets(i,'wind') ; - -set coal_noccs(i) "technologies that use coal and do not have CCS, aka unabated coal" ; -coal_noccs(i)$[coal(i)$(not ccs(i))] = yes ; - -* Create mapping of technology groups to technologies -set tg_i(tg,i) "technologies that belong in tech group tg" ; -tg_i('wind-ons',i)$onswind(i) = yes ; -tg_i('wind-ofs',i)$ofswind(i) = yes ; -tg_i('pv',i)$[(pv(i) or pvb(i))$(not distpv(i))] = yes ; -tg_i('csp',i)$csp(i) = yes ; -tg_i('gas',i)$gas(i) = yes ; -tg_i('coal',i)$coal(i) = yes ; -tg_i('nuclear',i)$nuclear(i) = yes ; -tg_i('battery',i)$battery(i) = yes ; -tg_i('hydro',i)$hydro(i) = yes ; -tg_i('h2',i)$h2_combustion(i) = yes ; -tg_i('geothermal',i)$geo(i) = yes ; -tg_i('biomass',i)$bio(i) = yes ; -tg_i('pumped-hydro',i)$psh(i) = yes ; -tg_i('dr_shed',i)$dr_shed(i) = yes ; - -*Hybrid pv+battery (PVB) configurations are defined by: -* (1) inverter loading ratio (DC/AC) and -* (2) battery capacity ratio (Battery/PV Array) -*Each configuration has ten resource classes -*The PV portion refers to "UPV", but not "DUPV" -*The battery portion refers to "battery_li" -set pvb_config "set of hybrid pv+battery configurations" -/ -$offlisting -$include inputs_case%ds%pvb_config.csv -$onlisting -/ ; - -set pvb_agg(pvb_config,i) "crosswalk between hybrid pv+battery configurations and technology options" -/ -$offlisting -$ondelim -$include inputs_case%ds%pvb_agg.csv -$offdelim -$onlisting -/ ; - -*add non-numeraire CSPs in index i of already defined set tg_i(tg,i) -tg_i("csp",i)$[(csp1(i) or csp2(i) or csp3(i) or csp4(i))$Sw_WaterMain] = yes ; - -*Offhsore wind turbine types -set ofstype "offshore types used in offshore requirement constraint (eq_RPS_OFSWind)" -/ -$offlisting -$include inputs_case%ds%ofstype.csv -$onlisting -/ ; - -set ofstype_i(ofstype,i) "crosswalk between ofstype and i" -/ -$offlisting -$ondelim -$include inputs_case%ds%ofstype_i.csv -$offdelim -$onlisting -/ ; - -storage_interday(i)$(Sw_InterDayLinkage = 0) = no ; - -$onempty -parameter water_with_cons_rate(i,ctt,w) "--gal/MWh-- technology specific-cooling tech based water withdrawal and consumption data" -/ -$offlisting -$ondelim -$include inputs_case%ds%water_with_cons_rate.csv -$offdelim -$onlisting -/ -; -$offempty - -$onempty -* Water requirement if all filling takes place in 1 year and minimum reservoir level is 15% of max volume -table water_req_psh(r,rscbin) "--Mgal/MW/yr-- required water for PSH during construction to fill reservoir" -$offlisting -$ondelim -$include inputs_case%ds%water_req_psh_10h_1_51.csv -$offdelim -$onlisting -; -$offempty - -* Recalculate PSH water requirements based on user input filling time -scalar psh_fillyrs "number of years assumed to fill PSH reservoirs" /%GSw_PSHfillyears%/ ; -water_req_psh(r,rscbin) = round(water_req_psh(r,rscbin) / psh_fillyrs, 6) ; - -*populate the water withdrawal and consumption data to non-numeraire technologies -*based on numeraire techs and cooling technologies types to avoid repetitive -*entry of data in the input data file and provide flexibility -*in populating data if new combinations come along the way -water_with_cons_rate(i,ctt,w)$i_water_cooling(i) = - sum{(ii,wst)$i_ii_ctt_wst(i,ii,ctt,wst), water_with_cons_rate(ii,ctt,w) } ; - -*CSP techs have same water withdrawal and consumption rates; populating all CSP data with the data of csp1_1 -water_with_cons_rate(i,ctt,w)$[i_water_cooling(i)$(csp1(i) or csp2(i) or csp3(i) or csp4(i))] = - sum{(ii,wst)$i_ii_ctt_wst(i,ii,ctt,wst), water_with_cons_rate("csp1_1",ctt,w) } ; - -water_with_cons_rate(ii,ctt,w)$[sum{(wst,i)$i_ii_ctt_wst(i,ii,ctt,wst), 1 }] = no ; - -parameter water_rate(i,w) "--gal/MWh-- water withdrawal/consumption w rate by technology i" ; -* adding geothermal categories for water accounting -i_water(i)$geo(i) = yes ; -water_with_cons_rate(i,ctt,w)$geo(i) = water_with_cons_rate("geothermal",ctt,w) ; - -* Till this point, i already has non-numeraire techs (e.g., gas-CC_o_fsa, gas-CC_r_fsa, -*and gas-CC_r_fg) instead of numeraire technology (e.g., gas-CC) -* The line below just removes ctt dimension, by summing over ctt. -water_rate(i,w)$i_water(i) = sum{ctt, water_with_cons_rate(i,ctt,w) } ; - -water_rate(i,w)$upgrade(i) = sum{ii$upgrade_to(i,ii), water_rate(ii,w) } ; - -set dispatchtech(i) "technologies that are dispatchable", - noret_upgrade_tech(i) "upgrade techs that do not retire", - retiretech(i,v,r,t) "combinations of i,v,r,t that can be retired", - sccapcosttech(i) "technologies that have their capital costs embedded in supply curves", - inv_cond(i,v,r,t,tt) "allows an investment in tech i of class v to be built in region r in year tt and usable in year t" ; - -noret_upgrade_tech(i)$hyd_add_pump(i) = yes ; -noret_upgrade_tech(i)$[(coal_ccs(i) or gas_cc_ccs(i))$upgrade(i)$Sw_CCS_NoRetire] = yes ; -dispatchtech(i)$[not(vre(i) or hydro_nd(i) or ban(i))] = yes ; -sccapcosttech(i)$[hydro(i) or psh(i) or dr_shed(i)] = yes ; - -*initialize sets to "no" -retiretech(i,v,r,t) = no ; -inv_cond(i,v,r,t,tt) = no ; - -parameter min_retire_age(i) "minimum retirement age by technology" -/ -$offlisting -$ondelim -$include inputs_case%ds%min_retire_age.csv -$offdelim -$onlisting -/ ; - -min_retire_age(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), min_retire_age(ii) } ; -* if GSw_Clean_Air_Act is enabled, there is no minimum retire age for coal plants -min_retire_age(i)$[coal(i)$Sw_Clean_Air_Act] = no ; - -parameter retire_penalty(allt) "--fraction-- penalty for retiring a power plant expressed as a fraction of FOM" -/ -$offlisting -$ondelim -$include inputs_case%ds%retire_penalty.csv -$offdelim -$onlisting - / ; - - -set prescriptivelink0(pcat,ii) "initial set of prescribed categories and their technologies - used in assigning prescribed builds" -/ -$offlisting -$ondelim -$include inputs_case%ds%prescriptivelink0.csv -$offdelim -$onlisting -/ ; - -*include non-numeraire CSPs and then exclude numeraire CSPs in ii dimension of -*prescriptivelink0(pcat,ii) set when Sw_WaterMain is ON -prescriptivelink0("csp-ws",ii)$[(csp1(ii) or csp2(ii) or csp3(ii) or csp4(ii))$Sw_WaterMain] = yes ; -prescriptivelink0("csp-ws",ii)$[csp(ii)$i_numeraire(ii)$Sw_WaterMain] = no ; - -set prescriptivelink(pcat,i) "final set of prescribed categories and their technologies - used in the model" ; - -prescriptivelink(pcat,i)$prescriptivelink0(pcat,i) = yes ; - -alias(pcat,ppcat) ; - -* active prescriptivelink for all techs not included in the table above -* but restrict out csp techs in this calculation - since they -* are indexed by a separate pcat (csp-ws) and have special considerations -prescriptivelink(pcat,i)$[sameas(pcat,i)$(not sum{ppcat, prescriptivelink(ppcat,i) })$(not csp1(i))] = yes ; -*only geo_hydro techs are considered to meet geothermal prescriptions -prescriptivelink(pcat,i)$[geo_extra(i)] = no ; - - -*upgrades have no prescriptions -prescriptivelink(pcat,i)$[upgrade(i)] = no ; - -set rsc_agg(i,ii) "rsc technologies that belong to the same class" ; - -set tg_rsc_cspagg(i,ii) "csp technologies that belong to the same class" -/ -$offlisting -$ondelim -$include inputs_case%ds%tg_rsc_cspagg.csv -$offdelim -$onlisting -/ ; - -set tg_rsc_cspagg_tmp(i,ii) "expanded tg_rsc_cspagg(i,ii) to include new non-numeraire CSP techs" ; - -*input parameters for linking set only when Sw_WaterMain is ON and start with a blank slate -tg_rsc_cspagg_tmp(i,ii) = no ; -$ifthen.coolingwatersets %GSw_WaterMain% == 1 -set tg_rsc_cspagg_tmp_temp(i,ii) -/ -$offlisting -$ondelim -$include inputs_case%ds%tg_rsc_cspagg_tmp.csv -$offdelim -$onlisting -/ ; -tg_rsc_cspagg_tmp(i,ii)$tg_rsc_cspagg_tmp_temp(i,ii) = yes ; -$endif.coolingwatersets - -*include non-numeraire CSPs and then exclude numeraire CSPs in ii dimension -*of tg_rsc_cspagg(i,ii) set when Sw_WaterMain is ON -tg_rsc_cspagg(i,ii)$[tg_rsc_cspagg_tmp(i,ii)$Sw_WaterMain] = yes ; -tg_rsc_cspagg(i,ii)$[csp(ii)$i_numeraire(ii)$Sw_WaterMain] = no ; - -$ontext -Replicating the construct for CSP to link Hybrid PV+battery and UPV for the resoruce supply curve constraints - eq_rsc_invlim(i,bin).. sum{ii$rsc_agg(i,ii), INV_RSC(i,bin) } <= bin_capacity(i,bin) ; - When i = "upv_1", this constraint looks like: - eq_rsc_invlim("upv_1",bin).. INV_RSC("pvb1_1") + INV_RSC("upv_1") <= bin_capacity("upv_1",bin) - Because the first index of rsc_agg is only a UPV technology the above constraint will never be generated when "i" is a pvb(i). -$offtext - -set tg_rsc_upvagg(i,ii) "pv and pvb technologies that belong to the same class" -/ -$offlisting -$ondelim -$include inputs_case%ds%tg_rsc_upvagg.csv -$offdelim -$onlisting -/ ; - -*initialize rsc aggregation set for 'i'='ii' -*rsc_agg(i,ii)$[sameas(i,ii)$(not csp(i))$(not csp(ii))$rsc_i(i)$rsc_i(ii)] = yes ; -rsc_agg(i,ii)$[sameas(i,ii)$rsc_i(i)$rsc_i(ii)] = yes ; -*add csp to rsc aggregation set -rsc_agg(i,ii)$tg_rsc_cspagg(i,ii) = yes ; -*add upv to rsc aggregation set -rsc_agg(i,ii)$tg_rsc_upvagg(i,ii) = yes ; -*All PSH types use the same supply curve -rsc_agg('pumped-hydro',ii)$psh(ii) = yes ; -rsc_agg(i,ii)$[ban(i) or ban(ii)] = no ; - -*============================ -* -- Demand flexibility setup -- -*============================ - -set flex_type "set of demand flexibility types: daily, previous, next, adjacent" -/ -$offlisting -$include inputs_case%ds%flex_type.csv -$onlisting -/ ; - -*====================================== -* --- Begin hierarchy --- -*====================================== - -set hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) "hierarchy of various regional definitions" -/ -$offlisting -$ondelim -$include inputs_case%ds%hierarchy.csv -$offdelim -$onlisting -/ ; - - -* Mappings between r and other region sets -set r_itlgrp(r,itlgrp) - r_nercr(r,nercr) - r_transreg(r,transreg) - r_transgrp(r,transgrp) - r_cendiv(r,cendiv) - r_st(r,st) - r_interconnect(r,interconnect) - r_country(r,country) - r_usda(r,usda_region) - r_h2ptcreg(r,h2ptcreg) - r_hurdlereg(r,hurdlereg) - r_ccreg(r,ccreg) -; - -r_nercr(r,nercr) $sum{( transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_transreg(r,transreg) $sum{(nercr, transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_transgrp(r,transgrp) $sum{(nercr,transreg, cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_cendiv(r,cendiv) $sum{(nercr,transreg,transgrp, st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_st(r,st) $sum{(nercr,transreg,transgrp,cendiv, interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_interconnect(r,interconnect) $sum{(nercr,transreg,transgrp,cendiv,st, country,usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_country(r,country) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect, usda_region,h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_usda(r,usda_region) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country, h2ptcreg,hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_h2ptcreg(r,h2ptcreg) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region, hurdlereg,ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_hurdlereg(r,hurdlereg) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg, ccreg) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; -r_ccreg(r,ccreg) $sum{(nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg ) $hierarchy(r,nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg,hurdlereg,ccreg),1} = yes ; - -set r_itlgrp(r,itlgrp) "mapping of r to itlgrp" -/ -$offlisting -$ondelim -$include inputs_case%ds%hierarchy_itlgrp.csv -$offdelim -$onlisting -/ ; - -* Region hierarchy level within which to site optimally sited load -alias(%GSw_LoadSiteReg%, loadsitereg) ; -set r_loadsitereg(r,loadsitereg) "Mapping from model zones to loadsite regions" ; -r_loadsitereg(r,%GSw_LoadSiteReg%) = r_%GSw_LoadSiteReg%(r,%GSw_LoadSiteReg%) ; - - -*================================ -*sets that define model boundaries -*================================ -set tmodel(t) "years to include in the model", - tfix(t) "years to fix variables over when summing over previous years", - tprev(t,tt) "previous modeled tt from year t", - stfeas(st) "states to include in the model", - tsolved(t) "years that have solved" ; - -*following parameters get re-defined when the solve years have been declared -parameter mindiff(t) "minimum difference between t and all other tt that are in tmodel(t)" ; - - -tmodel(t) = no ; -tfirst(t) = no ; -tlast(t) = no ; -tfix(t) = no ; -stfeas(st) = no ; -tprev(t,tt) = no ; -tsolved(t) = no ; - - -*============================== -* Year specification -*============================== - -* declared over allt to allow for external data files that extend beyond end_year -set tmodel_new(allt) "years to run the model" -/ -$offlisting -$include inputs_case%ds%modeledyears.csv -$onlisting -/ ; - -tmodel_new(allt)$[year(allt) > %endyear%]= no ; - -*reset the first and last year indices of the model -tfirst(t)$[ord(t) = smin{tt$tmodel_new(tt), ord(tt) }] = yes ; -tlast(t)$[ord(t) = smax{tt$tmodel_new(tt), ord(tt) }] = yes ; - -*now get rid of all non-immediately-previous values (it takes three steps to get there...) -tprev(t,tt)$[tmodel_new(t)$tmodel_new(tt)$(tt.valmindiff(t))] = no ; - -* In order to fill all necessary dimensions of upgrade techs parameters, we require -* Sw_UpgradeYear in ban(i) to be a modeled year and thus we compute as either -* the GSw_UpgradeYear option or the next modeled years after GSw_UpgradeYear - -* reset sw_upgradeyear -Sw_UpgradeYear = 0 ; - -* if the upgradeyear is modeled set it to upgrade year -Sw_UpgradeYear$tmodel_new("%GSw_UpgradeYear%") = %GSw_UpgradeYear% ; - -* if upgrade year is not modeled, set it to the next available upgrade year -Sw_UpgradeYear$[(not Sw_UpgradeYear)] = - smin(tt$[(tt.val>=%GSw_UpgradeYear%)$tmodel_new(tt)],tt.val) ; - - -* if caa_coal_retire_year is not in the set of years being modeled, then set it to the first year that is modeled after caa_coal_retire_year -caa_coal_retire_year$[not sum{tt$[tt.val=caa_coal_retire_year], tmodel_new(tt) }] = - smin(tt$[(tt.val>=caa_coal_retire_year)$tmodel_new(tt)],tt.val) ; - -* if Sw_Clean_Air_Act = 0, then set caa_coal_retire_year to the last solve year -caa_coal_retire_year$[Sw_Clean_Air_Act = 0] = smax(tmodel_new, tmodel_new.val) ; - -*====================================== -* ---------- Bintage Mapping ---------- -*====================================== -*following set is un-assumingly important -*it allows for the investment of bintage 'v' at time 't' - -*table ivtmap(i,t) -* declared over allt to allow for external data files that extend beyond end_year -table ivt_num(i,allt) "number associated with bin for ivt calculation" -$offlisting -$ondelim -$include inputs_case%ds%ivt.csv -$offdelim -$onlisting -; - - -set ivt(i,v,t) "mapping set between i v and t - for new technologies" ; -ivt(i,newv,t)$[ord(newv) = ivt_num(i,t)] = yes ; - -*Expand ivt to water techs -ivt(i,v,t)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), ivt(ii,v,t) } ; - -*Also expand ivt_num to water techs for use in Augur -ivt_num(i,t)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), ivt_num(ii,t) } ; - - -*important assumption here that upgrade technologies -*receive the same binning assumptions as the technologies -*that they are upgraded to - this allows for easier translation -*and mapping of plant characteristics (cost_vom, cost_fom, heat_rate) -ivt(i,newv,t)$[(yeart(t)>=Sw_UpgradeYear)$upgrade(i)] = - sum{ii$upgrade_to(i,ii), ivt(ii,newv,t) } ; - - -parameter countnc(i,newv) "number of years in each newv set" ; - -*add 1 for each t item in the ct_corr set -countnc(i,newv) = sum{t$ivt(i,newv,t),1} ; - -set one_newv(i) "technologies that only have one vintage for new plants" ; - -* Only one vintage is present if there is a new1 for that technology... -one_newv(i)$[sum{t, ivt(i,"new1",t) }] = yes ; -* ...and there are no entries other than new1 for that technology -one_newv(i)$sum{(v,t)$[not sameas(v,"new1")], ivt(i,v,t) } = no ; - -*===================================== -*--- basic parameter declarations --- -*===================================== - -parameter crf(t) "--unitless-- capital recovery factor" -/ -$offlisting -$ondelim -$include inputs_case%ds%crf.csv -$offdelim -$onlisting -/, - crf_co2_incentive(t) "--unitless-- capital recovery factor using a 12-year economic lifetime" -/ -$offlisting -$ondelim -$include inputs_case%ds%crf_co2_incentive.csv -$offdelim -$onlisting -/, - - crf_h2_incentive(t) "--unitless-- capital recovery factor using a 10-year economic lifetime" -/ -$offlisting -$ondelim -$include inputs_case%ds%crf_h2_incentive.csv -$offdelim -$onlisting -/, - -* pvf_capital and pvf_onm here are for intertemporal mode. These parameters -* are overwritten for sequential mode in d_solveprep.gms. - pvf_capital(t) "--unitless-- present value factor for overnight capital costs" -/ -$offlisting -$ondelim -$include inputs_case%ds%pvf_cap.csv -$offdelim -$onlisting -/, - pvf_onm(t)"--unitless-- present value factor of operations and maintenance costs" -/ -$offlisting -$ondelim -$include inputs_case%ds%pvf_onm_int.csv -$offdelim -$onlisting -/, - tc_phaseout_mult(i,v,t) "--unitless-- multiplier that reduces the value of the PTC and ITC after the phaseout trigger has been hit", - tc_phaseout_mult_t(i,t) "--unitless-- a single year's multiplier of tc_phaseout_mult", - tc_phaseout_mult_t_load(i,t) "--unitless-- a single year's multiplier of tc_phaseout_mult", - co2_captured_incentive(i,v,r,allt) "--$/tco2 stored-- incentive on CO2 captured dependent on technology" - co2_captured_incentive_in(i,v,allt) "--$/tco2 stored-- incentive on CO2 captured dependent on technology" -/ -$offlisting -$ondelim -$include inputs_case%ds%co2_capture_incentive.csv -$offdelim -$onlisting -/, - - h2_ptc(i,v,r,allt) "--2004$/kg h2 produced -- incentive on hydrogen production by electrolyzers which purchase Energy Attribute Credits" - h2_ptc_in(i,v,allt) "--2004$/kg h2 produced -- incentive on hydrogen production by electrolyzers which purchase Energy Attribute Credits, this parameter is used to build h2_ptc and is produced in input_processing/calc_financial_inputs.py" -/ -$offlisting -$ondelim -$include inputs_case%ds%h2_ptc.csv -$offdelim -$onlisting -/, - - ptc_value_scaled(i,v,allt) "--$/MWh-- value of the PTC incorporating adjustments for monetization costs, tax grossup benefits, and the difference between ptc duration and reeds evaluation period" -/ -$offlisting -$ondelim -$include inputs_case%ds%ptc_value_scaled.csv -$offdelim -$onlisting -/, - pvf_onm_undisc(t) "--unitless-- undiscounted present value factor of operations and maintenance costs" -; - -ptc_value_scaled(i,v,t)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), ptc_value_scaled(ii,v,t) } ; - -parameter firstyear_v(i,v) "flag for first year that a new new vintage can be built" ; -parameter lastyear_v(i,v) "flag for the last year that a new new vintage can be built" ; - -firstyear_v(i,v) = sum{t$[yeart(t)=smin(tt$ivt(i,v,tt),yeart(tt))], yeart(t) } ; -lastyear_v(i,v) = sum{t$[yeart(t)=smax(tt$ivt(i,v,tt),yeart(tt))], yeart(t) } ; - -* pvf_onm_undisc is based on intertemporal pvf_onm and pvf_capital, -* and is used for bulk system cost outputs -pvf_onm_undisc(t)$pvf_capital(t) = pvf_onm(t) / pvf_capital(t) ; - -*========================================== -* --- Technology start years --- -*========================================== - -* Note that some techs have a dummy firstyear of 2500 -parameter firstyear(i) "first year where new investment is allowed" -/ -$offlisting -$ondelim -$include inputs_case%ds%firstyear.csv -$offdelim -$onlisting -/ ; - -*---Add first year that capacity can be built: -firstyear(i)$[(firstyear(i) < firstyear_min)$firstyear(i)] = firstyear_min ; - -scalar co2_detail_startyr "--year-- Year to start the detailed representation of CO2 capture/storage" ; -co2_detail_startyr = smin{i$[ccs(i)$firstyear(i)], firstyear(i) } ; - -*========================================== - -scalar model_builds_start_yr "--integer-- Start year allowing new generators to be built" ; - -*Ignore gas units because gas-ct's are allowed in historical years -model_builds_start_yr = smin{i$[(not gas_ct(i))$(not distpv(i))$(not upgrade(i))$(not ban(i))$firstyear(i)], firstyear(i) } ; - -firstyear(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), firstyear(ii) } ; - -firstyear(i)$[not firstyear(i)] = model_builds_start_yr ; -firstyear(i)$[i_water_cooling(i)$(not Sw_WaterMain)] = NO ; -firstyear(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), firstyear(ii) } ; - -parameter firstyear_pcat(pcat) ; -firstyear_pcat(pcat)$[sum{i$[sameas(i,pcat)$(not ban(i))], firstyear(i) }] = sum{i$sameas(i,pcat), firstyear(i) } ; -firstyear_pcat("upv") = firstyear("upv_1") ; -firstyear_pcat("wind-ons") = firstyear("wind-ons_1") ; -firstyear_pcat("wind-ofs") = firstyear("wind-ofs_1") ; -firstyear_pcat("csp-ws") = firstyear("csp2_1") ; -firstyear_pcat("geohydro_allkm") = firstyear("geohydro_allkm_1") ; -firstyear_pcat("egs_allkm") = firstyear("egs_allkm_1") ; - - -*============================== -* Region specification -*============================== - -*set the state feasibility set -*determined by which regions are feasible -stfeas(st)$[sum{r$r_st(r,st), 1 }] = yes ; - - -*========================== -* -- existing capacity -- -*========================== - -*Begin loading of capacity data -parameter poi_cap_init(r) "--MW-- initial (pre-2010) capacity of all types" -/ -$offlisting -$ondelim -$include inputs_case%ds%poi_cap_init.csv -$offdelim -$onlisting -/ ; - -*created by /input_processing/writecapdat.py -table capnonrsc(i,r,*) "--MW-- raw power capacity data for non-RSC tech created by .\input_processing\writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%capnonrsc.csv -$offdelim -$onlisting -; - -*created by /input_processing/writecapdat.py -$onempty -table capnonrsc_energy(i,r,*) "--MWh-- raw energy capacity data for battery tech created by .\input_processing\writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%capnonrsc_energy.csv -$offdelim -$onlisting -; -$offempty - -*created by /input_processing/writecapdat.py -$onempty -table caprsc(pcat,r,*) "--MW-- raw RSC capacity data, created by .\writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%caprsc.csv -$offdelim -$onlisting -; -$offempty - -*created by /input_processing/writecapdat.py -* declared over allt to allow for external data files that extend beyond end_year -$onempty -table prescribednonrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for non-RSC tech created by writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%prescribed_nonRSC.csv -$offdelim -$onlisting -; -$offempty - -$onempty -table prescribednonrsc_energy(allt,pcat,r,*) "--MWh-- raw prescribed energy capacity data for non-RSC tech created by writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%prescribed_nonRSC_energy.csv -$offdelim -$onlisting -; -$offempty - -*Created using input_processing\writecapdat.py -$onempty -table prescribedrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for RSC tech created by .\input_processing\writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%prescribed_rsc.csv -$offdelim -$onlisting -; -$offempty - -$onempty -*For onshore and offshore wind, use outputs of hourlize to override what is in prescribedrsc -table prescribed_wind_ons(r,allt,*) "--MW-- prescribed wind capacity, created by hourlize" -$offlisting -$ondelim -$include inputs_case%ds%prescribed_builds_wind-ons.csv -$offdelim -$onlisting -; -$offempty - -prescribedrsc(allt,"wind-ons",r,"value") = prescribed_wind_ons(r,allt,"capacity") ; - -$onempty -table prescribed_wind_ofs(r,allt,*) "--MW-- prescribed wind capacity, created by hourlize" -$offlisting -$ondelim -$include inputs_case%ds%prescribed_builds_wind-ofs.csv -$offdelim -$onlisting -; -$offempty - -prescribedrsc(allt,"wind-ofs",r,"value") = prescribed_wind_ofs(r,allt,"capacity") ; - -*created by /input_processing/writecapdat.py -*following does not include wind -*Retirements for techs binned by heatrates are handled in hintage_data.csv -$onempty -table prescribedretirements(allt,r,i,*) "--MW-- raw prescribed power capacity retirement data for non-RSC, non-heatrate binned tech created by /input_processing/writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%retirements.csv -$offdelim -$onlisting -; -$offempty - -*created by /input_processing/writecapdat.py -*Retirements for techs binned by heatrates are handled in hintage_data.csv -$onempty -table prescribedretirements_energy(allt,r,i,*) "--MWh-- raw prescribed energy capacity retirement data for battery tech created by /input_processing/writecapdat.py" -$offlisting -$ondelim -$include inputs_case%ds%retirements_energy.csv -$offdelim -$onlisting -; -$offempty - -$onempty -parameter forced_retirements(i,st) "--integer-- year in which to force retirements of certain techs by state" -/ -$offlisting -$ondelim -$include inputs_case%ds%forced_retirements.csv -$offdelim -$onlisting -/ ; -$offempty - -set forced_retire(i,r,t) ; - -forced_retire(i,r,t)$[sum{st$r_st(r,st), (yeart(t)>=forced_retirements(i,st))$forced_retirements(i,st) }] = yes ; -* If the technology you would upgrade to is part of forced_retire, then include the -* upgrade tech in forced_retire -forced_retire(i,r,t)$[upgrade(i)$(sum{ii$upgrade_to(i,ii), forced_retire(ii,r,t) })] = yes ; - -set hintage_char "characteristics available in hintage_data" -/ -$offlisting -$include inputs_case%ds%hintage_char.csv -$onlisting -/ ; - -*created by /input_processing/writehintage.py -table hintage_data(i,v,r,allt,hintage_char) "table of existing unit characteristics written by writehintage.py" -$offlisting -$ondelim -$include inputs_case%ds%hintage_data.csv -$offdelim -$onlisting -; - -* if not updating heat rate on upgrades, change to the default value -if((not Sw_UpgradeHeatRateAdj), - hintage_data(i,initv,r,t,"wCCS_Retro_HR")$hintage_data(i,initv,r,t,"wCCS_Retro_HR") - = hintage_data(i,initv,r,t,"wHR") ; -) ; - -set upgrade_hintage_char(hintage_char) "sets to operate over in extension of hintage_data characteristics when sw_upgrades = 1" -/ -$offlisting -$ondelim -$include inputs_case%ds%upgrade_hintage_char.csv -$offdelim -$onlisting -/ ; - -* need to extend characteristics for years where a tech could still exist if it was upgraded in a previous year -* - ie a hintages characteristics would need to persist if it is upgraded and has a lifetime extension -if(Sw_Upgrades = 1, -* need to loop over the model years as we set values to the previous modeled year that will -* also need to be updated given the check for whether data exists in current year or not - loop(tt$tmodel_new(tt), -* if there is still capacity for upgradeable units in Sw_UpgradeYear -* make sure to extend their characteristics out beyond Sw_UpgradeYear - hintage_data(i,v,r,tt,upgrade_hintage_char)$[sum{ii,upgrade_from(ii,i) } - $sum{ttt$[ttt.val = Sw_UpgradeYear],hintage_data(i,v,r,ttt,"cap") } - $(not hintage_data(i,v,r,tt,upgrade_hintage_char))] - -* set to the previous modeled year relative to the looped year - = sum{ttt$tprev(tt,ttt), hintage_data(i,v,r,ttt,upgrade_hintage_char) } ; - ) ; -) ; - -*created by /input_processing/writecapdat.py -parameter binned_capacity(i,v,r,allt) "existing capacity (that is not rsc, but including distpv) binned by heat rates" ; - -binned_capacity(i,v,r,allt) = hintage_data(i,v,r,allt,"cap") ; - -parameter maxage(i) "--years-- maximum age for technologies" -/ -$offlisting -$ondelim -$include inputs_case%ds%maxage.csv -$offdelim -$onlisting -/ ; -* generators not included in maxage.csv get maxage=100 years -maxage(i)$[not maxage(i)] = maxage_default ; -* upgrades and cooling-water techs inherit maxage from the base tech -maxage(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), maxage(ii) } ; -maxage(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), maxage(ii) } ; - -*loading in capacity mandates here to avoid conflicts in calculation of valcap -* declared over allt to allow for external data files that extend beyond end_year -$onempty -parameter batterymandate(st,allt) "--MW-- cumulative battery mandate levels" -/ -$offlisting -$ondelim -$include inputs_case%ds%storage_mandates.csv -$offdelim -$onlisting -/ ; -$offempty - -scalar firstyear_battery "--year-- the first year battery technologies can be built, used to enforce storage mandate" ; -firstyear_battery = smin(i$battery(i),firstyear(i)) ; - -$onempty -table offshore_cap_req(st,allt) "--MW-- offshore wind capacity requirement by state" -$offlisting -$ondelim -$include inputs_case%ds%offshore_req.csv -$offdelim -$onlisting -; -$offempty - -parameter r_offshore(r,t) "regions where offshore wind is required by a mandate" ; -r_offshore(r,t)$[sum{st$r_st(r,st), offshore_cap_req(st,t) }] = 1 ; - -* initial smr capacity to ensure that exogenous H2 demand can be supplied, csv is written by writecapdat.py -$onempty -parameter h2_existing_smr_cap(r,t) "--MW-- capacity of existing SMR - used for meeting H2 demand before new H2 producing tech deployment is allowed to begin" -/ -$offlisting -$ondelim -$include inputs_case%ds%h2_existing_smr_cap.csv -$offdelim -$onlisting -/ ; -$offempty - -*========================================== -* --- Canadian Imports/Exports --- -*========================================== - -$ifthene.Canada %GSw_Canada% == 1 -* declared over allt to allow for external data files that extend beyond end_year -$onempty -table can_imports(r,allt) "--MWh-- [Sw_Canada=1] Imports from Canada by year" -$offlisting -$ondelim -$include inputs_case%ds%can_imports.csv -$offdelim -$onlisting -; - -parameter can_imports_capacity(r,allt) "--MW-- [Sw_Canada=1] Peak Canadian import capacity" -/ -$offlisting -$ondelim -$include inputs_case%ds%can_imports_capacity.csv -$offdelim -$onlisting -/ ; - -table can_exports(r,allt) "--MWh-- [Sw_Canada=1] Exports to Canada by year" -$offlisting -$ondelim -$include inputs_case%ds%can_exports.csv -$offdelim -$onlisting -; -$offempty - -$endif.Canada - - - -*============================= -* Resource supply curve setup -*============================= - -* Written by writesupplycurves.py -parameter rsc_dat(i,r,sc_cat,rscbin) "--units vary-- resource supply curve data for renewables with capacity in MW and costs in $/MW (MW-DC and $/MW-AC for UPV)" -/ -$offlisting -$ondelim -$include inputs_case%ds%rsc_combined.csv -$offdelim -$onlisting -/ ; - - -* Written by writesupplycurves.py -$onempty -parameter geo_discovery_factor(i,r) "--fraction-- factor representing undiscovered geothermal" -/ -$offlisting -$ondelim -$include inputs_case%ds%geo_discovery_factor.csv -$offdelim -$onlisting -/ ; -$offempty - -* Written by writesupplycurves.py -$onempty -parameter geo_discovery_rate(allt) "--fraction-- fraction of undiscovered geothermal that has been 'discovered'" -/ -$offlisting -$ondelim -$include inputs_case%ds%geo_discovery_rate.csv -$offdelim -$onlisting -/ ; -$offempty - -parameter geo_discovery(i,r,allt) "--fraction-- fraction of undiscovered geothermal that has been 'discovered'" ; -geo_discovery(i,r,t)$geo_hydro(i) = (1 - geo_discovery_factor(i,r)) * geo_discovery_rate(t) + geo_discovery_factor(i,r) ; - -* read data defining increase in hydropower upgrade availability over time. should only exist for hydUD and hydUND -$onempty -table hyd_add_upg_cap(r,i,rscbin,allt) "--MW-- cumulative increase in available upgrade capacity relative to base year" -$offlisting -$ondelim -$include inputs_case%ds%hyd_add_upg_cap.csv -$offdelim -$onlisting -; -$offempty - -parameter distance_spur(i,r,rscbin) "--miles-- Spur line distance" -/ -$offlisting -$ondelim -$include inputs_case%ds%distance_spur.csv -$offdelim -$onlisting -/ ; - -parameter distance_reinforcement(i,r,rscbin) "--miles-- Network reinforcement distance" -/ -$offlisting -$ondelim -$include inputs_case%ds%distance_reinforcement.csv -$offdelim -$onlisting -/ ; - -**rsc_dat adjustments (see additional adjustments to m_rsc_dat further below) - -*need to adjust units for pumped hydro costs from $ / KW to $ / MW -rsc_dat("pumped-hydro",r,"cost",rscbin) = rsc_dat("pumped-hydro",r,"cost",rscbin) * 1000 ; - -*need to adjust units for hydro costs from $ / KW to $ / MW -rsc_dat(i,r,"cost",rscbin)$hydro(i) = rsc_dat(i,r,"cost",rscbin) * 1000 ; - -*To allow pumped-hydro-flex via rscfeas and m_rscfeas, we set its supply curve capacity equal to pumped-hydro fixed. -*Note however that they will share the same supply curve capacity (see rsc_agg). -rsc_dat("pumped-hydro-flex",r,"cap",rscbin) = rsc_dat("pumped-hydro",r,"cap",rscbin) ; - -*Make pumped-hydro-flex more expensive than fixed pumped-hydro by a fixed percent -rsc_dat("pumped-hydro-flex",r,"cost",rscbin) = rsc_dat("pumped-hydro",r,"cost",rscbin) * %GSw_HydroVarPumpCostRatio% ; - -$ontext -Replicate the UPV supply curve data for hybrid PV+battery -"rsc_data" for hybrid PV+battery is never used in the resource constraint (see note above about rsc_dat and tg_rsc_upvagg). -This copy is necessary to ensure the conditionals for the supply curve investment variables get created for pvb. -Example: "m_rscfeas(r,i,rscbin)" is created for "eq_rsc_inv_account" -$offtext - -rsc_dat(i,r,sc_cat,rscbin)$pvb(i) = sum{ii$[upv(ii)$rsc_agg(ii,i)], rsc_dat(ii,r,sc_cat,rscbin) } ; - -*following set indicates which combinations of r and i are possible -*this is based on whether or not the bin has capacity available -rscfeas(i,r,rscbin)$rsc_dat(i,r,"cap",rscbin) = yes ; - -rscfeas(i,r,rscbin)$[csp2(i)$sum{ii$[csp1(ii)$csp2(i)$tg_rsc_cspagg(ii,i)], rscfeas(ii,r,rscbin) }] = yes ; -rscfeas(i,r,rscbin)$[csp3(i)$sum{ii$[csp1(ii)$csp3(i)$tg_rsc_cspagg(ii,i)], rscfeas(ii,r,rscbin) }] = yes ; -rscfeas(i,r,rscbin)$[csp4(i)$sum{ii$[csp1(ii)$csp4(i)$tg_rsc_cspagg(ii,i)], rscfeas(ii,r,rscbin) }] = yes ; - -*expand feasibility and supply curve data for water-enumerated PSH techs -*m_rsc_con will still require all PSH types to use the same resource base -rscfeas(i,r,rscbin)$[psh(i)$Sw_WaterMain$sum{ii$ctt_i_ii(i,ii), rsc_dat(ii,r,"cap",rscbin) }] = yes ; - -rscfeas(i,r,rscbin)$ban(i) = no ; - - -* This flag will deactivate eq_rsc_INVLIM when the RHS is < 1e-6 and set INV_RSC -* to zero for the r,i,rscbin combination. Because INV_RSC is a positive variable -* and RHS < 1e-6, INV_RSC would have to be < 1e-6 (which is basically zero). -set flag_eq_rsc_INVlim(r,i,rscbin,t) "flag for when there are small numbers in the RHS of eq_rsc_INVlim" ; -parameter rhs_eq_rsc_INVlim(r,i,rscbin,t) "RHS value of eq_rsc_INVlim" ; - -*Initialize values to 'no' -flag_eq_rsc_INVlim(r,i,rscbin,t) = no ; - -parameter binned_heatrates(i,v,r,allt) "--MMBtu / MWh-- existing capacity binned by heat rates" ; -binned_heatrates(i,v,r,allt) = hintage_data(i,v,r,allt,"wHR") ; - - -*Created by hourlize -*declared over allt to allow for external data files that extend beyond end_year -* Written by writesupplycurves.py -$onempty -parameter exog_wind_ons_rsc(i,r,rscbin,allt) "exogenous (pre-tfirst) wind capacity binned by capacity factor and rscbin" -/ -$offlisting -$ondelim -$include inputs_case%ds%exog_wind_ons_rsc.csv -$offdelim -$onlisting -/ ; -$offempty - -parameter exog_wind_ons(i,r,allt) "exogenous (pre-tfirst) wind capacity binned by capacity factor" ; -exog_wind_ons(i,r,t) = sum{rscbin, exog_wind_ons_rsc(i,r,rscbin,t) } ; - -*Created by hourlize -*declared over allt to allow for external data files that extend beyond end_year -* Written by writesupplycurves.py -$onempty -parameter exog_upv_rsc(i,r,rscbin,allt) "exogenous (pre-tfirst) upv capacity binned by capacity factor and rscbin" -/ -$offlisting -$ondelim -$include inputs_case%ds%exog_upv_rsc.csv -$offdelim -$onlisting -/ ; -$offempty - -parameter exog_upv(i,r,allt) "exogenous (pre-tfirst) upv capacity binned by capacity factor" ; -exog_upv(i,r,t) = sum{rscbin, exog_upv_rsc(i,r,rscbin,t) } ; - -parameter avail_retire_exog_rsc(i,v,r,t) "--MW-- available retired capacity for refurbishments" ; -avail_retire_exog_rsc(i,v,r,t) = 0 ; - -* declared over allt to allow for external data files that extend beyond end_year -$onempty -parameter capacity_exog(i,v,r,allt) "--MW-- exogenously specified capacity", - capacity_exog_energy(i,v,r,allt) "--MWh-- exogenously specified energy capacity", - capacity_exog_rsc(i,v,r,rscbin,allt) "--MW-- exogenous (pre-tfirst) capacity for wind-ons and upv", - m_capacity_exog(i,v,r,allt) "--MW-- exogenous power capacity used in the model", - m_capacity_exog_energy(i,v,r,allt) "--MWh-- exogenous energy capacity used in the model", - geo_cap_exog(i,r) "--MW-- existing geothermal capacity" -/ -$offlisting -$ondelim -$include inputs_case%ds%geoexist.csv -$offdelim -$onlisting -/ ; -$offempty - -set exog_rsc(i) "RSC techs whose exogenous (pre-tfirst) capacity is tracked by rscbin" ; -exog_rsc(i)$(onswind(i)) = yes ; -exog_rsc(i)$(upv(i)) = yes ; - -*Created by hourlize -*declared over allt to allow for external data files that extend beyond end_year -* Written by writesupplycurves.py -$onempty -parameter exog_geohydro_allkm_rsc(i,r,rscbin,allt) "exogenous (pre-tfirst) geohydro_allkm capacity binned by temperature and rscbin" -/ -$offlisting -$ondelim -$ifthene.readgeohydrorevexog ((%GSw_Geothermal%<>0)and(sameas(%geohydrosupplycurve%,reV))) -$include inputs_case%ds%exog_geohydro_allkm_rsc.csv -$endif.readgeohydrorevexog -$offdelim -$onlisting -/ ; -$offempty - -*reset all geothermal exogenous capacity levels -capacity_exog(i,v,r,t)$geo(i) = 0 ; - -$ifthen.geohydrorevexog %geohydrosupplycurve% == 'reV' -parameter exog_geohydro_allkm(i,r,allt) "exogenous (pre-tfirst) geohydro_allkm capacity binned by temperature" ; -exog_geohydro_allkm(i,r,t) = sum{rscbin, exog_geohydro_allkm_rsc(i,r,rscbin,t) } ; -exog_rsc(i)$(geo_hydro(i)) = yes ; -capacity_exog(i,"init-1",r,t)$geo_hydro(i) = exog_geohydro_allkm(i,r,t) ; -capacity_exog_rsc(i,"init-1",r,rscbin,t)$geo_hydro(i) = exog_geohydro_allkm_rsc(i,r,rscbin,t) ; -$else.geohydrorevexog -capacity_exog(i,"init-1",r,t)$geo_hydro(i) = geo_cap_exog(i,r) ; -$endif.geohydrorevexog - -capacity_exog(i,"init-1",r,t)$geo_egs(i) = geo_cap_exog(i,r) ; - -* existing capacity equals all 2010 capacity less retirements -* here we use the max of zero or that number to avoid any errors -* with variables that are gte to zero -* also have expiration of capital if t - tfirst is greater than the maximum age -* note the first conditional limits this calculation to units that -* do NOT have their capacity binned by heat rates (this include distpv for reasons explained below) -capacity_exog(i,"init-1",r,t)${[yeart(t)-sum{tt$tfirst(tt),yeart(tt) } capacity_exog(i,v,r,t))] = - capacity_exog(i,v,r,t-1) - capacity_exog(i,v,r,t) ; - -avail_retire_exog_rsc(i,v,r,t)$[not initv(v)] = 0 ; - -m_capacity_exog(i,v,r,t)$capacity_exog(i,v,r,t) = capacity_exog(i,v,r,t) ; -m_capacity_exog_energy(i,v,r,t)$capacity_exog_energy(i,v,r,t) = capacity_exog_energy(i,v,r,t) ; -m_capacity_exog(i,"init-1",r,t)$geo(i) = geo_cap_exog(i,r) ; - -* We assign the ~1.3 GW of exising csp-ns to upv throughout the model, but then -* convert 1.3 GW of upv back to csp-ns in the output processing. -$onempty -parameter cap_cspns(r,allt) "--MW-- csp-ns capacity" -/ -$offlisting -$ondelim -$include inputs_case%ds%cap_cspns.csv -$offdelim -$onlisting -/ ; -$offempty - - -* with regional h2 demands, we assume capacity follows demand and thus load in -* national demand values, the shares of national demand by each BA -* we then convert those to MW of capacity using the conversion of tons / mw -* -parameter h2_exogenous_demand(p,allt) "--metric tons/yr-- exogenous demand for hydrogen" -/ -$offlisting -$ondelim -$include inputs_case%ds%h2_exogenous_demand.csv -$offdelim -$onlisting -/ ; -* h2_exogenous_demand.csv is in million tons so convert to tons -h2_exogenous_demand(p,t) = 1e6 * h2_exogenous_demand(p,t) ; - -scalar h2_demand_start "--year-- first year that h2 demand should be modeled" - h2_gen_firstyear "--year-- first year that h2 generation technologies are available" -; - -* Identify the first year that hydrogen generation technologies are allowed -h2_gen_firstyear = smin{i$[h2_combustion(i)$(not ban(i))], firstyear(i) } ; - -* Set h2_demand_start to the first year that there is data -* in h2_exogenous_demand -h2_demand_start = smin{t$[sum{p, h2_exogenous_demand(p,t)}], yeart(t) } ; - -* If h2_gen_firstyear is smaller than h2_demand_start, set h2_demand_start -* to be h2_gen_firstyear -h2_demand_start$[h2_gen_firstyear=yeart(tt)], prescribednonrsc(tt,pcat,r,"value") } ; - - -m_required_prescriptions(pcat,r,t)$[tmodel_new(t) - $(sum{tt$[yeart(t)>=yeart(tt)], prescribedrsc(tt,pcat,r,"value") } - or caprsc(pcat,r,"value"))] - = sum{(tt)$[(yeart(t) >= yeart(tt))], prescribedrsc(tt,pcat,r,"value") } - + caprsc(pcat,r,"value") -; - -m_required_prescriptions_energy(pcat,r,t)$tmodel_new(t) - = sum{tt$[yeart(t)>=yeart(tt)], prescribednonrsc_energy(tt,pcat,r,"value") } ; - -parameter degrade(i,t,tt) "degradation factor by i" - degrade_pcat(pcat,t,tt) "degradation factor by pcat" ; - -parameter degrade_annual(i) "annual degredation rate" -/ -$offlisting -$ondelim -$include inputs_case%ds%degradation_annual.csv -$offdelim -$onlisting -/ ; - -* Hybrid degradation is initially defined as the battery degradation for calculating the ITC for the hybrid battery (degradation_annual_default.csv). -* Here, reassign hybrid PV+Battery to have the same value as UPV. -* Currently the degradation for the battery is zero, but if becomes non-zero, then two separate degradation factors should be defined -* (e.g., degrade_pvb_p, degrade_pvb_b) to allow for degradation to be applied to both the PV and battery. -degrade_annual(i)$pvb(i) = sum{ii$[upv(ii)$rsc_agg(ii,i)], degrade_annual(ii) } ; - -degrade_annual(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), degrade_annual(ii) } ; - -degrade(i,t,tt)$[(yeart(tt)>=yeart(t))$(not ban(i))] = 1 ; -degrade(i,t,tt)$[(yeart(tt)>=yeart(t))$(not ban(i))] = (1-degrade_annual(i))**(yeart(tt)-yeart(t)) ; - -set prescription_check(i,v,r,t) "check to see if prescriptive capacity comes online in a given year" ; - -parameter noncumulative_prescriptions(pcat,r,t) "--MW-- prescribed capacity that comes online in a given year" ; -* need to fill in for unmodeled, gap years via tprev but -* tprev is not defined with tprev(t,tfirst) -noncumulative_prescriptions(pcat,r,t)$tmodel_new(t) - = sum{tt$[(yeart(tt)<=yeart(t) -* this condition populates values of tt which exist between the -* previous modeled year and the current year - $(yeart(tt)>sum{ttt$tprev(t,ttt), yeart(ttt) })) - ], - prescribednonrsc(tt,pcat,r,"value") + prescribedrsc(tt,pcat,r,"value") - } ; - -parameter noncumulative_prescriptions_energy(pcat,r,t) "--MWh-- prescribed energy capacity that comes online in a given year" ; -noncumulative_prescriptions_energy(pcat,r,t)$tmodel_new(t) - = sum{tt$[(yeart(tt)<=yeart(t) - $(yeart(tt)>sum{ttt$tprev(t,ttt), yeart(ttt) })) - ], - prescribednonrsc_energy(tt,pcat,r,"value") - } ; - -prescription_check(i,newv,r,t)$[sum{pcat$prescriptivelink(pcat,i), noncumulative_prescriptions(pcat,r,t) } - $ivt(i,newv,t)$tmodel_new(t)$(not ban(i))] = yes ; - -*Extend feasibility for prescribed rsc capacity where there is no supply curve data. -*Resource will be manualy added to supply curve in bin1 in these cases. -*Only enable for bin1 if there is no resource in any bins to keep parameter size down. -m_rscfeas(r,i,"bin1")$[sum{(pcat,t)$[sameas(pcat,i)$tmodel_new(t)], noncumulative_prescriptions(pcat,r,t) }$rsc_i(i)$(not bannew(i))$(sum{rscbin, rsc_dat(i,r,"cap",rscbin) }=0)] = yes ; - -*========================================================== -*--- Interconnection queues (Capacity deployment limit) --- -*========================================================== -alias(tg,tgg) ; - -$onempty -table cap_limit(tg,r,allt) "--MW-- capacity deployment limit by region and technology based on interconnection queues" -$offlisting -$ondelim -$include inputs_case%ds%cap_limit.csv -$offdelim -$onlisting -; -$offempty - - -parameter cap_penalty(tg) "--per MW-- cost penalty for capacity deployment above cap limit" -/ -$offlisting -$ondelim -$include inputs_case%ds%cap_penalty.csv -$offdelim -$onlisting -/ ; - -*============================================= -* -- Explicit spur-line capacity (if used) -- -*============================================= - -* Indicate which technologies have spur lines handled endogenously (none by default) -set spur_techs(i) "Generators with endogenous spur lines" ; -spur_techs(i) = no ; - -* Written by writesupplycurves.py -$onempty -set x "reV resource sites" -/ -$offlisting -$include inputs_case%ds%x.csv -$onlisting -/ ; - -* Written by writesupplycurves.py -parameter spurline_cost(x) "--$/MW-- Spur-line cost for each reV site" -/ -$offlisting -$ondelim -$include inputs_case%ds%spurline_cost.csv -$offdelim -$onlisting -/ ; - -* Written by writesupplycurves.py -set spurline_sitemap(i,r,rscbin,x) "Mapping set from generators to reV sites" -/ -$offlisting -$ondelim -$include inputs_case%ds%spurline_sitemap.csv -$offdelim -$onlisting -/ ; - -* Written by writesupplycurves.py -set x_r(x,r) "Mapping set from reV sites to model regions" -/ -$offlisting -$ondelim -$include inputs_case%ds%x_r.csv -$offdelim -$onlisting -/ ; -$offempty - -* Include techs in spurline_sitemap in spur_techs (currently only wind-ons and upv) -$ifthene.spursites %GSw_SpurScen% == 1 -spur_techs(i)$(onswind(i) or upv(i)) = yes ; - -$ifthen.geohydrorev %geohydrosupplycurve% == 'reV' -spur_techs(i)$(geo_hydro(i)) = yes ; -$endif.geohydrorev - -$ifthen.egsrev %egssupplycurve% == 'reV' -spur_techs(i)$(geo_egs_allkm(i)) = yes ; -$endif.egsrev - -$endif.spursites - -* Indicate which reV sites are included in the model -set xfeas(x) "Sites to include in the model" ; -xfeas(x)$sum{r$x_r(x,r), 1} = yes ; - - -*========================================== -* -- Initialize tc_phaseout_mult -- -*========================================== -*initialize tc_phaseout_mult with full value -tc_phaseout_mult(i,v,t)$tmodel_new(t) = 1 ; -tc_phaseout_mult_t(i,t)$tmodel_new(t) = 1 ; - -*========================================== -* -- Valid Capacity and Generation Sets -- -*========================================== - -* -- valcap specification -- -* first all available techs are included -* then we remove those as specified - -* start with a blank slate -valcap(i,v,r,t) = no ; - -*existing plants are enabled if not in ban(i) -valcap(i,v,r,t)$[m_capacity_exog(i,v,r,t)$(not ban(i))$tmodel_new(t)] = yes ; - -* if a plant is still available by upgrade year -* and it is able to be upgraded - keep that plant in the valcap set -valcap(i,v,r,t)$[sum{tt$[tt.val = Sw_UpgradeYear], m_capacity_exog(i,v,r,tt) } - $(Sw_Upgrades = 1)$(t.val >= Sw_UpgradeYear) - $(not ban(i)) - $sum{ii, upgrade_from(ii,i) }$tmodel_new(t)] = yes ; - -*enable all new classes for balancing regions -*if available (via ivt) and if not an rsc tech -*and if it is not in ban or bannew -*the year also needs to be greater than the first year indicated -*for that specific class (this is the summing over tt portion) -*or it needs to be specified in prescriptivelink -valcap(i,newv,r,t)$[(not rsc_i(i))$tmodel_new(t)$(not ban(i))$(not bannew(i)) - $(sum{tt$(yeart(tt)<=yeart(t)), ivt(i,newv,tt) })$(not upgrade(i)) - ] = yes ; - -*for rsc technologies, enabled if m_rscfeas is populated -*similarly to non-rsc technologies and there is the additional -*condition that m_rscfeas must contain values in at least one rscbin -valcap(i,newv,r,t)$[rsc_i(i)$tmodel_new(t)$(not ban(i))$(not bannew(i)) - $sum{rscbin, m_rscfeas(r,i,rscbin) }$(not upgrade(i)) - $sum{tt$(yeart(tt)<=yeart(t)), ivt(i,newv,tt) } - ] = yes ; - - -*enable capacity if there is a required prescription in that region -*first for non-rsc techs -valcap(i,newv,r,t)$[(not rsc_i(i)) - $(sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,t) }) - $sum{tt$[sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,tt) } - $(yeart(tt)<=yeart(t))], ivt(i,newv,tt) } - $(not ban(i))] = yes ; - -*then for rsc techs -valcap(i,newv,r,t)$[rsc_i(i) - $(sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,t) }) - $sum{tt$[sum{pcat$prescriptivelink(pcat,i), m_required_prescriptions(pcat,r,tt) } - $(yeart(tt)<=yeart(t))], ivt(i,newv,tt) } - $(not ban(i)) - $sum{rscbin, m_rscfeas(r,i,rscbin) }] = yes ; - -* Techs where new investment are banned: Start by removing from valcap -valcap(i,newv,r,t)$bannew(i) = no ; -* Then add back only if they have prescribed capacity in years with the appropriate i/v/t combination -valcap(i,newv,r,t) - $[bannew(i) - $(not ban(i)) - $sum{(tt,pcat)$[ivt(i,newv,tt)$prescriptivelink(pcat,i)], - noncumulative_prescriptions(pcat,r,tt) }] - = yes ; - -*NEW capacity only valid in historical years if and only if it has required prescriptions -*logic here is that we don't want to populate the constraint with CAP <= 0 and instead -*want to simply remove the consideration for CAP altogether and make the constraint unnecessary -*note that the constraint itself is also conditioned on valcap - -*therefore remove the consideration of valcap if... -valcap(i,newv,r,t)$[ -*if there are no required prescriptions - (not sum{pcat$prescriptivelink(pcat,i), - m_required_prescriptions(pcat,r,t) } ) -*if the year is before the first year the technology is allowed - $(yeart(t)=Sw_UpgradeYear) - $(yeart(t)>=firstyear(i)) - $sum{ii$upgrade_from(i,ii), valcap(ii,initv,r,t) } - $(not ban(i)) - $(not sum{ii$upgrade_to(i,ii), ban(ii) }) - ] = yes ; - -*upgrades from new techs are included in valcap if... -* it is an upgrade tech, the switch is enabled, and past the beginning upgrade year -valcap(i,newv,r,t)$[upgrade(i)$Sw_Upgrades$(yeart(t)>=Sw_UpgradeYear) -*if the capacity that it is upgraded to is available - $sum{ii$upgrade_to(i,ii), valcap(ii,newv,r,t) } -*if the technology is not banned - $(not ban(i)) -*if the technology you upgrade to is not banned - $(not sum{ii$upgrade_to(i,ii), ban(ii) }) -*if it is past the first year that technology is available - $(yeart(t)>=firstyear(i)) -*if it is a valid ivt combination which is duplicated from upgrade_to - $sum{tt$(yeart(tt)<=yeart(t)), ivt(i,newv,tt) } - $(yeart(t)>=Sw_UpgradeYear) - ] = yes ; - -*remove any upgrade considerations if before the upgrade year -valcap(i,v,r,t)$[upgrade(i)$(yeart(t) caa_coal_retire_year) - $(sum{ii$(not forced_retire(ii,r,t)), upgrade_from(ii,i) }) ] = 0 ; - -* remove upgrade technologies that are explicitly banned -valcap(i,v,r,t)$[upgrade(i)$ban(i)] = no ; - -*Restrict valcap for nuclear in BAs that are impacted By state nuclear bans -if(Sw_NukeStateBan = 1, - valcap(i,v,r,t)$[nuclear(i)$newv(v)$nuclear_ba_ban(r)] = no ; -) ; - -$ifthene.hydEDban %GSw_hydED% == 0 -* Only leave hydED, turn off remaining hydro technologies -valcap(i,v,r,t)$[hydro(i)$(not sameas(i,"hydED"))] = no ; -$endif.hydEDban - -* Drop vintages in non-modeled future years -valcap(i,v,r,t)$[(not sum{tt$[tmodel_new(tt)], ivt(i,v,tt) })$newv(v)] = no ; - -* Remove non-offshore resources from offshore zones -valcap(i,v,r,t)$[offshore(r)$(not ofswind(i))] = no ; - -* Add aggregations of valcap -valcap_irt(i,r,t) = sum{v, valcap(i,v,r,t) } ; -valcap_iv(i,v)$sum{(r,t)$tmodel_new(t), valcap(i,v,r,t) } = yes ; -valcap_ir(i,r)$sum{(v,t)$tmodel_new(t), valcap(i,v,r,t) } = yes ; -valcap_i(i)$sum{v, valcap_iv(i,v) } = yes ; -valcap_ivr(i,v,r)$sum{t, valcap(i,v,r,t) } = yes ; - -* -- valinv specification -- -valinv(i,v,r,t) = no ; -valinv(i,v,r,t)$[valcap(i,v,r,t)$ivt(i,v,t)] = yes ; - -* Do not allow investments in regions where that technology is banned, expect for prescribed builds -valinv(i,v,r,t)$[tech_banned(i,r)$(not sum{pcat$prescriptivelink(pcat,i), noncumulative_prescriptions(pcat,r,t) })] = no ; - -*remove non-prescribed numeraire technologies that remain in valcap -valinv(i,newv,r,t)$[i_numeraire(i)$Sw_WaterMain$(not sum{pcat$prescriptivelink(pcat,i), noncumulative_prescriptions(pcat,r,t) })] = no ; - -*upgrades are not allowed for the INV variable as they are the sum of UPGRADES -valinv(i,v,r,t)$upgrade(i) = no ; - -valinv(i,v,r,t)$[(yeart(t)=h2_ptc_firstyear) -* if the generating tech itself is available - $valcap(i,v,r,t) -* if the technology has low enough of emissions to comply with the policy - $i_h2_ptc_gen(i) - ] = yes ; - -* -- valgen_h2ptc specification -- -* generators that can receive the hydrogen production tax credit are available based on valcap_h2ptc -valgen_h2ptc(i,v,r,t)$valcap_h2ptc(i,v,r,t) = yes ; - - -* -- m_refurb_cond specification -- - -* technologies can be refurbished if... -* they are part of refurbtech -* the number of years from tt to t are beyond the expiration of the tech (via maxage) -* it is valid capacity in t, the current solve year. -* it was a valid investment in year tt, the initial investment year. -m_refurb_cond(i,newv,r,t,tt)$[refurbtech(i) - $(yeart(tt) maxage(i)) - $valcap(i,newv,r,t)$valinv(i,newv,r,tt) - ] = yes ; - - -* -- inv_cond specification -- - -*if there is a link between the bintage and the year -*all previous years -*if the unit we invested in is not retired... -inv_cond(i,newv,r,t,tt)$[(not ban(i)) - $tmodel_new(t)$tmodel_new(tt) - $(yeart(tt) <= yeart(t)) - $valinv(i,newv,r,tt) - $(ord(t)-ord(tt) < maxage(i)) - ] = yes ; - -inv_cond(i,newv,r,t,tt)$[Sw_WaterMain$sum{ctt$bannew_ctt(ctt),i_ctt(i,ctt) }$tmodel_new(t)$tmodel_new(tt) - $sum{(pcat)$[sameas(pcat,i)], noncumulative_prescriptions(pcat,r,tt) } - $(yeart(tt) <= yeart(t)) - $valinv(i,newv,r,tt) - $(ord(t)-ord(tt) < maxage(i)) - ] = yes ; - - - -* cannot restrict by valcap here to maintain compatibility with water techs -co2_captured_incentive(i,v,r,t) = co2_captured_incentive_in(i,v,t) ; - -* expand to water techs -co2_captured_incentive(i,v,r,t)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), co2_captured_incentive(ii,v,r,t) } ; - -* expand to upgrade techs -co2_captured_incentive(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)] = - sum{ii$upgrade_to(i,ii),co2_captured_incentive(ii,v,r,t) } ; - -* incentive for captured co2 for initial plants set to the amount available -* as of upgradeyear, similar to other performance and cost characteristics -co2_captured_incentive(i,v,r,t)$[initv(v)$upgrade(i) - $valcap(i,v,r,t)$(Sw_Upgrades = 1) - $(yeart(t)>=Sw_UpgradeYear) - $(yeart(t) <= co2_capture_incentive_last_year_)] = -* note we populate the incentive for all years and then trim the incentive for later years -* when the upgrade occurs - this is after the solve statement in d_solveoneyear -* we also cast this forward based on whether or not a plant was built in that year -* but remove the last year for consideration of that below via co2_capture_incentive_last_year_ - sum{(ii,vv,tt)$[upgrade_to(i,ii)$newv(vv) - $(firstyear_v(ii,vv) = Sw_UpgradeYear) - $(yeart(tt) = Sw_UpgradeYear)], - co2_captured_incentive(ii,vv,r,tt) } ; - -* plants can only receive the CO2 capture incentive for the length of the incentive 'co2_capture_incentive_length', starting in the first year of that tech, vintage combination -co2_captured_incentive(i,newv,r,t)$[(yeart(t) > firstyear_v(i,newv) + co2_capture_incentive_length)] = 0 ; -* vintages whose first year comes after 'co2_capture_incentive_last_year_' cannot receive the CO2 capture incentive because the incentive is no longer available -co2_captured_incentive(i,newv,r,t)$[(firstyear_v(i,newv) > co2_capture_incentive_last_year_)] = 0 ; - -* remove any invalid values to shrink parameter -co2_captured_incentive(i,v,r,t)$[not valcap(i,v,r,t)] = 0 ; - -* making h2_ptc for all regions -h2_ptc(i,v,r,t)$valcap(i,v,r,t) = h2_ptc_in(i,v,t) ; - -* if Sw_H2_PTC = 1, then tech 'electrolyzer' can also receive the hydrogen PTC, as designated in h2_ptc. -* Otherwise, we assume it receives $0/kg because the cleanliness of its carbon cannot be proven -h2_ptc("electrolyzer",v,r,t)$[(not Sw_H2_PTC)] = 0; - -set h2_ptc_years(t) "years in which the hydrogen production incentive is active"; -h2_ptc_years(t) = tmodel_new(t)$[sum{(i,v,r),h2_ptc(i,v,r,t)}]; - - -*========================================== -* --- Parameters for water constraints --- -*========================================== - -set sw(wst) "surface water types where access is based on consumption not withdrawal" -/ -$offlisting -$ondelim -$include inputs_case%ds%sw.csv -$offdelim -$onlisting -/ ; - -set i_water_surf(i) "subset of technologies that uses surface water", - i_w(i,w) "linking set between technology and water use type used in constraining water availability" ; - -i_water_surf(i)$[sum{(sw,ctt,ii)$i_ii_ctt_wst(i,ii,ctt,sw), 1}] = yes ; -i_w(i,"cons")$[i_water(i)$i_water_surf(i)] = yes ; -i_w(i,"with")$[i_water(i)$(not i_water_surf(i))] = yes ; - -parameter wat_supply_init(wst,r) "-- million gallons per year -- water supply allocated to initial fleet " ; - -*WatAccessAvail - water access available (Mgal/year) -*WatAccessCost - cost of water access (2004$/Mgal) -$onempty -parameter wat_supply_new(wst,*,r) "-- million gallons per year , $ per million gallons per year -- water supply curve for post-2010 capacity with *=cap,cost" -/ -$offlisting -$ondelim -$include inputs_case%ds%wat_access_cap_cost.csv -$offdelim -$onlisting -/ ; -$offempty - -parameter m_watsc_dat(wst,*,r,t) "-- million gallons per year, $ per million gallons per year -- water supply curve data with *=cap,cost" ; - -* UnappWaterSeaDistr - seasonal distribution factors for new unappropriated water access -$onempty -table watsa_temp(wst,r,quarter) "fractional quarterly allocation of water" -$offlisting -$ondelim -$include inputs_case%ds%unapp_water_sea_distr.csv -$offdelim -$onlisting -; -$offempty - -m_watsc_dat(wst,"cost",r,t)$tmodel_new(t) = wat_supply_new(wst,"cost",r) ; - -*not allowed to invest in upgrade techs since they are a product of upgrades -inv_cond(i,v,r,t,tt)$upgrade(i) = no ; - - -*===================================== -* --- Regional Carbon Constraints --- -*===================================== - -$onempty -set RGGI_States(st) "states with RGGI regulation" -/ -$offlisting -$include inputs_case%ds%rggi_states.csv -$onlisting -/ -; -$offempty - -Set RGGI_r(r) "BAs with RGGI regulation" ; - -RGGI_r(r)$[sum{st$RGGI_States(st),r_st(r,st) }] = yes ; - -* declared over allt to allow for external data files that extend beyond end_year -parameter RGGI_cap(allt) "--metric tons-- CO2 emissions cap for RGGI states" -/ -$offlisting -$ondelim -$include inputs_case%ds%rggicon.csv -$offdelim -$onlisting -/ ; - -* These values are based on 42 MMT trajectory from section 8.1 of the CPUC "Inputs & Assumptions: -* "2019-2020 Integrated Resource Planning." This document can be found at -* ftp://ftp.cpuc.ca.gov/energy/modeling/Inputs%20%20Assumptions%202019-2020%20CPUC%20IRP%202020-02-27.pdf -$onempty -parameter state_cap(st,allt) "--metric tons-- CO2 emissions cap for state cap and trade policies" -/ -$offlisting -$ondelim -$include inputs_case%ds%state_cap.csv -$offdelim -$onlisting -/ ; -$offempty - - -*========================== -* -- Climate heuristics -- -*========================== -parameter climate_heuristics_yearfrac(allt) "--fraction-- annual scaling factor for climate heuristics" -$onempty -/ -$offlisting -$ondelim -$include inputs_case%ds%climate_heuristics_yearfrac.csv -$offdelim -$onlisting -/ ; -$offempty - -set climate_param "parameters defined in climate_heuristics_finalyear" -/ -$offlisting -$include inputs_case%ds%climate_param.csv -$onlisting -/ ; - -parameter climate_heuristics_finalyear(climate_param) "--fraction-- climate heuristic adjustment in final year" -$onempty -/ -$offlisting -$ondelim -$include inputs_case%ds%climate_heuristics_finalyear.csv -$offdelim -$onlisting -/ ; -$offempty - -* hydro_capcredit_delta applies to dispatchable hydro. -* We don't apply it through cap_hyd_szn_adj because we only want to change cap credit, not energy dispatch. -parameter hydro_capcredit_delta(i,allt) "--fraction-- fractional adjustment to dispatchable hydro capacity credit from climate heuristics" ; -hydro_capcredit_delta(i,t)$hydro_d(i) = - climate_heuristics_finalyear('hydro_capcredit_delta') * climate_heuristics_yearfrac(t) -; - -*==================================== -* --- RPS data --- -*==================================== - -set RPSCat "RPS constraint categories, including clean energy standards" -/ -$offlisting -$include inputs_case%ds%RPSCat.csv -$onlisting -/ ; - -set RPSCat_i(RPSCat,i,st) "mapping between rps category and technologies for each state", - RecMap(i,RPSCat,st,ast,t) "Mapping set for technologies to RPS categories and indicates if credits can be sent from st to ast", - RecStates(RPSCat,st,t) "states that can generate RECS for their own or other states' requirements", - RecTrade(RPSCat,st,ast,t) "mapping set between states that can trade RECs with each other (from st to ast)", - RecTech(RPSCat,i,st,t) "set to indicate which technologies and classes can contribute to a state's RPSCat", - r_st_rps(r,st) "mapping of eligible regions to each state for RPS/CES purposes" ; - -Parameter RecPerc(RPSCat,st,t) "--fraction-- fraction of total generation for each state that must be met by RECs for each category" - RPSTechMult(RPSCat,i,st) "--fraction-- fraction of generation from each technology that counts towards the requirement for each category" -; - -* Create a new r-to-state mapping set that allows voluntary purchases -r_st_rps(r,st) = r_st(r,st) ; -* All regions can create voluntary RECS -r_st_rps(r,"voluntary") = yes ; - -$onempty -table techs_banned_rps(i,st) "Techs that are banned for serving RPS in a given state" -$offlisting -$ondelim -$include inputs_case%ds%techs_banned_rps.csv -$offdelim -$onlisting -; -$offempty - -$onempty -parameter RecStyle(st,RPSCat) "--integer-- Indication for how to apply state requirement (0 = end-use sales, 1 = bus-bar sales, 2 = generation). Default is 0." -/ -$offlisting -$ondelim -$include inputs_case%ds%recstyle.csv -$offdelim -$onlisting -/ -; -$offempty - -$onempty -table techs_banned_ces(i,st) "Techs that are banned for serving CES in a given state" -$offlisting -$ondelim -$include inputs_case%ds%techs_banned_ces.csv -$offdelim -$onlisting -; -$offempty - -$onempty -* declared over allt to allow for external data files that extend beyond end_year -Table rps_fraction(allt,st,RPSCat) "--fraction-- requirement for state RPS" -$offlisting -$ondelim -$include inputs_case%ds%rps_fraction.csv -$offdelim -$onlisting -; -$offempty - -$onempty -parameter ces_fraction(allt,st) "--fraction-- requirement for clean energy standard" -/ -$offlisting -$ondelim -$include inputs_case%ds%ces_fraction.csv -$offdelim -$onlisting -/ -; -$offempty - -RecPerc(RPSCat,st,t) = sum{allt$att(allt,t), rps_fraction(allt,st,RPSCat) } ; -RecPerc(RPSCat,st,t)$[(Sw_StateRPS_Carveouts = 0)$(sameas(RPSCat, "RPS_solar") or sameas(RPSCat, "RPS_Wind"))] = 0; -RecPerc("CES",st,t) = ces_fraction(t,st) ; - -* RE generation creates both CES and RPS credits, which can cause double-counting -* if a state has an RPS but not a CES. By setting each state's CES as the maximum -* of its RPS or CES, we prevent the double-counting. -RecPerc("CES",st,t) = max(RecPerc("CES",st,t), RecPerc("RPS_all",st,t)) ; - -*Some links (value in RECtable = 2) restricted to bundled trading, while -*some (value in RECtable = 1) allowed to also trade unbundled RECs. -*Note the reversed set index order for rectable as compared to RecTrade, RecMap, and RECS. -$onempty -table rectable(st,ast) "Allowed credit trade from ast to st. [1] Unbundled allowed; [2] Only bundled allowed" -$offlisting -$ondelim -$include inputs_case%ds%rectable.csv -$offdelim -$onlisting -; -$offempty - -table acp_price(st,allt) "$/REC - alternative compliance payment price for RPS constraint" -$offlisting -$ondelim -$include inputs_case%ds%acp_prices.csv -$offdelim -$onlisting -; - -$onempty -parameter acp_disallowed(st,RPSCat) "--integer-- Indication for whether ACP purchases are disallowed (1) or allowed (0)." -/ -$offlisting -$ondelim -$include inputs_case%ds%acp_disallowed.csv -$offdelim -$onlisting -/ -; -$offempty - -RecStates(RPSCat,st,t)$[RecPerc(RPSCat,st,t) or sum{ast, rectable(ast,st) }] = yes ; - -*If both states have an RPS for the RPSCat and if they're allowed to trade, they can trade -RecTrade(RPSCat,st,ast,t)$((rectable(ast,st)=1)$RecStates(RPSCat,ast,t)) = yes ; - -*If both states have an RPS for the RPSCat and if they're allowed to trade, they can trade -RecTrade("RPS_bundled",st,ast,t)$[(rectable(ast,st)=2)$RecStates("RPS_all",ast,t)] = yes ; -RecTrade("CES_bundled",st,ast,t)$[(rectable(ast,st)=2)$RecStates("CES",ast,t)] = yes ; - -*Assign eligible techs for RPS_All -RPSCat_i("RPS_All",i,st)$[re(i)$(not techs_banned_rps(i,st))$valcap_i(i)] = yes ; - -*Assign eligible techs for RPS_Wind -RPSCat_i("RPS_Wind",i,st)$[wind(i)$(not techs_banned_rps(i,st))$valcap_i(i)] = yes ; - -*Assign eligible techs for RPS_Solar -RPSCat_i("RPS_Solar",i,st)$[(pv(i) or pvb(i))$(not techs_banned_rps(i,st))$valcap_i(i)] = yes ; - -*We allow CCS techs and upgrades to be eligible for CES policies -*CCS contribution is limited based on the amount of emissions captured later on down -RPSCat_i("CES",i,st)$[(RPSCAT_i("RPS_All",i,st) or nuclear(i) or hydro(i) or ccs(i) or canada(i)) - $(not techs_banned_ces(i,st)) - $valcap_i(i)] = yes ; - -RecTech(RPSCat,i,st,t)$(RPSCat_i(RPSCat,i,st)$RecStates(RPSCat,st,t)) = yes ; -RecTech(RPSCat,i,st,t)$(hydro(i)$RecTech(RPSCat,"hydro",st,t)$valcap_i(i)) = yes ; -RecTech("RPS_Bundled",i,st,t)$[RecTech("RPS_All",i,st,t)] = yes ; - -RecTech("CES_Bundled",i,st,t)$[RecTech("CES",i,st,t)] = yes ; - -*Voluntary market RECs can come from any RE tech -RecTech(RPSCat,i,"voluntary",t)$re(i) = yes ; - -*Remove combinations that are not allowed by valgen -RecTech(RPSCat,i,st,t)$[not sum{(v,r)$r_st_rps(r,st), valgen(i,v,r,t) }] = no ; - -*Remove CCS techs if explicitly disallowed -RecTech(RPSCat,i,st,t)$[ccs(i)$Sw_CCS_NotRecTech] = no ; - -$onempty -table hydrofrac_policy(st,RPSCat) "fraction of hydro RPS or CES credits that can count towards policy targets" -$offlisting -$ondelim -$include inputs_case%ds%hydrofrac_policy.csv -$offdelim -$onlisting -; -$offempty - -*initialize values to 1 -RPSTechMult(RPSCat,i,st)$[sum{t, RecTech(RPSCat,i,st,t) }] = 1 ; -*reduce multipliers for hydro technologies based on eligibility fractions -RPSTechMult(RPSCat,i,st)$[hydro(i)$valcap_i(i)] = hydrofrac_policy(st,RPSCat) ; -RPSTechMult("RPS_bundled",i,st)$[hydro(i)$valcap_i(i)] = RPSTechMult("RPS_All",i,st) ; -RPSTechMult("CES_bundled",i,st)$[hydro(i)$valcap_i(i)] = RPSTechMult("CES",i,st) ; - -*Reduce RPS/CES values for distributed PV based on distloss because we increase their generation to the busbar level -RPSTechMult(RPSCat,i,st)$[(distpv(i))$RPSTechMult(RPSCat,i,st)] = 1 - distloss ; - -$onempty -table techs_banned_imports_rps(i,st) "Techs that are not allowed to be imported into a state to meet the RPS" -$offlisting -$ondelim -$include inputs_case%ds%techs_banned_imports_rps.csv -$offdelim -$onlisting -; -$offempty - -*CCS technologies have a variety of capture rates, so we assign them below after reading in capture rates - -RecMap(i,RPSCat,st,ast,t)$[ -*if the receiving state has a requirement for RPSCat - RecPerc(RPSCat,ast,t) -*if both states can use that technology - $RecTech(RPSCat,i,st,t) - $RecTech(RPSCat,i,ast,t) -*if the state can trade - $RecTrade(RPSCat,st,ast,t) - ] = yes ; - -RecMap(i,"RPS_bundled",st,ast,t)$( -*if the receiving state has a requirement for RPSCat - RecPerc("RPS_all",ast,t) -*if both states can use that technology - $RecTech("RPS_bundled",i,st,t) - $RecTech("RPS_bundled",i,ast,t) -*if the state can trade - $RecTrade("RPS_bundled",st,ast,t) - ) = yes ; - - -RecMap(i,"CES_bundled",st,ast,t)$( -*if the receiving state has a requirement for RPSCat - RecPerc("CES",ast,t) -*if both states can use that technology - $RecTech("CES_bundled",i,st,t) - $RecTech("CES_bundled",i,ast,t) -*if the state can trade - $RecTrade("CES_bundled",st,ast,t) - ) = yes ; - -*states can "import" their own RECs (except for "voluntary") -RecMap(i,RPSCat,st,ast,t)$[ - sameas(st,ast) - $RecTech(RPSCat,i,st,t) - $RecPerc(RPSCat,st,t) - $(not sameas(st,"voluntary")) - ] = yes ; - -*states that allow hydro to fulfill their RPS requirements can trade hydro recs -RecMap(i,RPSCat,st,ast,t)$[ - hydro(i) - $RPSTechMult(RPSCat,i,st) - $RPSTechMult(RPSCat,i,ast) - $RecMap("hydro",RPSCat,st,ast,t) - $valcap_i(i) - ] = yes ; - -*Do not allow banned imports -RecMap(i,RPSCat,st,ast,t)$[ - (sameas(RPSCat,"RPS_All") or sameas(RPSCat,"RPS_bundled")) - $(not sameas(st,ast)) - $techs_banned_imports_rps(i,ast) - ] = no ; - -*Only allow voluntary market to use renewable energy when consuming CES credits -RecMap(i,RPSCat,st,"voluntary",t)$[ - (not re(i)) - $(sameas(RPSCat,"CES") or sameas(RPSCat,"CES_bundled")) - ] = no ; - -*Do not allow voluntary market to use canadian imports -RecMap(i,RPSCat,st,"voluntary",t)$[ - (canada(i)) - ] = no ; - -if(Sw_WaterMain=1, - RecMap(i,RPSCat,st,ast,t)$[i_water_cooling(i)$(not RecMap(i,RPSCat,st,ast,t))] - = sum{ii$ctt_i_ii(i,ii), RecMap(ii,RPSCat,st,ast,t) } ; -) ; - -$onempty -parameter RPS_oosfrac(st) "fraction of RECs from out of state that can meet the RPS" -/ -$offlisting -$ondelim -$include inputs_case%ds%oosfrac.csv -$offdelim -$onlisting -/ ; -$offempty - -$onempty -table RPS_unbundled_limit_in(st,allt) "--fraction-- upper bound of state RPS that can be met with unbundled RECS" -$offlisting -$ondelim -$include inputs_case%ds%unbundled_limit_rps.csv -$offdelim -$onlisting -; -$offempty - -$onempty -table CES_unbundled_limit_in(st,allt) "--fraction-- upper bound of state CES that can be met with unbundled RECS" -$offlisting -$ondelim -$include inputs_case%ds%unbundled_limit_ces.csv -$offdelim -$onlisting -; -$offempty - -parameter REC_unbundled_limit(RPScat,st,allt) '--fraction-- portion for RPS/CES constraint that can be met with unbundled RECS' ; -set st_unbundled_limit(RPScat,st) "states that have a unbundled limit on RECs" ; - -REC_unbundled_limit("RPS_All",st,t) = RPS_unbundled_limit_in(st,t) ; -REC_unbundled_limit("CES",st,t) = CES_unbundled_limit_in(st,t) ; - -st_unbundled_limit(RPSCat,st)$sum{t, REC_unbundled_limit(RPSCat,st,t) } = yes ; - -parameter national_gen_frac(allt) "--%-- national fraction of load + losses that must be met by RE" -/ -$offlisting -$ondelim -$include inputs_case%ds%gen_mandate_trajectory.csv -$offdelim -$onlisting -/ ; - -parameter nat_gen_tech_frac(i) "--fraction-- fraction of each tech generation that may be counted toward eq_national_gen" -/ -$offlisting -$ondelim -$include inputs_case%ds%gen_mandate_tech_list.csv -$offdelim -$onlisting -/ ; -nat_gen_tech_frac(i)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), nat_gen_tech_frac(ii) } ; - -*==================== -* --- CSAPR Data --- -*==================== - -* a CSAPR budget indicates the cap for trading whereas -* assurance indicates the maximum amount a state can emit regardless of trading -set csapr_cat "CSAPR regulation categories" -/ -$offlisting -$include inputs_case%ds%csapr_cat.csv -$onlisting -/ ; - -*trading rules dictate there are two groups of states that can trade with each other -set csapr_group "CSAPR trading group" -/ -$offlisting -$include inputs_case%ds%csapr_group.csv -$onlisting -/ ; - -$onempty -table csapr_cap(st,csapr_cat,allt) "--metric tons-- maximum amount of NOX emissions during the ozone season (May-September)" -$offlisting -$ondelim -$include inputs_case%ds%csapr_ozone_season.csv -$offdelim -$onlisting -; -$offempty - -$onempty -set csapr_group1_ex(st) "CSAPR states that cannot trade with those in group 2" -/ -$offlisting -$include inputs_case%ds%csapr_group1_ex.csv -$onlisting -/ -; -$offempty - -$onempty -set csapr_group2_ex(st) "CSAPR states that cannot trade with those in group 1" -/ -$offlisting -$include inputs_case%ds%csapr_group2_ex.csv -$onlisting -/ -; -$offempty - -set csapr_group_st(csapr_group,st) "final crosswalk set for use in modeling CSAPR trade relationships" ; - -csapr_group_st("cg1",st)$[sum{t,csapr_cap(st,"budget",t) }$(not csapr_group1_ex(st))$stfeas(st)] = yes ; -csapr_group_st("cg2",st)$[sum{t,csapr_cap(st,"budget",t) }$(not csapr_group2_ex(st))$stfeas(st)] = yes ; - -*assumption here is that the ozone season covers only 1/3 of the months -*in the spring and fall but the entire season in summer, -*therefore weighting each seasons emissions accordingly -parameter quarter_weight_csapr(quarter) "quarter weights for CSAPR ozone season constraints" - / spri 0.333, summ 1, fall 0.333 /; - - - -*============================== -* --- Transmission Inputs --- -*============================== - -* --- transmission sets --- -set trtype "transmission capacity type" -/ -$offlisting -$include inputs_case%ds%trtype.csv -$onlisting -/ ; - -set aclike(trtype) "AC transmission capacity types" -/ -$offlisting -$ondelim -$include inputs_case%ds%aclike.csv -$offdelim -$onlisting -/ ; - -set notvsc(trtype) "transmission capacity types that are not VSC" -/ -$offlisting -$ondelim -$include inputs_case%ds%notvsc.csv -$offdelim -$onlisting -/ ; - -set lcclike(trtype) "transmission capacity types where lines are bundled with AC/DC converters" -/ -$offlisting -$ondelim -$include inputs_case%ds%lcclike.csv -$offdelim -$onlisting -/ ; - -set trancap_fut_cat "categories of near-term transmission projects that describe the likelihood of being completed" -/ -$offlisting -$include inputs_case%ds%trancap_fut_cat.csv -$onlisting -/ ; - -set routes(r,rr,trtype,t) "final conditional on transmission feasibility" - routes_inv(r,rr,trtype,t) "routes where new transmission investment is allowed" - routes_prm(r,rr) "routes where PRM trading is allowed" - opres_routes(r,rr,t) "final conditional on operating reserve flow feasibility" -; - -alias(trtype,intype,outtype) ; - -* Specify the transmission types that are limited by Sw_TransCapMax and Sw_TransCapMaxTotal -set trtypemax(trtype) "trtypes to limit" ; -trtypemax(trtype)$[(Sw_TransCapMaxTypes=0)] = no ; -trtypemax(trtype)$[(Sw_TransCapMaxTypes=1)] = yes ; -trtypemax(trtype)$[(Sw_TransCapMaxTypes=2)$sameas(trtype,'VSC')] = yes ; -trtypemax(trtype)$[(Sw_TransCapMaxTypes=3)$(not sameas(trtype,'AC'))] = yes ; - -* --- initial transmission capacity --- -* transmission capacity input data are defined in both directions for each region-to-region pair -* Written by transmission.py -$onempty -parameter trancap_init_energy(r,rr,trtype) "--MW-- initial transmission capacity for energy trading" -/ -$offlisting -$ondelim -$include inputs_case%ds%trancap_init_energy.csv -$offdelim -$onlisting -/ ; - -parameter trancap_init_prm(r,rr,trtype) "--MW-- initial transmission capacity for capacity (PRM) trading" -/ -$offlisting -$ondelim -$include inputs_case%ds%trancap_init_prm.csv -$offdelim -$onlisting -/ ; -$offempty - -* --- future transmission capacity --- -* Transmission additions are defined in one direction for each region-to-region pair with the lowest region number listed first -* Written by transmission.py -$onempty -parameter trancap_fut(r,rr,trancap_fut_cat,trtype,allt) "--MW-- potential future transmission capacity by type (one direction)" -/ -$offlisting -$ondelim -$include inputs_case%ds%trancap_fut.csv -$offdelim -$onlisting -/ ; -$offempty - -* --- exogenously specified transmission capacity --- -* Transmission additions are defined in one direction for each region-to-region pair with the lowest region number listed first -parameter invtran_exog(r,rr,trtype,t) "--MW-- exogenous transmission capacity investment (one direction)" ; -* "certain" future transmission project capacity in the current year t -invtran_exog(r,rr,trtype,t)$trancap_fut(r,rr,"certain",trtype,t) = trancap_fut(r,rr,"certain",trtype,t) ; - -* --- valid transmission routes --- - -*transmission routes are enabled if: -* (1) there is transmission capacity between the two regions -routes(r,rr,trtype,t)$[ - trancap_init_energy(r,rr,trtype) or trancap_init_energy(rr,r,trtype) - or trancap_init_prm(r,rr,trtype) or trancap_init_prm(rr,r,trtype) - or invtran_exog(r,rr,trtype,t) or invtran_exog(rr,r,trtype,t) -] = yes ; -* (2) there is future capacity available between the two regions -routes(r,rr,trtype,t)$[sum{(tt,trancap_fut_cat)$(yeart(tt)<=yeart(t)), - trancap_fut(r,rr,trancap_fut_cat,trtype,tt) }] = yes ; -* (3) there exists a route (r,rr) that is in the opposite direction as (rr,r) -routes(rr,r,trtype,t)$(routes(r,rr,trtype,t)) = yes ; -* (4) the year is modeled -routes(r,rr,trtype,t)$(not tmodel_new(t)) = no ; - -* disable AC routes that cross interconnect boundaries (only happens if aggregating regions across interconnects) -routes(r,rr,trtype,t) - $[routes(r,rr,trtype,t) - $aclike(trtype) - $[(not sum{interconnect$[r_interconnect(r,interconnect)$r_interconnect(rr,interconnect)], 1 })] - ] = no ; - -* Disable links between offshore zones if specified -routes(r,rr,trtype,t)$[(not Sw_OffshoreBackbone)$offshore(r)$offshore(rr)] = no ; - -* If any routes use VSC, activate the VSC constraints and variables -scalar Sw_VSC "Activate VSC constraints and variables" ; -Sw_VSC = sum{routes(r,rr,trtype,t)$sameas(trtype,'VSC'), 1} ; - -* initialize all investment routes to no -routes_inv(r,rr,trtype,t) = no ; -* allow new investment along existing routes -routes_inv(r,rr,trtype,t)$[notvsc(trtype)$routes(r,rr,trtype,t)] = yes ; -* Do not allow transmission expansion on most interfaces until firstyear_trans_nearterm -routes_inv(r,rr,trtype,t)$[yeart(t) sum{country$r_country(rr,country),ord(country) }] -* then add the cost_hurdle by country (not defined for USA) -* for both the r and rr regions - = sum{country$r_country(r,country),cost_hurdle_country(country) } + - sum{country$r_country(rr,country),cost_hurdle_country(country) } ; - -cost_hurdle_regiongrp2(r,rr,t)$[sum{country$r_country(r,country),ord(country) } - <> sum{country$r_country(rr,country),ord(country) }] - = sum{country$r_country(r,country),cost_hurdle_country(country) } + - sum{country$r_country(rr,country),cost_hurdle_country(country) } ; - - -* define hurdle rates for intra-country lines -cost_hurdle_regiongrp1(r,rr,t) - $[sum{country$[r_country(r,country)$r_country(rr,country)], 1 } - $sum{trtype, routes(r,rr,trtype,t) }] = cost_hurdle_rate1(t) ; - -cost_hurdle_regiongrp2(r,rr,t) - $[sum{country$[r_country(r,country)$r_country(rr,country)], 1 } - $sum{trtype, routes(r,rr,trtype,t) }] = cost_hurdle_rate2(t) ; - -* set hurdle rates for regions within the same GSw_TransHurdleLevel region to zero -$ifthen.hurdlelevel_regiongrp1 %GSw_TransHurdleLevel1% == 'r' - ; -$else.hurdlelevel_regiongrp1 - cost_hurdle_regiongrp1(r,rr,t) - $[sum{%GSw_TransHurdleLevel1% - $[r_%GSw_TransHurdleLevel1%(r,%GSw_TransHurdleLevel1%) - $r_%GSw_TransHurdleLevel1%(rr,%GSw_TransHurdleLevel1%)], 1 } - $sum{trtype, routes(r,rr,trtype,t) }] - = 0 ; -$endif.hurdlelevel_regiongrp1 - -$ifthen.hurdlelevel_regiongrp2 %GSw_TransHurdleLevel2% == 'r' - ; -$else.hurdlelevel_regiongrp2 - cost_hurdle_regiongrp2(r,rr,t) - $[sum{%GSw_TransHurdleLevel2% - $[r_%GSw_TransHurdleLevel2%(r,%GSw_TransHurdleLevel2%) - $r_%GSw_TransHurdleLevel2%(rr,%GSw_TransHurdleLevel2%)], 1 } - $sum{trtype, routes(r,rr,trtype,t) }] - = 0 ; -$endif.hurdlelevel_regiongrp2 - -* The final hurdle cost is the higher cost among regiongrp1 and regiongrp2, and hurdle_rate_floor -cost_hurdle(r,rr,t)$[sum{trtype, routes(r,rr,trtype,t) }] = max{cost_hurdle_regiongrp1(r,rr,t),cost_hurdle_regiongrp2(r,rr,t), hurdle_rate_floor} ; - -* --- transmission distance --- - -* The distance for a transmission interface is calculated in reV using the same "least-cost-path" -* algorithm and cost tables as for wind and solar spur lines. -* Distances are more representative of new greenfield lines than existing lines. -* Written by transmission.py -$onempty -parameter distance(r,rr,trtype) "--miles-- distance between BAs by line type" -/ -$offlisting -$ondelim -$include inputs_case%ds%transmission_miles.csv -$offdelim -$onlisting -/ ; - - -* --- transmission losses --- -* Written by transmission.py -parameter tranloss(r,rr,trtype) "--fraction-- transmission loss between r and rr" -/ -$offlisting -$ondelim -$include inputs_case%ds%tranloss.csv -$offdelim -$onlisting -/ ; -$offempty - - -* --- VSC HVDC macrogrid --- -set val_converter(r,t) "BAs where VSC converter investment is allowed" ; -val_converter(r,t) = no ; - -* VSC converters are allowed in BAs on either side of a valid VSC interface -val_converter(r,t)$[sum{rr, routes_inv(r,rr,"VSC",t) }] = yes ; -val_converter(r,t)$[sum{rr, routes_inv(rr,r,"VSC",t) }] = yes ; - -* Use LCC DC per-MW costs for VSC (converters are handled separately) -transmission_line_fom(r,rr,"VSC")$sum{t, routes(r,rr,"VSC",t) } = transmission_line_fom(r,rr,"LCC") ; - - -* --- Transmission switches --- -* Sw_TransInvMax: According to -* https://www.energy.gov/eere/wind/articles/land-based-wind-market-report-2021-edition-released -* the maximum annual growth rate since 2009 was in 2013, with 543 miles of ≤230 kV, -* 3632 miles of 345 kV, and 466 miles of 500 kV. Using the WECC/TEPPC assumption of -* 1500 MW for 500 kV, 750 MW for 345 kV, and 400 MW for 230 kV (all single-circuit) -* [https://www.wecc.org/Administrative/TEPPC_TransCapCostCalculator_E3_2019_Update.xlsx] -* gives a maximum of 3.64 TWmile/year and an average of 1.36 TWmile/year. - -parameter trans_inv_max(allt) "--TWmile/year-- annual limit on transmission investments" ; -trans_inv_max(t)$[ - tmodel_new(t) - $(yeart(t) >= firstyear_trans_nearterm) - $(yeart(t) < firstyear_trans_longterm) -] = Sw_TransInvMaxNearterm ; - -trans_inv_max(t)$[ - tmodel_new(t) - $(yeart(t) >= firstyear_trans_longterm) -] = Sw_TransInvMaxLongterm ; - -*============================ -* --- Fuel Prices --- -*============================ -*Note - NG supply curve has its own section - -set f "fuel types" -/ -$offlisting -$include inputs_case%ds%f.csv -$onlisting -/ ; - -set fuel2tech(f,i) "mapping between fuel types and generations" -/ -$offlisting -$ondelim -$include inputs_case%ds%fuel2tech.csv -$offdelim -$onlisting -/ ; - -*double check in case any sets have been changed. -fuel2tech("coal",i)$coal(i) = yes ; -fuel2tech("naturalgas",i)$gas(i) = yes ; -fuel2tech("uranium",i)$nuclear(i) = yes ; -fuel2tech(f,i)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), fuel2tech(f,ii) } ; -fuel2tech(f,i)$upgrade(i) = sum{ii$upgrade_to(i,ii), fuel2tech(f,ii) } ; - -*=============================== -* Generator Characteristics -*=============================== - -set plantcat "categories for plant characteristics" -/ -$offlisting -$include inputs_case%ds%plantcat.csv -$onlisting -/ ; - -* declared over allt to allow for external data files that extend beyond end_year -parameter plant_char0(i,allt,plantcat) "--units vary-- input plant characteristics" -/ -$offlisting -$ondelim -$include inputs_case%ds%plantcharout.csv -$offdelim -$onlisting -/ ; - -parameter - winter_cap_ratio(i,v,r) "--scalar-- ratio of winter capacity to summer capacity" - winter_cap_frac_delta(i,v,r) "--scalar-- fractional change in winter compared to summer capacity" - quarter_cap_frac_delta(i,v,r,quarter,allt) "--scalar-- fractional change in quarterly capacity compared to summer" - ccseason_cap_frac_delta(i,v,r,ccseason,allt) "--scalar-- fractional change in ccseason capacity compared to summer" -; - -parameter derate_geo_vintage(i,v) "--fraction-- fraction of capacity available for gen; only geo is <1" ; -derate_geo_vintage(i,v)$valcap_iv(i,v) = 1 ; - -* Initialize values to one so that we do not accidentally zero out capacity -winter_cap_ratio(i,v,r)$valcap_ivr(i,v,r) = 1 ; -* Existing capacity is assigned the winter capacity ratio based on the value from the existing unit database -* Only hintage_data techs (which have a heat rate) are used here. Hydro, CSP, and other techs that might have -* different winter capacities are not treated here. -* Don't filter by valcap because you need the full dataset for calculating new vintages -winter_cap_ratio(i,initv,r)$hintage_data(i,initv,r,'%startyear%','cap') - = hintage_data(i,initv,r,'%startyear%','wintercap') / hintage_data(i,initv,r,'%startyear%','cap') ; -* New capacity is given the capacity-weighted average value from existing units -winter_cap_ratio(i,newv,r)$[valcap_ivr(i,newv,r) - $sum{(initv,rr), hintage_data(i,initv,rr,'%startyear%','wintercap') }] - = sum{(initv,rr), winter_cap_ratio(i,initv,rr) * hintage_data(i,initv,rr,'%startyear%','wintercap') } - / sum{(initv,rr), hintage_data(i,initv,rr,'%startyear%','wintercap') } ; - -* Assign H2-CT and H2-CC techs to have the same winter_cap_ratio as their corresponding gas techs -winter_cap_ratio(i,newv,r)$h2_ct(i) = winter_cap_ratio('gas-ct',newv,r) ; -winter_cap_ratio(i,newv,r)$h2_cc(i) = winter_cap_ratio('gas-cc',newv,r) ; - -* Assign additional nuclear techs to have the same winter_cap_ratio as 'nuclear' -winter_cap_ratio(i,newv,r)$nuclear(i) = winter_cap_ratio('nuclear',newv,r) ; - -* Upgraded plant have the same winter_cap_ratio as what they are upgraded from -winter_cap_ratio(i,newv,r)$upgrade(i) = sum{ii$upgrade_from(i,ii), winter_cap_ratio(ii,newv,r) } ; - -* Remove entries where valcap is false -winter_cap_ratio(i,v,r)$[not valcap_ivr(i,v,r)] = 0 ; - -* Calculate fractional change in winter capacity relative to summer capacity to avoid -* having lots of 1 values -winter_cap_frac_delta(i,v,r)$winter_cap_ratio(i,v,r) = round((winter_cap_ratio(i,v,r) - 1), 3) ; -* Seasonal capacity fraction delta is zero except in winter -quarter_cap_frac_delta(i,v,r,quarter,t)$[winter_cap_frac_delta(i,v,r)$sameas(quarter,'wint')] = winter_cap_frac_delta(i,v,r) ; -ccseason_cap_frac_delta(i,v,r,ccseason,t)$[winter_cap_frac_delta(i,v,r)$sameas(ccseason,'cold')] = winter_cap_frac_delta(i,v,r) ; - -* Apply thermal_summer_cap_delta through seas_cap_frac_delta -quarter_cap_frac_delta(i,v,r,quarter,t)$[conv(i)$sameas(quarter,'summ')] = - climate_heuristics_finalyear('thermal_summer_cap_delta') * climate_heuristics_yearfrac(t) -; -ccseason_cap_frac_delta(i,v,r,ccseason,t)$[conv(i)$sameas(ccseason,'hot')] = - climate_heuristics_finalyear('thermal_summer_cap_delta') * climate_heuristics_yearfrac(t) -; - - - -*============================================ -* -- Consume technologies specification -- -*============================================ - -$onempty -set routes_adjacent(r,rr) "all pairs of adjacent land-based BAs" -/ -$offlisting -$ondelim -$include inputs_case%ds%routes_adjacent.csv -$offdelim -$onlisting -/ ; -$offempty -* Remove offshore zones -routes_adjacent(r,rr)$(offshore(r) or offshore(rr)) = no ; - -set h2_routes(r,rr) "set of feasible pipeline corridors for hydrogen" - h2_routes_inv(r,rr) "set of feasible investment pipeline corridors for hydrogen" -; -* First allow H2 pipelines between any two adjacent zones -h2_routes(r,rr)$[routes_adjacent(r,rr)$Sw_H2_Transport] = yes ; -* Restrict pipelines to the level indicated by GSw_H2_TransportLevel -$ifthen.h2transportlevel %GSw_H2_TransportLevel% == 'r' - h2_routes(r,rr) = no ; -$else.h2transportlevel - h2_routes(r,rr) - $[(not sum{%GSw_H2_TransportLevel% - $[r_%GSw_H2_TransportLevel%(r,%GSw_H2_TransportLevel%) - $r_%GSw_H2_TransportLevel%(rr,%GSw_H2_TransportLevel%)], 1 })] - = no ; -$endif.h2transportlevel - -* Populate H2 pipeline investment routes -h2_routes_inv(r,rr) = h2_routes(r,rr) ; -* Only keep routes with r < rr for investment -h2_routes_inv(r,rr)$(ord(rr) nuke_fom_adj_age_threshold)] = - cost_fom(i,initv,r,t) + nuke_fom_adj * Sw_NukeCoalFOM ; - -table hyd_fom(i,r) "--$/MW-year -- Fixed O&M for hydro technologies" -$offlisting -$ondelim -$include inputs_case%ds%hyd_fom.csv -$offdelim -$onlisting -; - -*note conditional here that will only replace fom -*for hydro techs if it is included in hyd_fom(i,r) -cost_fom(i,v,r,t)$[valcap(i,v,r,t)$hydro(i)$hyd_fom(i,r)] = hyd_fom(i,r) ; - -* Add FOM cost for dr shed resource to cost_fom -cost_fom(i,v,r,t)$[valcap(i,v,r,t)$dr_shed(i)] = fom_dr_shed(i,r,t) ; - -cost_fom(i,initv,r,t)$[(not Sw_BinOM)$valcap(i,initv,r,t)] = sum{tt$tfirst(tt), cost_fom(i,initv,r,tt) } ; - -*upgrade fom costs for initial classes are the fom costs for that tech -*plus the delta between upgrade_to and upgrade_from for the initial year -cost_fom(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,initv,r,t)] = - sum{(v,ii,tt)$[newv(v)$ivt(ii,v,tt)$upgrade_to(i,ii)$(tt.val=Sw_UpgradeChar_Year)], - plant_char(ii,v,tt,"FOM") + hyd_fom(ii,r)$hydro(ii) } -; - -*if available, set cost_fom for upgrades of CCS plants to those specified in hintage_data -cost_fom(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$ccs(i)$Sw_UpgradeFOM_Nems$unitspec_upgrades(i) - $sum{ii$upgrade_from(i,ii), valcap(ii,initv,r,t) } - $sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_FOM") }] = - 1e3 * sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_FOM") } -; - -*upgrade fom costs for new classes are the fom costs -*of the plant that it is being upgraded to -cost_fom(i,newv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,newv,r,t)] = - sum{ii$upgrade_to(i,ii), cost_fom(ii,newv,r,t) } ; - -*==================== -* --- Heat Rates --- -*==================== - -parameter heat_rate(i,v,r,t) "--MMBtu/MWh-- heat rate" ; - -heat_rate(i,v,r,t)$valcap(i,v,r,t) = plant_char(i,v,t,'heatrate') ; - -heat_rate(i,newv,r,t)$[valcap(i,newv,r,t)$countnc(i,newv)] = - sum{tt$ivt(i,newv,tt), plant_char(i,newv,tt,'heatrate') } / countnc(i,newv) ; - -* fill in heat rate for initial capacity that does not have a binned heatrate -heat_rate(i,initv,r,t)$[valcap(i,initv,r,t)$(not heat_rate(i,initv,r,t))] = plant_char(i,initv,"%startyear%",'heatrate') ; - -*note here conversion from btu/kwh to MMBtu/MWh -heat_rate(i,v,r,t)$[valcap(i,v,r,t)$sum{allt$att(allt,t), binned_heatrates(i,v,r,allt) }] = - sum{allt$att(allt,t), binned_heatrates(i,v,r,allt) } / 1000 ; - - -set prepost "set defining pre-2010 values versus post-2010 values" -/ -$offlisting -$include inputs_case%ds%prepost.csv -$onlisting -/ ; - -*part load heatrate adjust based on historical EIA generation and fuel use data -*this reflects the indescrepancy from the partial-loaded heat rate -*and the fully-loaded heat rate - -table heat_rate_adj(i,prepost) "--unitless-- partial load heatrate adjuster based on historical EIA generation and fuel use data" -$offlisting -$ondelim -$include inputs_case%ds%heat_rate_adj.csv -$offdelim -$onlisting -; - -heat_rate_adj(i,prepost)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), heat_rate_adj(ii,prepost) } ; - -heat_rate_adj(i,prepost)$upgrade(i) = sum{ii$upgrade_to(i,ii), heat_rate_adj(ii,prepost) } ; - -*upgrade heat rates for initial classes are the heat rates for that tech -*plus the delta between upgrade_to and upgrade_from for the initial year -heat_rate(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,initv,r,t)] = - sum{(v,ii,tt)$[newv(v)$ivt(ii,v,tt)$upgrade_to(i,ii)$(tt.val=Sw_UpgradeChar_Year)], - plant_char(ii,v,tt,"heatrate") } -; - -*if available, set heat_rate for upgrades of CCS plants to those specified in hintage_data -heat_rate(i,initv,r,t)$[upgrade(i)$Sw_Upgrades$ccs(i) - $sum{ii$upgrade_from(i,ii), valcap(ii,initv,r,t) } - $sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_HR") }] = - sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wCCS_Retro_HR") / 1000 } -; - -*upgrade heat rates for new classes are the heat rates for -*the bintage and technology for what it is being upgraded to -heat_rate(i,newv,r,t)$[upgrade(i)$Sw_Upgrades$valcap(i,newv,r,t)] = - sum{ii$upgrade_to(i,ii), heat_rate(ii,newv,r,t) } ; - -heat_rate(i,v,r,t)$[heat_rate_adj(i,'pre2010')$initv(v)] = heat_rate_adj(i,'pre2010') * heat_rate(i,v,r,t) ; -heat_rate(i,v,r,t)$[heat_rate_adj(i,'post2010')$newv(v)] = heat_rate_adj(i,'post2010') * heat_rate(i,v,r,t) ; - -*========================================= -* --- Fuel Prices --- -*========================================= - -parameter fuel_price(i,r,t) "$/MMBtu - fuel prices by technology" ; - - -* Written by input_processing\fuelcostprep.py -* declared over allt to allow for external data files that extend beyond end_year -table fprice(allt,r,f) "--2004$/MMBtu-- fuel prices by fuel type" -$offlisting -$ondelim -$include inputs_case%ds%fprice.csv -$offdelim -$onlisting -; - -fuel_price(i,r,t)$[sum{f$fuel2tech(f,i),1}] = - sum{(f,allt)$[fuel2tech(f,i)$(year(allt)=yeart(t))], fprice(allt,r,f) } ; - -fuel_price(i,r,t)$[sum{f$fuel2tech(f,i),1}$(not fuel_price(i,r,t))] = - sum{rr$fuel_price(i,rr,t), fuel_price(i,rr,t) } / max(1,sum{rr$fuel_price(i,rr,t), 1 }) ; - -fuel_price(i,r,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), fuel_price(ii,r,t) } ; - - -*===================================================== -* -- Climate impacts on nondispatchable hydropower -- -*===================================================== - -$ifthen.climatehydro %GSw_ClimateHydro% == 1 - -* declared over allt to allow for external data files that extend beyond end_year -* Written by climateprep.py -table climate_hydro_annual(r,allt) "annual dispatchable hydropower availability" -$offlisting -$ondelim -$include inputs_case%ds%climate_hydadjann.csv -$offdelim -$onlisting -; -$endif.climatehydro - - -*===================================================== -* -- Climate impacts on nondispatchable hydropower -- -*===================================================== - -$ifthen.climatewater %GSw_ClimateWater% == 1 - -* Written by climateprep.py -table wat_supply_climate(wst,r,allt) "time-varying annual water supply" -$offlisting -$ondelim -$include inputs_case%ds%climate_UnappWaterMultAnn.csv -$offdelim -$onlisting -; - -set wst_climate(wst) "water sources affected by climate change" -/ -$offlisting -$ondelim -$include inputs_case%ds%wst_climate.csv -$offdelim -$onlisting -/ -; -$endif.climatewater - - -*============================================= -* -- Capacity Factor Adjustments over Time -- -*============================================= - -*created by /input_processing/writecapdat.py -parameter cap_hyd_ccseason_adj(i,ccseason,r) "--fraction-- ccseason max capacity adjustment for dispatchable hydro" -/ -$offlisting -$ondelim -$include inputs_case%ds%cap_hyd_ccseason_adj.csv -$offdelim -$onlisting -/ ; - -table wind_cf_adj_t(allt,i) "--unitless-- wind capacity factor adjustments by class, from ATB" -$offlisting -$ondelim -$include inputs_case%ds%windcfmult.csv -$offdelim -$onlisting -; - -parameter pv_cf_improve(allt) "--unitless-- PV capacity factor improvement" -/ -$offlisting -$ondelim -$include inputs_case%ds%pv_cf_improve.csv -$offdelim -$onlisting -/ ; - -parameter cf_adj_t(i,v,t) "--unitless-- capacity factor adjustment over time for RSC technologies" ; - -cf_adj_t(i,v,t)$[(rsc_i(i) or hydro(i))$sum{r, valcap(i,v,r,t) }] = 1 ; - -* Existing wind uses 2010 cf adjustment -cf_adj_t(i,initv,t)$[wind(i)$sum{r, valcap(i,initv,r,t) }] = wind_cf_adj_t("%startyear%",i) ; - -cf_adj_t(i,newv,t)$[wind_cf_adj_t(t,i)$countnc(i,newv)$sum{r, valcap(i,newv,r,t) }] = - sum{tt$ivt(i,newv,tt), wind_cf_adj_t(tt,i) } / countnc(i,newv) ; - -* Apply PV capacity factor improvements -cf_adj_t(i,newv,t)$[(pv(i) or pvb(i))$countnc(i,newv)$sum{r, valcap(i,newv,r,t) }] = - sum{tt$ivt(i,newv,tt), pv_cf_improve(tt) } / countnc(i,newv) ; - - - -*======================================== -* --- OPERATING RESERVES --- -*======================================== - -set ortype "types of operating reserve constraints" -/ -$offlisting -$include inputs_case%ds%ortype.csv -$onlisting -/ ; - -set opres_model(ortype) "operating reserve types modeled" ; - -set orcat "operating reserve category for RHS calculations" -/ -$offlisting -$include inputs_case%ds%orcat.csv -$onlisting -/ ; - -* define elements in opres_model based on sw_opres -opres_model(ortype)$[not Sw_Opres] = no ; -opres_model(ortype)$[(Sw_Opres = 1)$(not sameas(ortype,"combo"))] = yes ; -opres_model("combo")$[(Sw_Opres = 2)] = yes ; - - -Parameter - reserve_frac(i,ortype) "--fraction-- fraction of a technology's online capacity that can contribute to a reserve type" - ramptime(ortype) "--minutes-- minutes for ramping limit constraint in operating reserves" -/ -$offlisting -$ondelim -$include inputs_case%ds%ramptime.csv -$offdelim -$onlisting -/ ; - - -table orperc(ortype,orcat) "operating reserve percentage by type and category" -$offlisting -$ondelim -$include inputs_case%ds%orperc.csv -$offdelim -$onlisting -; - -* for simplified combination, make the constraints as -* stringent as possible - ie sum over all requirements -orperc("combo",orcat) = sum{ortype,orperc(ortype,orcat) } ; - -* combo ramptime is average across all ortypes where defined -ramptime("combo") = sum{ortype$ramptime(ortype) , ramptime(ortype) } - / sum{ortype$ramptime(ortype) , 1 } ; - -* multiplier for reserves requirement -orperc(ortype,orcat) = orperc(ortype,orcat) * Sw_OpResReqMult ; - -*ramp rates are used to limit a technology's contribution to Operating Reserve. -parameter ramprate(i) "--fraction/min-- ramp rate of dispatchable generators" -/ -$offlisting -$ondelim -$include inputs_case%ds%ramprate.csv -$offdelim -$onlisting -/ ; - -*dispatchable hydro is the only "hydro" technology that can provide operating reserves. -ramprate(i)$hydro_d(i) = ramprate("hydro") ; -ramprate(i)$geo(i) = ramprate("geothermal") ; - -*if running with flexible nuclear, set ramp rate of nuclear to that of coal -ramprate(i)$[nuclear(i)$Sw_NukeFlex] = ramprate("coal-new") ; - -ramprate(i)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), ramprate(ii) } ; - -ramprate(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), ramprate(ii) } ; - -* Do not allow the reserve fraction to exceed 100%, so use the minimum of 1 or the computed value. -reserve_frac(i,ortype) = min(1,ramprate(i) * ramptime(ortype)) ; - -reserve_frac(i,ortype)$upgrade(i) = sum{ii$upgrade_to(i,ii), reserve_frac(ii,ortype) } ; - -* input data for opres reserve costs by generator type (in $2004) -* current options are bottom up costs by generator type ("default") or estimates based on historical reserve prices ("market") -* market data based on national reserve prices in https://www.nrel.gov/docs/fy19osti/72578.pdf (converted from $2017) -table cost_opres_input(i,ortype) -$offlisting -$ondelim -$include inputs_case%ds%cost_opres_%GSw_OpResCost%.csv -$offdelim -$onlisting -; - -parameter cost_opres(i,ortype,t) "--$ / MWh-- cost of reg operating reserves" ; -cost_opres(i,ortype,t) = cost_opres_input(i, ortype) ; - -* assign reserve costs to all geothermal techs -cost_opres(i,ortype,t)$geo(i) = cost_opres("geothermal",ortype,t) ; - -* Assign hybrid PV+battery the same value as battery_li -cost_opres(i,ortype,t)$pvb(i) = cost_opres("battery_li",ortype,t) ; - -* add heat rate penalty for providing reserves (currently only applied to spin) -* input data calculated based on heat rates in the PLEXOS EI database as of Dec. 2020 -parameter spin_hr_penalty(i) "--fraction-- heat rate penalty for providing spinning reserves" -/ -$offlisting -$ondelim -$include inputs_case%ds%heat_rate_penalty_spin.csv -$offdelim -$onlisting -/ ; - -* calculate average heat rate and fuel prices -parameter fuel_price_avg(i,t) ; -parameter heat_rate_avg(i,t) ; - -* calculate average fuel price and heat rates -fuel_price_avg(i,t)$[sum{r, fuel_price(i,r,t) }] = sum{r, fuel_price(i,r,t) } / sum{r, 1$[fuel_price(i,r,t)] } ; - -heat_rate_avg(i,t)$[sum{(v,r), heat_rate(i,v,r,t) }] = - sum{(v,r), heat_rate(i,v,r,t) } / sum{(v,r), 1$[heat_rate(i,v,r,t)] } ; - -* calculate penalty value, assign to cost_opres -* only assign penalty in instances where spin costs are not already defined -cost_opres(i,"spin",t)$[not cost_opres(i,"spin",t)] = - spin_hr_penalty(i) * heat_rate_avg(i,t) * fuel_price_avg(i,t) ; - -cost_opres(i,ortype,t)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), cost_opres(ii,ortype,t) } ; - -cost_opres(i,ortype,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), cost_opres(ii,ortype,t) } ; - -* again - making the assumption that the combination -* operating reserve is the most stringent/costly -cost_opres(i,"combo",t) = smax{ortype, cost_opres(i,ortype,t) } ; - - -*====================================================== -* --- MinLoading (only used if Sw_MinLoading != 0) --- -*====================================================== - -parameter minloadfrac0(i) "--fraction-- initial minimum loading fraction" -/ -$offlisting -$ondelim -$include inputs_case%ds%minloadfrac0.csv -$offdelim -$onlisting -/ ; - -minloadfrac0(i)$geo(i) = minloadfrac0("geothermal") ; - -parameter hydmin_quarter(i,r,quarter) "minimum hydro loading factors by quarter and region" -/ -$offlisting -$ondelim -$include inputs_case%ds%hydro_mingen.csv -$offdelim -$onlisting -/ ; - -parameter startcost(i) "--$/MW-- linearized startup cost" -/ -$offlisting -$ondelim -$include inputs_case%ds%startcost.csv -$offdelim -$onlisting -/ ; -startcost(i)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), startcost(ii) } ; -startcost(i)$upgrade(i) = sum{ii$upgrade_to(i,ii), startcost(ii) } ; -* Turn off startcost for some techs based on GSw_StartCost -startcost(i)$[(Sw_StartCost=0)] = 0 ; -startcost(i)$[(Sw_StartCost=1)$(not nuclear(i))] = 0 ; -startcost(i)$[(Sw_StartCost=2)$(not nuclear(i))$(not ccs(i))$(not coal(i))] = 0 ; -startcost(i)$[(Sw_StartCost=3)$(not ccs(i))$(not coal(i))] = 0 ; -startcost(i)$[(Sw_StartCost=4)$(not ccs(i))$(not coal(i))$(not gas_cc(i))$(not h2_cc(i))] = 0 ; -startcost(i)$[(Sw_StartCost=5)$(nuclear(i))] = 0 ; - -parameter mingen_fixed(i) "--fraction-- minimum generation level across all hours" -/ -$offlisting -$ondelim -$include inputs_case%ds%mingen_fixed.csv -$offdelim -$onlisting -/ ; - -*========================================= -* --- Load --- -*========================================= - -$onempty -parameter loadsite_annual(loadsitereg,allt) "--MW-- Load trajectory by loadsitereg" -/ -$offlisting -$ondelim -$include inputs_case%ds%loadsite_annual.csv -$offdelim -$onlisting -/ ; -set val_loadsite(r) "Valid regions for load sites" ; -val_loadsite(r) - $sum{(loadsitereg,t)$r_loadsitereg(r,loadsitereg), - loadsite_annual(loadsitereg,t) - } = yes ; - -table can_growth_rate(st,allt) "growth rate for candadian demand by province" -$offlisting -$ondelim -$include inputs_case%ds%cangrowth.csv -$offdelim -$onlisting -; - -parameter mex_growth_rate(allt) "growth rate for mexican demand - national" -/ -$offlisting -$ondelim -$include inputs_case%ds%mex_growth_rate.csv -$offdelim -$onlisting -/ ; -$offempty - - -*============================== -* --- Planning Reserve Margin --- -*================================ - -parameter prm(r,t) "--fraction-- planning reserve margin by model year" -/ -$offlisting -$ondelim -$include inputs_case%ds%prm_initial.csv -$offdelim -$onlisting -/ ; - -$onempty -parameter firm_import_limit(nercr,allt) "--fraction-- limit on net firm imports into NERC regions" -/ -$offlisting -$ondelim -$include inputs_case%ds%firm_import_limit.csv -$offdelim -$onlisting -/ ; - -parameter peakload_nercr(nercr,allt) "--MW-- Peak exogenous demand across all weather years by NERC region" -/ -$offlisting -$ondelim -$include inputs_case%ds%peakload_nercr.csv -$offdelim -$onlisting -/ ; -$offempty - - -* =========================================================================== -* Regional and temporal capital cost multipliers -* =========================================================================== -* Load scenario-specific capital cost multiplier components - -parameter ccmult(i,allt) "construction cost multiplier" -/ -$offlisting -$ondelim -$include inputs_case%ds%ccmult.csv -$offdelim -$onlisting -/ ; - -parameter tax_rate(allt) "all-in tax rate" -/ -$offlisting -$ondelim -$include inputs_case%ds%tax_rate.csv -$offdelim -$onlisting -/ ; - -parameter itc_frac_monetized(i,allt) "fractional value of the ITC, after adjusting for the costs of monetization" -/ -$offlisting -$ondelim -$include inputs_case%ds%itc_frac_monetized.csv -$offdelim -$onlisting -/ ; - -$onempty -parameter itc_energy_comm_bonus(i,r) "energy community tax credit bonus factor" -/ -$offlisting -$ondelim -$include inputs_case%ds%itc_energy_comm_bonus.csv -$offdelim -$onlisting -/ ; -$offempty - -parameter pv_frac_of_depreciation(i,allt) "present value of depreciation, expressed as a fraction of the capital cost of the investment" -/ -$offlisting -$ondelim -$include inputs_case%ds%pv_frac_of_depreciation.csv -$offdelim -$onlisting -/ ; - -parameter degradation_adj(i,allt) "adjustment to reflect degradation over the lifetime of an asset" -/ -$offlisting -$ondelim -$include inputs_case%ds%degradation_adj.csv -$offdelim -$onlisting -/ ; - -parameter financing_risk_mult(i,allt) "multiplier to reflect higher financing costs for riskier assets" -/ -$offlisting -$ondelim -$include inputs_case%ds%financing_risk_mult.csv -$offdelim -$onlisting -/ ; - -parameter reg_cap_cost_diff(i,r) "regional capital cost difference [fraction] (note that wind-ons and upv have separate multiplers in the supply curve cost)" -/ -$offlisting -$ondelim -$include inputs_case%ds%reg_cap_cost_diff.csv -$offdelim -$onlisting -/ ; - -parameter eval_period_adj_mult(i,allt) "adjustment multiplier for the capital costs of techs with non-standard evaluation periods" -/ -$offlisting -$ondelim -$include inputs_case%ds%eval_period_adj_mult.csv -$offdelim -$onlisting -/ ; - -eval_period_adj_mult(i,t)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), eval_period_adj_mult(ii,t) } ; -eval_period_adj_mult(i,t)$[upgrade(i)] = - sum{ii$upgrade_to(i,ii),eval_period_adj_mult(ii,t) } ; - - -* Define and calculate the scenario-specific capital cost multipliers -* If the ITC is phasing out dynamically, these will need to be re-calculated based on the phase-out -parameter -cost_cap_fin_mult(i,r,t) "final capital cost multiplier for regions and technologies - used in the objective function", -cost_cap_fin_mult_noITC(i,r,t) "final capital cost multiplier excluding ITC - used only in outputs", -cost_cap_fin_mult_no_credits(i,r,t) "final capital cost multiplier ITC/PTC/Depreciation (i.e. the actual expenditures) - used only in outputs", -cost_cap_fin_mult_out(i,r,t) "final capital cost multiplier for system cost outputs" ; - -parameter trans_cost_cap_fin_mult(allt) "capital cost multiplier for transmission - used in the objective function" -/ -$offlisting -$ondelim -$include inputs_case%ds%trans_cap_cost_mult.csv -$offdelim -$onlisting -/ ; - -parameter trans_cost_cap_fin_mult_noITC(allt) "capital cost multiplier for transmission excluding ITC - used only in outputs" -/ -$offlisting -$ondelim -$include inputs_case%ds%trans_cap_cost_mult_noITC.csv -$offdelim -$onlisting -/ ; - - -* --- Hybrid PV+Battery --- -* Hybrid PV+Battery: PV portion -parameter cost_cap_fin_mult_pvb_p(i,r,t) "capital cost multiplier for the PV portion of hybrid PV+Battery" - cost_cap_fin_mult_pvb_p_noITC(i,r,t) "capital cost multiplier for the PV portion of hybrid PV+Battery, excluding ITC" - cost_cap_fin_mult_pvb_p_no_credits(i,r,t) "capital cost multiplier for the PV portion of hybrid PV+Battery, excluding ITC/PTC/Depreciation" -; - -* Hybrid PV+Battery: Battery portion -parameter cost_cap_fin_mult_pvb_b(i,r,t) "capital cost multiplier for the battery portion of hybrid PV+Battery" - cost_cap_fin_mult_pvb_b_noITC(i,r,t) "capital cost multiplier for the battery portion of hybrid PV+Battery, excluding ITC" - cost_cap_fin_mult_pvb_b_no_credits(i,r,t) "capital cost multiplier for the battery portion of hybrid PV+Battery, excluding ITC/PTC/Depreciation" -; - - -* --- Nuclear Ban --- -*Assign increased cost multipliers to regions with state nuclear bans -scalar nukebancostmult "--fraction-- penalty for constructing new nuclear in a restricted region" /%GSw_NukeStateBanCostMult%/ ; - -* --- Renewable Supply Curves --- -* For offshore wind, rsc_fin_mult(i,r,t) also carries the ITC that is applied to the transmission costs in the resource supply curve cost, while rsc_fin_mult_no_ITC(i,r,t) carries financing multipliers for its transmission costs without the ITC -parameter rsc_fin_mult(i,r,t) "--fraction-- financial cost multiplier for resource supply curve technologies that have their capital costs included in the supply curves (capital cost reduction multipliers are also included where relevant)" - rsc_fin_mult_noITC(i,r,t) "--fraction-- financial cost multiplier excluding ITC for resource supply curve technologies that have their capital costs included in the supply curves" -; - -*========================================= -* --- Emission Rate --- -*========================================= - -* Emission rate by technology and etype (broken down to process and upstream) -* Note that CH4 upstream emission rate of natural gas is 0 here -* as we will use CH4 methane leakage from GSw_MethaneLeakageScen for it later) -table emit_rate_fuel(i,etype,e) "--metric tons per MMBtu-- emissions rate of fuel by technology and emission type" -$offlisting -$ondelim -$include inputs_case%ds%emitrate.csv -$offdelim -$onlisting -; - -* this table links CCS techs with their uncontrolled tech counterpart (where such a tech exists) -set ccs_link(i,ii) "links CCS techs with their uncontrolled tech counterpart (where such a tech exists)" -/ -$offlisting -$ondelim -$include inputs_case%ds%ccs_link.csv -$ifthen.ctech %GSw_WaterMain% == 1 -$include inputs_case%ds%ccs_link_water.csv -$endif.ctech -$offdelim -$onlisting -/ ; - -parameter capture_rate_input(i,e) "--fraction-- fraction of emissions that are captured" ; - -* Set CO2 capture rate for new CCS capacity -capture_rate_input(i,"CO2")$[ccs_mod(i)]=Sw_CCS_Rate_New_mod; -capture_rate_input(i,"CO2")$[ccs_max(i)]=Sw_CCS_Rate_New_max; - -* Set CO2 capture rate for retrofit/upgrade CCS capacity -capture_rate_input(i,"CO2")$[upgrade(i)$(coal_ccs(i) or gas_cc_ccs(i))$ccs_mod(i)]=Sw_CCS_Rate_Upgrade_mod; -capture_rate_input(i,"CO2")$[upgrade(i)$(coal_ccs(i) or gas_cc_ccs(i))$ccs_max(i)]=Sw_CCS_Rate_Upgrade_max; - -* emit_rate_fuel water expansion -emit_rate_fuel(i,etype,e)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), emit_rate_fuel(ii,etype,e) } ; - -* Assign the appropriate % of generation for each technology to count toward CES requirements. -* Exclude capture rates of BECCS, which receive full credit in a CES and were already set to 1 above in the "RPS" section. -RPSTechMult(RPSCat,i,st)$[ccs(i)$(sameas(RPSCat,"CES") or sameas(RPSCat,"CES_Bundled"))$(not beccs(i))] = capture_rate_input(i,"CO2") ; - -* calculate process emit rate for CCS techs (except beccs techs, which are defined directly in emitrate.csv) -emit_rate_fuel(i,"process",e)$[ccs(i)$(not beccs(i))] = - (1 - capture_rate_input(i,e)) * sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"process",e) } ; - -* calculate upstream emit rate for CCS techs (except beccs techs, which are defined directly in emitrate.csv) -emit_rate_fuel(i,"upstream",e)$[ccs(i)$(not beccs(i))] = sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"upstream",e) } ; - -* assign flexible ccs the same process emission rate as the uncontrolled technology to allow variable CO2 removal (e.g., for gas-cc-ccs-f1, use gas-cc) -emit_rate_fuel(i,"process",e)$[ccsflex(i)] = sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"process",e) } ; - -* set upgrade tech process emissions for non-CCS upgrades (e.g. gas-ct -> h2-ct); CCS upgrade emissions are handled above -emit_rate_fuel(i,"process",e)$[upgrade(i)$(not ccs(i))] = sum{ii$upgrade_to(i,ii), emit_rate_fuel(ii,"process",e) } ; - -* set upgrade tech upstream emissions for upgrades -emit_rate_fuel(i,"upstream",e)$[upgrade(i)] = sum{ii$upgrade_to(i,ii), emit_rate_fuel(ii,"upstream",e) } ; - -* parameters for calculating captured emissions -parameter capture_rate_fuel(i,e) "--metric tons per MMBtu-- emissions capture rate of fuel by technology type"; -capture_rate_fuel(i,e) = capture_rate_input(i,e) * sum{ii$ccs_link(i,ii), emit_rate_fuel(ii,"process",e) } ; - -* capture_rate_fuel is used to calculate how much CO2 is captured and stored; -* for beccs, the captured CO2 is the entire negative emissions rate -* since any uncontrolled emissions are assumed to be lifecycle net zero -capture_rate_fuel(i,"CO2")$beccs(i) = - emit_rate_fuel(i,"process","CO2") - -parameter capture_rate(e,i,v,r,t) "--metric tons per MWh-- emissions capture rate" ; - -parameter methane_leakage_rate(allt) "--fraction-- methane leakage as fraction of gross production" -* best estimate for fixed leakage rate is 0.023 (Alvarez et al. 2018, https://dx.doi.org/10.1126/science.aar7204) -/ -$offlisting -$ondelim -$include inputs_case%ds%methane_leakage_rate.csv -$offdelim -$onlisting -/ ; - -scalar methane_tonperMMBtu "--metric tons per MMBtu-- methane content of natural gas" ; -* [ton CO2 / MMBtu] * [ton CH4 / ton CO2] -methane_tonperMMBtu = emit_rate_fuel("gas-CC","process","CO2") * molWeightCH4 / molWeightCO2 ; - -* H2 leakage rate by technology and etype (broken down to process and upstream) -parameter h2_leakage_rate(i) "--fraction-- h2 leakage rate as a fraction of total production by technology and emission type" -/ -$offlisting -$ondelim -$include inputs_case%ds%h2_leakage_rate.csv -$offdelim -$onlisting -/ ; - -parameter prod_emit_rate(etype,e,i,allt) "--metric tons emitted per metric ton product-- emissions rate per metric ton of product (e.g. tonCO2/tonH2 for SMR & SMR-CCS)" ; -* Steam methane reformer (SMR)'s process emission here refers to emissions from steam methane reforming process -prod_emit_rate("process","CO2","smr",t) = smr_co2_intensity ; -prod_emit_rate("process","CO2","smr_ccs",t) = smr_co2_intensity * (1 - smr_capture_rate) ; -prod_emit_rate("process","CO2","dac",t)$Sw_DAC = -1 ; -prod_emit_rate("process","CO2","dac_gas",t)$Sw_DAC_Gas = -1 ; - -scalar smr_methane_rate "--metric tons CH4 per metric ton H2-- methane used to produce a metric ton of H2 via SMR" ; -* NOTE that we don't yet include the impact of CCS on methane use -* [ton CH4 used / ton H2] = [ton CO2 emitted / ton H2] * [ton CH4 used / ton CO2 emitted], where -* [ton CH4 used / ton CO2 emitted] is the ratio of the molecular weight of CH4 to CO2 -smr_methane_rate = smr_co2_intensity * molWeightCH4 / molWeightCO2 ; - -* Upstream fuel emissions for SMR -*** [ton CH4 used / ton H2] * [ton CH4 leaked / ton CH4 produced] * [ton CH4 produced / ton CH4 used] -prod_emit_rate("upstream",e,i,t) - $[sameas(e,"CH4") - $smr(i) - $methane_leakage_rate(t)] - = smr_methane_rate * methane_leakage_rate(t) / (1 - methane_leakage_rate(t)) -; - -* Process H2 emissions for SMR, SMR-CC, and electrolyzer -*** [ton H2 leaked / ton H2 produced] * [ton H2 produced / ton H2 used] -prod_emit_rate("process",e,i,t) - $[sameas(e,"H2") - $h2(i) - $h2_leakage_rate(i)] - = h2_leakage_rate(i) / (1 - h2_leakage_rate(i)) -; - -parameter - emit_rate(etype,eall,i,v,r,t) "--metric tons per MWh-- emissions rate" - emit_r_tc(r,t) "--metric tons-- CO2 emissions, regional" - emit_nat_tc(t) "--metric tons-- CO2 emissions, national" -; - -emit_rate(etype,e,i,v,r,t)$[emit_rate_fuel(i,etype,e)$valcap(i,v,r,t)] - = round(heat_rate(i,v,r,t) * emit_rate_fuel(i,etype,e),10) ; - -*only emissions from the coal portion of cofire plants are considered -emit_rate(etype,e,i,v,r,t)$[sameas(i,"cofire")$emit_rate_fuel("coal-new",etype,e)$valcap(i,v,r,t)] - = round((1-bio_cofire_perc) * heat_rate(i,v,r,t) * emit_rate_fuel("coal-new",etype,e),10) ; - -* Fill in CH4 upstream emission rate -*** [MMBtu/MWh] * [ton methane used / MMBtu] * [ton methane leaked / ton methane produced] -*** * [ton methane produced / ton methane used] = [ton methane leaked / MWh] -emit_rate("upstream",e,i,v,r,t) - $[methane_leakage_rate(t) - $gas(i) - $sameas(e,"CH4")] - = heat_rate(i,v,r,t) * methane_tonperMMBtu * methane_leakage_rate(t) / (1 - methane_leakage_rate(t)) -; - -* Fill in H2 process emission rates for H2 combustion techs (h2-ct and h2-cc) (and fuel cell later) -*** [heat rate (MMBtu/MWh)] * [h2 combustion intensity (metric ton h2 used / MMBtu)] * [h2 leakage rate (metric ton h2 leaked / ton h2 produced)] -*** * [ton h2 produced / ton h2 used] = [ton h2 leaked / MWh] -emit_rate("process",e,i,v,r,t) - $[h2_leakage_rate(i) - $sameas(e,"H2")] - = heat_rate(i,v,r,t) * h2_combustion_intensity * h2_leakage_rate(i) / (1 - h2_leakage_rate(i)) -; - -* set upgraded H2 tech emissions -emit_rate("process","H2",i,v,r,t)$[upgrade(i)] = sum{ii$upgrade_to(i,ii), emit_rate("process","H2",ii,v,r,t) } ; - -* Global warming potential of different pollutants -parameter gwp(e) "--metric ton CO2-equivalents --global warming potential" -/ -$ondelim -$include inputs_case%ds%gwp.csv -$offdelim -/ ; - -* CO2(e) emissions rate (used in postprocessing only) -emit_rate(etype,"CO2e",i,v,r,t)$[Sw_AnnualCap=2] - = round(sum{e, emit_rate(etype,e,i,v,r,t) * gwp(e)$[(not sameas(e, "H2"))]},10) ; - -emit_rate(etype,"CO2e",i,v,r,t)$[Sw_AnnualCap<>2] - = round(sum{e, emit_rate(etype,e,i,v,r,t) * gwp(e)},10) ; - -* calculate emissions capture rates (same logic as emissions calc above) -capture_rate(e,i,v,r,t)$[capture_rate_fuel(i,e)$valcap(i,v,r,t)] - = round(heat_rate(i,v,r,t) * capture_rate_fuel(i,e),10) ; - -capture_rate(e,i,v,r,t)$[upgrade(i)$capture_rate_fuel(i,e)] = round(heat_rate(i,v,r,t) * capture_rate_fuel(i,e),10) ; - -* Declare regional emissions rate (used in c_supplymodel, defined in d_solveoneyear) -parameter - co2_emit_rate_r(r,t) "--metric tons per MWh-- CO2 emissions rate by ReEDS region, for use in state carbon caps" - co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t) "--metric tons per MWh-- CO2 regional emissions rate, for use in state carbon caps" -; -co2_emit_rate_r(r,t) = 0 ; -co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t) = 0 ; - -* =========================================================================== -* Regional emissions rate limit (currently unused) -* =========================================================================== - -set emit_rate_con(e,r,t) "set to enable or disable emissions rate limits by pollutant and region" ; -emit_rate_con(e,r,t) = no ; - -parameter emit_rate_limit(e,r,t) "--metric tons per MWh-- emission rate limit" ; -emit_rate_limit(e,r,t) = 0 ; - -*============================ -* Growth limits and penalties -*============================ - -set gbin "growth bins" -/ -$offlisting -$include inputs_case%ds%gbin.csv -$onlisting -/ ; - -*absolute growth penalties based on greatest annual change of capacity for each tech group from 1990-2016 -parameter growth_limit_absolute(tg) "--MW-- growth limit for technology groups in absolute terms" -/ -$offlisting -$ondelim -$include inputs_case%ds%growth_limit_absolute.csv -$offdelim -$onlisting -/ ; - -parameter growth_penalty(gbin) "--unitless-- multiplier penalty on the capital cost for growth in each bin" -/ -$offlisting -$ondelim -$include inputs_case%ds%growth_penalty.csv -$offdelim -$onlisting -/ ; - -* gbin_min is based on the representative plant size for a single plant in that tech group -parameter gbin_min(tg) "--MW-- minimum size of the first (zero cost) growth bin" -/ -$offlisting -$ondelim -$include inputs_case%ds%gbin_min.csv -$offdelim -$onlisting -/ ; - -parameter growth_bin_size_mult(gbin) "--unitless-- multiplier for each growth bin to be applied to the prior solve year's annual deployment" -/ -$offlisting -$ondelim -$include inputs_case%ds%growth_bin_size_mult.csv -$offdelim -$onlisting -/ ; - -parameter growth_bin_limit(gbin,st,tg,t) "--MW/yr-- size of each growth bin" - last_year_max_growth(st,tg,t) "--MW-- maximum growth that could have been achieved in the prior year (acutal year, not solve year)" - cost_growth(i,st,t) "--$/MW-- cost basis for growth penalties" -; - -* Initialize values -growth_bin_limit(gbin,st,tg,tfirst)$stfeas(st) = gbin_min(tg) ; -cost_growth(i,st,t) = 0 ; - -*==================================== -* --- CES Gas supply curve setup --- -*==================================== - -set gb "gas price bin must be an odd number of bins, e.g. gb1*gb15" -/ -$offlisting -$include inputs_case%ds%gb.csv -$onlisting -/ ; - -alias(gb,gbb) ; - -* gassupply scale determines how far the bins reference quantity should deviate from its reference price -* with gassupply scale = -0.5, the center of the reference price bin will be the reference quantity -* with gassupplyscale = 0, the end of the reference gas price's bin will the limit for that reference bin - -*note that the supply curve is set up such that the edge of the bin pertaining to the reference -*price sits at its upper limit, we want to move the curve such that the reference price sits at the middle of the -*respective bin - -parameter gasprice(cendiv,gb,t) "--$/MMBtu-- price of each gas bin", - gasquant(cendiv,gb,t) "--MMBtu - natural gas quantity for each bin", - gaslimit(cendiv,gb,t) "--MMBtu-- gas limit by gas bin" - gassupply_ele(cendiv,t) "--MMBtu-- reference gas consumption by the ELE sector" - gassupply_tot(cendiv,t) "--MMBtu-- reference gas consumption by the ELE sector" ; - -* declared over allt to allow for external data files that extend beyond end_year -table gasprice_ref(cendiv,allt) "--2004$/MMBtu-- natural gas price by census division" -$offlisting -$ondelim -$include inputs_case%ds%gasprice_ref.csv -$offdelim -$onlisting -; - -* fuel costs for H2 production -* starting units for gas efficiency are MMBtu / kg - need to express this in terms of -* $ / MT through MMBtu / kg * (kg / MT) * ($ / MMBtu) -* SMR production costs seem high given gas-intensity and units -* -- cost of production, for now, just gas_intensity times reference gas price, can revisit gas price assumptions -- -parameter h2_fuel_cost(i,v,r,t) "--$ per metric ton-- fuel cost for hydrogen production" ; -h2_fuel_cost(i,newv,r,t)$[h2(i)$valcap(i,newv,r,t)] = 1000 * (sum{tt$ivt(i,newv,tt),consume_char0(i,tt,"gas_efficiency") } / countnc(i,newv)) - * sum{cendiv$r_cendiv(r,cendiv),gasprice_ref(cendiv,t) } * industrialGasMult ; - -* initial capacity gets charged at the initial NG efficiency -h2_fuel_cost(i,initv,r,t)$[h2(i)$valcap(i,initv,r,t)] = 1000 * consume_char0(i,"%startyear%","gas_efficiency") - * sum{cendiv$r_cendiv(r,cendiv),gasprice_ref(cendiv,t) } * industrialGasMult ; - -* -- adding in $ / metric ton adder for transport and storage and h2 vom cost -parameter - h2_stor_tran(i,t) "--$ per metric ton-- adder for the cost of hydrogen transport and storage" - h2_vom(i,t) "--$ per metric ton-- variable cost of hydrogen production" -; - -* h2_stor_tran cost applies if running hydrogen nationally (Sw_H2=1) -* if running regionally (Sw_H2=2) the costs are endogenized in the h2 network -h2_stor_tran(i,t)$[Sw_H2=1] = deflator("2016") * consume_char0(i,t,"stortran_adder") ; - -* option to apply a uniform H2 storage/transport cost that does not vary by tech or year -* note that this overrides input values from the consume_char input file -h2_stor_tran(i,t)$[(Sw_H2=1)$Sw_H2_TransportUniform$h2(i)$sum{(v,r), valcap(i,v,r,t) }] = Sw_H2_TransportUniform ; - -* multiply vom by 1000 because input costs are in $/kg -h2_vom(i,t)$h2(i) = deflator("2016") * consume_char0(i,t,"vom") * 1000 ; - -* total cost of h2 production activities ($ per metric ton) -cost_prod(i,v,r,t)$[h2(i)$valcap(i,v,r,t)] = h2_fuel_cost(i,v,r,t) + h2_vom(i,t) + h2_stor_tran(i,t) ; - -* include VOM for DAC in cost_prod -cost_prod(i,v,r,t)$[dac(i)$valcap(i,v,r,t)] = consume_char0(i,t,"vom") ; - - -table gasquant_elec(cendiv,allt) "--Quads-- Natural gas consumption in the electricity sector" -$offlisting -$ondelim -$include inputs_case%ds%ng_demand_elec.csv -$offdelim -$onlisting -; - -table gasquant_tot(cendiv,allt) "--Quads-- Total natural gas consumption" -$offlisting -$ondelim -$include inputs_case%ds%ng_demand_tot.csv -$offdelim -$onlisting -; - -*need to convert from quadrillion btu to million btu -gassupply_ele(cendiv,t) = 1e9 * gasquant_elec(cendiv,t) ; -gassupply_tot(cendiv,t) = 1e9 * gasquant_tot(cendiv,t) ; - - -parameter -gassupply_ele_nat(t) "--quads-- national reference gas supply for electricity " , -gasprice_nat(t) "--$/MMBtu-- national NG price", -gasquant_nat(t) "--quads-- national NG usage", -gasquant_nat_bin(gb,t) "--quads-- national NG quantity by bin", -gasprice_nat_bin(gb,t) "--$/MMbtu-- price for each national NG bin", -gaslimit_nat(gb,t) "--MMbtu-- national gas bin limit" ; - -gassupply_ele_nat(t) = sum{cendiv$gassupply_ele(cendiv,t), gassupply_ele(cendiv,t) } ; - -gasprice_nat(t) = sum{cendiv$gassupply_ele(cendiv,t), gassupply_ele(cendiv,t) * gasprice_ref(cendiv,t) } - / gassupply_ele_nat(t) ; - -*now compute the amounts going into each gas bin -*this is computed as the amount relative to the reference amount based on the ordinal of the -*gas bin - e.g. gas bin 4 (with a central gas bin of 6 and bin width of 0.1) -*will be gassupply_ele * (1+4-6*0.1) = 0.8 * reference -gasquant(cendiv,gb,t)$gassupply_ele(cendiv,t) = gassupply_ele(cendiv,t) * - (1+(ord(gb)-(smax(gbb,ord(gbb)) / 2 + 0.5)) * 0.1) ; - - -gasquant_nat_bin(gb,t)$gassupply_ele_nat(t) = gassupply_ele_nat(t) * - (1+(ord(gb)-(smax(gbb,ord(gbb)) / 2 + 0.5)) * 0.1) ; - - -gasprice(cendiv,gb,t)$gassupply_ele(cendiv,t) = - gas_scale * round(gasprice_ref(cendiv,t) * - ( -* numerator is the quantity in the bin -* [plus] all natural gas usage -* [minus] gas usage in the ele sector - (gasquant(cendiv,gb,t) + gassupply_tot(cendiv,t) - gassupply_ele(cendiv,t)) - /(gassupply_tot(cendiv,t)) - ) ** (1 / gas_elasticity),4) ; - - -gasprice_nat_bin(gb,t)$sum{cendiv, gassupply_tot(cendiv,t) } = - gas_scale * round(gasprice_nat(t) * - ( - (gasquant_nat_bin(gb,t) + sum{cendiv, gassupply_tot(cendiv,t) } - gassupply_ele_nat(t)) - /(sum{cendiv, gassupply_tot(cendiv,t) }) - ) ** (1 / gas_elasticity),4) ; - - -*the quantity available in each bin is the quantity on the supply curve minus the previous bin's quantity supplied -gaslimit(cendiv,gb,t) = round((gasquant(cendiv,gb,t) - gasquant(cendiv,gb-1,t)),0) / gas_scale; - - -gaslimit(cendiv,"gb1",t) = gaslimit(cendiv,"gb1",t) - - gassupplyscale * sum{gb$[ord(gb)=(smax(gbb,ord(gbb)) / 2 + 0.5)],gaslimit(cendiv,gb,t) } ; - -*final category gets a huge bonus so we make sure we do not run out of gas -gaslimit(cendiv,gb,t)$[ord(gb)=smax(gbb,ord(gbb))] = 5 * gaslimit(cendiv,gb,t) ; - - -gaslimit_nat(gb,t) = round((gasquant_nat_bin(gb,t) - gasquant_nat_bin(gb-1,t)),0) / gas_scale; - -gaslimit_nat("gb1",t) = gaslimit_nat("gb1",t) - - gassupplyscale * sum{gb$[ord(gb)=(smax(gbb,ord(gbb)) / 2 + 0.5)],gaslimit_nat(gb,t) } ; - -*final category gets a huge bonus so we make sure we do not run out of gas -gaslimit_nat(gb,t)$(ord(gb)=smax(gbb,ord(gbb))) = 5 * gaslimit_nat(gb,t) ; - -*Penalizing new gas built within cost recovery period of 30 years for states that -* require fossil plants to retire in some future model period. -* This value is calculated as the ratio of CRF_X / CRF_30 where X is the number of -* years until the required retirement year. -$onempty -parameter ng_crf_penalty_st(allt,st) "--unitless-- cost adjustment for NG in states where all NG techs must be retired by a certain year" -/ -$offlisting -$ondelim -$include inputs_case%ds%ng_crf_penalty_st.csv -$offdelim -$onlisting -/ ; -$offempty - -parameter ng_carb_lifetime_cost_adjust(allt) "--unitless-- cost adjustment for NG with full-region zero-carbon policy" -/ -$offlisting -$ondelim -$include inputs_case%ds%ng_crf_penalty.csv -$offdelim -$onlisting -/ ; - -parameter ng_crf_penalty_nat(i,t) "--unitless-- cost adjustment for NG techs that must be retired by a certain year" ; -* Penalize new gas that can be upgraded to recover upgrade costs prior to upgrade within 20 years of a zero-carbon policy -ng_crf_penalty_nat(i,t)$[gas(i)$sum{r, valcap_irt(i,r,t) }] = ((ng_carb_lifetime_cost_adjust(t) - 1) * .2) + 1 ; -* Do not apply the penalty to CCS technologies -ng_crf_penalty_nat(i,t)$[gas(i)$ccs(i)$sum{r, valcap_irt(i,r,t) }] = 1 ; - -*=========================================== -* --- Regional Gas supply curve --- -*=========================================== - -set fuelbin "gas usage bracket" -/ -$offlisting -$include inputs_case%ds%fuelbin.csv -$onlisting -/ ; - -alias(fuelbin,afuelbin) ; - -Scalar numfuelbins "number of fuel bins", - normfuelbinwidth "typical fuel bin width", - botfuelbinwidth "bottom fuel bin width" -; - -parameter cd_beta(cendiv,t) "--$/MMBtu per Quad-- beta value for census divisions' natural gas supply curves", - nat_beta(t) "--$/MMBtu per Quad-- beta value for national natural gas supply curves", - gasbinwidth_regional(fuelbin,cendiv,t) "--MMBtu-- census division's gas bin width", - gasbinwidth_national(fuelbin,t) "--MMBtu-- national gas bin width", - gasbinp_regional(fuelbin,cendiv,t) "--$/MMBtu-- price for each gas bin", - gasusage_national(t) "--MMBtu-- reference national gas usage", - gasbinqq_regional(fuelbin,cendiv,t) "--MMBtu-- regional reference level for supply curve calculation of each gas bin", - gasbinqq_national(fuelbin,t) "--MMBtu-- national reference level for supply curve calculation of each gas bin", - gasbinp_national(fuelbin,t) "--$/MMBtu--price for each national gas bin", - gasmultterm(cendiv,t) "parameter to be multiplied by total gas usage to compute the reference costs of gas consumption, from which the bins deviate" ; - -*note these do not change over years, only exception -* is that the value in the first year is set to zero -parameter cd_beta0(cendiv) "--$/MMBtu per Quad-- reference census division beta levels electric sector" -/ -$offlisting -$ondelim -$include inputs_case%ds%cd_beta0.csv -$offdelim -$onlisting -/ ; - -parameter cd_beta0_allsector(cendiv) "--$/MMBtu per Quad-- reference census division beta levels all sectors" -/ -$offlisting -$ondelim -$include inputs_case%ds%cd_beta0_allsector.csv -$offdelim -$onlisting -/ ; - -$ifthen.gassector %GSw_GasSector% == 'energy_sector' - -*beginning year value is zero (i.e., no elasticity) -cd_beta(cendiv,t)$[not tfirst(t)] = cd_beta0_allsector(cendiv) ; - -nat_beta(t)$(not tfirst(t)) = nat_beta_energy ; - -$else.gassector - -*beginning year value is zero (i.e., no elasticity) -cd_beta(cendiv,t)$[not tfirst(t)] = cd_beta0(cendiv) ; - -*see documentation for how value is calculated -nat_beta(t)$(not tfirst(t)) = nat_beta_nonenergy ; - -$endif.gassector - -* Written by input_processing\fuelcostprep.py -* declared over allt to allow for external data files that extend beyond end_year -table cd_alpha(allt,cendiv) "--$/MMBtu-- alpha value for natural gas supply curves" -$offlisting -$ondelim -$include inputs_case%ds%alpha.csv -$offdelim -$onlisting -; - -table cendiv_weights(r,cendiv) "--unitless-- weights to smooth gas prices between census regions to avoid abrupt price changes at the cendiv borders" -$offlisting -$ondelim -$include inputs_case%ds%cendivweights.csv -$offdelim -$onlisting -; - - -*number of fuel bins is just the sum of fuel bins -numfuelbins = sum{fuelbin, 1} ; - -*note we subtract two here because top and bottom bins are not included -normfuelbinwidth = (normfuelbinmax - normfuelbinmin)/(numfuelbins - 2) ; - -*set the bottom fuel bin width -botfuelbinwidth = normfuelbinmin ; - -*national gas usage computed as sum over census divisions' gas usage -gasusage_national(t) = sum{cendiv, gassupply_ele(cendiv,t) } ; - -*gas bin width is typically the reference gas usage times the bin width -gasbinwidth_regional(fuelbin,cendiv,t) = gassupply_ele(cendiv,t) * normfuelbinwidth ; - -*bottom and top bins get special treatment -*in that they are expanded by botfuelbinwidth and topfuelbinwidth -gasbinwidth_regional(fuelbin,cendiv,t)$[ord(fuelbin) = 1] = gassupply_ele(cendiv,t) * botfuelbinwidth ; -gasbinwidth_regional(fuelbin,cendiv,t)$[ord(fuelbin) = smax(afuelbin,ord(afuelbin))] = - gassupply_ele(cendiv,t) * topfuelbinwidth ; - -*don't want any super small or zero values -- this follows the same calculations in heritage ReEDS -gasbinwidth_regional(fuelbin,cendiv,t)$[gasbinwidth_regional(fuelbin,cendiv,t) < 10] = 10 ; - -*gas bin widths are defined similarly on the national level -gasbinwidth_national(fuelbin,t) = gasusage_national(t) * normfuelbinwidth ; -gasbinwidth_national(fuelbin,t)$[ord(fuelbin) = 1] = gasusage_national(t) * botfuelbinwidth ; -gasbinwidth_national(fuelbin,t)$[ord(fuelbin)=smax(afuelbin,ord(afuelbin))] = gasusage_national(t) * topfuelbinwidth ; - -*comment from heritage reeds: -*gasbinqq is the centerpoint of each of the smaller bins and is used to determine the price of each bin. The first and last bin have -*gasbinqqs that are just one more step before and after the smaller bins. -gasbinqq_regional(fuelbin,cendiv,t) = - gassupply_ele(cendiv,t) * (normfuelbinmin - + (ord(fuelbin) - 1)*normfuelbinwidth - normfuelbinwidth / 2) ; - -gasbinqq_national(fuelbin,t) = gasusage_national(t) * (normfuelbinmin + (ord(fuelbin) - 1)*normfuelbinwidth - normfuelbinwidth / 2) ; - -*bins' prices are those from the supply curves -*1e9 converts from MMBtu to Quads -gasbinp_regional(fuelbin,cendiv,t) = - round((cd_beta(cendiv,t) * (gasbinqq_regional(fuelbin,cendiv,t) - gassupply_ele(cendiv,t))) / 1e9,5) ; - -gasbinp_national(fuelbin,t)= round(nat_beta(t)*(gasbinqq_national(fuelbin,t) - gasusage_national(t)) / 1e9,5) ; - - -*this is the reference price of gas given last year's gas usage levels -gasmultterm(cendiv,t) = (cd_alpha(t,cendiv) - + nat_beta(t) * gasusage_national(t-2) / 1e9 - + cd_beta(cendiv,t) * gassupply_ele(cendiv,t-2) / 1e9 - ) ; - - - -*================================= -* ---- Storage ---- -*================================= - -* --- Storage Efficiency --- - -parameter storage_eff(i,t) "--fraction-- round-trip efficiency of storage technologies" ; - -storage_eff(i,t)$storage(i) = 1 ; -storage_eff(i,t)$psh(i) = storage_eff_psh ; -storage_eff(i,t)$[storage(i)$plant_char0(i,t,'rte')] = plant_char0(i,t,'rte') ; -storage_eff(i,t)$[evmc_storage(i)$plant_char0(i,t,'rte')] = plant_char0(i,t,'rte') ; -storage_eff(i,t)$pvb(i) = storage_eff("battery_li",t) ; - -parameter storage_eff_pvb_p(i,t) "--fraction-- efficiency of hybrid PV+battery when charging from the coupled PV" - storage_eff_pvb_g(i,t) "--fraction-- efficiency of hybrid PV+battery when charging from the grid" ; - -*when charging from PV the pvb system will have a higher efficiency due to one less inverter conversion -storage_eff_pvb_p(i,t)$pvb(i) = storage_eff(i,t) / inverter_efficiency ; -*when charging from the grid the efficiency will be the same as standalone storage -storage_eff_pvb_g(i,t)$pvb(i) = storage_eff("battery_li",t) ; - -*upgrade plants assume the same as what theyre upgraded to -storage_eff(i,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), storage_eff(ii,t) } ; - -* --- Storage Input Capacity --- - -parameter minstorfrac(i,v,r) "--fraction-- minimum storage_in as a fraction of total input capacity"; -minstorfrac(i,v,r)$[valcap_ivr(i,v,r)$psh(i)] = %GSw_HydroStorInMinLoad% ; -* Expand for water technologies -minstorfrac(i,v,r)$[i_water_cooling(i)$valcap_ivr(i,v,r)$psh(i)$Sw_WaterMain] - = sum{ii$ctt_i_ii(i,ii), minstorfrac(ii,v,r) } ; - -parameter storinmaxfrac(i,v,r) "--fraction-- max storage input capacity as a fraction of output capacity" ; - -$ifthen.storcap %GSw_HydroStorInMaxFrac% == "data" -$onempty -parameter storinmaxfrac_data(i,v,r) "--fraction-- data for max storage input capacity as a fraction of capacity if data is available" -/ -$offlisting -$ondelim -$ifthen.readstorinmaxfrac %GSw_Storage% == 1 -$include inputs_case%ds%storinmaxfrac.csv -$endif.readstorinmaxfrac -$offdelim -$onlisting -/ ; -$offempty -* Use data file for available PSH data -storinmaxfrac(i,v,r)$[valcap_ivr(i,v,r)$psh(i)] = storinmaxfrac_data(i,v,r) ; -$else.storcap -* Use numerical value from case file for PSH only -storinmaxfrac(i,v,r)$[valcap_ivr(i,v,r)$psh(i)] = %GSw_HydroStorInMaxFrac% ; -$endif.storcap -* Fill any gaps with values of 1 -storinmaxfrac(i,v,r)$[(storage_standalone(i) or hyd_add_pump(i))$(not storinmaxfrac(i,v,r))$valcap_ivr(i,v,r)] = 1 ; - -* --- Hybrid PV+Battery --- - -table pvbcapmult(allt,pvb_config) "PV+Battery capital cost multipliers over time" -$offlisting -$ondelim -$include inputs_case%ds%pvbcapcostmult.csv -$offdelim -$onlisting -; - -* the capital cost for PVB includes both the PV and battery portions -* total cost = cost(PV) * cap(PV) + cost(B) * cap(B) -* = cost(PV) * cap(PV) + cost(B) * bcr * cap(PV) -* = [cost(PV) + cost(B) * bcr ] * cap(PV) -cost_cap(i,t)$pvb(i) = (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) * sum{pvb_config$pvb_agg(pvb_config,i), pvbcapmult(t,pvb_config) } ; - -scalar pvb_itc_qual_frac "--fraction-- fraction of energy that must be charged from local PV for hybrid PV+battery" ; -pvb_itc_qual_frac = %GSw_PVB_Charge_Constraint% ; - -* --- CSP with storage --- - -* used in eq_rsc_INVlim -* csp: this include the SM for representative configurations, divided by the representative SM (2.4) for CSP supply curve; -* all other technologies are 1 -parameter - csp_sm(i) "--unitless-- solar multiple for configurations" - resourcescaler(i) "--unitless-- resource scaler for rsc technologies" -; - -csp_sm(i)$csp1(i) = csp_sm_1 ; -csp_sm(i)$csp2(i) = csp_sm_2 ; -csp_sm(i)$csp3(i) = csp_sm_3 ; -csp_sm(i)$csp4(i) = csp_sm_4 ; - -resourcescaler(i)$[(not CSP_Storage(i))$(not ban(i))] = 1 ; -resourcescaler(i)$csp(i) = CSP_SM(i) / csp_sm_baseline ; - -* --- Storage Duration --- - -* For PSH, tech-specific storage duration sets a default value. -* Then when when GSw_HydroPSHDurData = 1, -* region- and vintage-specific durations are defined where data exists. -parameter storage_duration(i) "--hours-- storage duration by tech" -/ -$offlisting -$ondelim -$include inputs_case%ds%storage_duration.csv -$offdelim -$onlisting -/ ; - -$onempty -scalar psh_sc_duration "--hours-- PSH storage duration corresponding to selected supply curve" -/ -$offlisting -$ifthene.readpshscduration %GSw_Storage%<>0 -$include inputs_case%ds%psh_sc_duration.csv -$endif.readpshscduration -$onlisting -/ ; -$offempty - -* Note that this PSH duration overwrites what is contained in storage_duration.csv -storage_duration(i)$psh(i) = psh_sc_duration ; - -storage_duration(i)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), storage_duration(ii) } ; - -storage_duration(i)$pvb(i) = %GSw_PVB_Dur% ; - -*upgrade plants assume the same as what they're upgraded to -storage_duration(i)$upgrade(i) = sum{ii$upgrade_to(i,ii),storage_duration(ii) } ; - -parameter storage_duration_m(i,v,r) "--hours-- storage duration by tech, vintage, and region" - cc_storage(i,sdbin) "--fraction-- capacity credit of storage by duration" - bin_duration(sdbin) "--hours-- duration of each storage duration bin" - bin_penalty(sdbin) "--$-- penalty to incentivize solve to fill the shorter duration bins first" -; -$onempty -parameter storage_duration_pshdata(i,v,r) "--hours-- storage duration data for PSH" -/ -$offlisting -$ondelim -$ifthene.readpshstorageduration ((%GSw_Storage%=1)and(%GSw_HydroPSHDurData%=1)) -$include inputs_case%ds%storage_duration_pshdata.csv -$endif.readpshstorageduration -$offdelim -$onlisting -/ ; -$offempty - -* Initialize using generic tech-specific duration -storage_duration_m(i,v,r)$[storage_duration(i)$valcap_ivr(i,v,r)] = storage_duration(i) ; -* Overwrite storage duration for existing PSH capacity when using datafile -$ifthen %GSw_HydroPSHDurData% == 1 -storage_duration_m(i,v,r)$[storage_duration_pshdata(i,v,r)$psh(i)$valcap_ivr(i,v,r)] = storage_duration_pshdata(i,v,r) ; -$endif - -* set the duration of each storage duration bin -bin_duration(sdbin) = sdbin.val ; - -* set the capacity credit of each storage technology for each storage duration bin. -* for example, 2-hour batteries get CC=1 for the 2-hour bin and CC=0.5 for the 4-hour bin -* likewise, 6-hour batteries get CC=1 for the 2-, 4-, and 6-hour bins, but only 0.75 for the 8-hour bin, etc. -* For capacity credit, CSP is treated like VRE rather than storage -cc_storage(i,sdbin)$[(not ban(i))$(not csp(i))] = storage_duration(i) / bin_duration(sdbin) ; -cc_storage(i,sdbin)$(cc_storage(i,sdbin) > 1) = 1 ; - -* for battery, the capacity credit for each bin is always 1, -* since the duration of continuous battery will be automatically greater than the sdbin duration. -cc_storage(i,sdbin)$(battery(i)) = 1 ; - -* The 8760 bin is included as a safety valve so that the model can build additional storage -* beyond what is available for diurnal peaking capacity -cc_storage(i,'8760') = 0 ; - -bin_penalty(sdbin) = 0 ; -bin_penalty(sdbin)$Sw_StorageBinPenalty = 1e-5 * (ord(sdbin) - 1) ; - -*upgrade plants assume the same as what they're upgraded to -cc_storage(i,sdbin)$upgrade(i) = sum{ii$upgrade_to(i,ii), cc_storage(ii,sdbin) } ; - -* --- storage fixed OM cost --- - -*fom and vom costs are constant for pumped-hydro -*values are taken from ATB -cost_fom(i,v,r,t)$[psh(i)$valcap(i,v,r,t)] = cost_fom_psh ; -cost_vom(i,v,r,t)$[psh(i)$valcap(i,v,r,t)] = cost_vom_psh ; - -* Apply a minimum VOM cost for storage (to avoid degeneracy with curtailment) -* Only apply the value to storage that does not have a VOM value -cost_vom(i,v,r,t)$[storage(i)$valgen(i,v,r,t)$(not cost_vom(i,v,r,t))] = storage_vom_min ; - -* --- minimum capacity factor ---- -parameter minCF(i,t) "--fraction-- minimum annual capacity factor for each tech fleet, applied to (i,r)" - maxdailycf(i,t) "--fraction-- maximum daily capacity factor" ; - -* 6% for H2-CT and H2-CC is based on unpublished PLEXOS runs of 100% RE scenarios performed in summer 2019 -parameter minCF_input(i) "--fraction-- minimum annual capacity factor for each tech fleet, applied to (i,r)" -/ -$offlisting -$ondelim -$include inputs_case%ds%minCF.csv -$offdelim -$onlisting -/ ; -minCF(i,t) = minCF_input(i) ; -minCF(i,t)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), minCF(ii,t) } ; -minCF(i,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), minCF(ii,t) } ; - -* adjust fleet mincf for nuclear when using flexible nuclear -minCF(i,t)$[nuclear(i)$Sw_NukeFlex] = minCF_nuclear_flex ; - -parameter maxdailycf_input(i) "--fraction-- maximum daily capacity factor for a technology" -/ -$offlisting -$ondelim -$include inputs_case%ds%maxdailycf.csv -$offdelim -$onlisting -/ ; - -maxdailycf(i,t) = maxdailycf_input(i) ; -maxdailycf(i,t)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), maxdailycf(ii,t) } ; -maxdailycf(i,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), maxdailycf(ii,t) } ; - -*================================= -* ---- Upgrades ---- -*================================= -*The last instance of cost_cap has already occurred, so now assign upgrade costs - -*costs for upgrading are the difference in capital costs -*between the initial techs and the tech to which the unit is upgraded -cost_upgrade(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)] = sum{ii$upgrade_to(i,ii), cost_cap(ii,t) } - - sum{ii$upgrade_from(i,ii), cost_cap(ii,t) } ; - -*increase cost_upgrade by 1% to prevent building and upgrading in the same year -*(otherwise there is a degeneracy between building new and building+upgrading in the same year) -cost_upgrade(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)] = cost_upgrade(i,v,r,t) * 1.01 ; - -*Sets upgrade costs for H2-CT and H2-CC plants based relative to capital cost for H2-CT -*this is done because upgrade costs are higher than new build costs -cost_upgrade('Gas-CT_H2-CT',v,r,t)$[valcap('Gas-CT_H2-CT',v,r,t)] = - cost_cap('gas-ct',t) * cost_upgrade_gasct2h2ct ; - -*(The set of filters on cost_upgrades yields "Gas-CC_H2-CC", but does so in a way to capture -*water techs when the water switch is turned on) -cost_upgrade(i,v,r,t)$[h2_combustion(i)$upgrade(i)$(not ccs(i))$valcap(i,v,r,t) - $sum{ii$gas_cc(ii), upgrade_from(i,ii) }] = - cost_cap('gas-cc',t) * cost_upgrade_gascc2h2cc ; - -*Override any upgrade costs computed above with exogenously specified retrofit costs -cost_upgrade(i,v,r,t)$[upgrade(i)$plant_char0(i,t,"upgradecost")$valcap(i,v,r,t)] - = plant_char0(i,t,"upgradecost") ; - -*the coal-CCS input from ATB 2021 and on is for a pulverized coal plant -*assume that the upgrade cost for coal-IGCC_coal-CCS is the same as for -*coal-new_coal-CCS -cost_upgrade('coal-IGCC_coal-CCS_mod',v,r,t)$valcap('coal-IGCC_coal-CCS_mod',v,r,t) = - cost_upgrade('coal-new_coal-CCS_mod',v,r,t) ; - -cost_upgrade('coal-IGCC_coal-CCS_max',v,r,t)$valcap('coal-IGCC_coal-CCS_max',v,r,t) = - cost_upgrade('coal-new_coal-CCS_max',v,r,t) ; - -* Assign upgrade costs for hydro technology upgrades using values from cases file -cost_upgrade('hydEND_hydED',v,r,t)$valcap('hydEND_hydED',v,r,t) = %GSw_HydroCostAddDispatch% ; -cost_upgrade('hydED_pumped-hydro',v,r,t)$valcap('hydED_pumped-hydro',v,r,t) = %GSw_HydroCostAddPump% ; -cost_upgrade('hydED_pumped-hydro-flex',v,r,t)$valcap('hydED_pumped-hydro-flex',v,r,t) = %GSw_HydroCostAddPump% ; - -parameter ccs_upgrade_costs_coal(allt) "--$2004/kW-- CCS upgrade costs for coal techs" -/ -$offlisting -$ondelim -$include inputs_case/upgrade_costs_ccs_coal.csv -$offdelim -$onlisting -/ ; - -parameter ccs_upgrade_costs_gas(allt) "--$2004/kW-- CCS upgrade costs for gas techs" -/ -$offlisting -$ondelim -$include inputs_case/upgrade_costs_ccs_gas.csv -$offdelim -$onlisting -/ ; - -* update ccs retrofit costs with conversion from kw to mw -* based on selected ccs upgrade cost case -cost_upgrade(i,v,r,t)$[upgrade(i)$coal_ccs(i)$valcap(i,v,r,t)] = 1e3 * ccs_upgrade_costs_coal(t) ; -cost_upgrade(i,v,r,t)$[upgrade(i)$gas_cc_ccs(i)$valcap(i,v,r,t)] = 1e3 * ccs_upgrade_costs_gas(t) ; - -* if specified, use the overnight retrofit -* costs specified in the EIA unit database -cost_upgrade(i,v,r,t)$[initv(v)$hintage_data(i,v,r,t,"wCCS_Retro_OvernightCost")$valcap(i,v,r,t)] = -* conversion from $ / kw to $ / mw - upgrade_inflator * 1e3 * hintage_data(i,v,r,t,"wCCS_Retro_OvernightCost") ; - -* set floor on the cost of an upgrade to prevent negative upgrade costs -cost_upgrade(i,v,r,t)$[valcap(i,v,r,t)$upgrade(i)] = max{0, cost_upgrade(i,v,r,t) } ; - - -*============================================================= -* ---------- Cost Adjustment for cost_upgrade Techs -*============================================================= -table upgrade_mult(i,allt) "--fraction-- cost adjustment for cost_upgrade techs" -$offlisting -$ondelim -$include inputs_case%ds%upgrade_mult_final.csv -$offdelim -$onlisting -; - -upgrade_mult(i,t)$[sum{ii$ctt_i_ii(i,ii), upgrade_mult(ii,t) }$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), upgrade_mult(ii,t) } ; - -cost_upgrade(i,v,r,t)$[initv(v)$valcap(i,v,r,t)$sum{ii$upgrade_from(i,ii),cost_upgrade(ii,v,r,t) }$unitspec_upgrades(i)$(not Sw_UpgradeATBCosts)] = - upgrade_mult(i,t) * sum{ii$upgrade_from(i,ii),cost_upgrade(ii,v,r,t) } ; - -* start with specifying upgrade_derate as zero -upgrade_derate(i,v,r,t) = 0 ; - -upgrade_derate(i,initv,r,t)$[upgrade(i)$ccs(i)$unitspec_upgrades(i)$valcap(i,initv,r,t) - $sum{ii$upgrade_from(i,ii),hintage_data(ii,initv,r,t,"wCCS_Retro_HR") }] = -* following calculation is from NEMS/EIA - stating the derate is 1 - [the original heat_rate] / [new heat rate] -* take the max of it and zero - max(0,1 - sum{ii$upgrade_from(i,ii), hintage_data(ii,initv,r,t,"wHR") / hintage_data(ii,initv,r,t,"wCCS_Retro_HR") }); - -* set upgrade derate for new plants and existing plants without data -* to the average across all values from NETL CCRD: -* https://www.osti.gov/servlets/purl/1887588 -upgrade_derate(i,initv,r,t)$[upgrade(i)$ccs(i)$coal(i) - $(not upgrade_derate(i,initv,r,t)) - $valcap(i,initv,r,t)] = 0.29 ; - -upgrade_derate(i,initv,r,t)$[upgrade(i)$ccs(i)$gas(i) - $(not upgrade_derate(i,initv,r,t)) - $valcap(i,initv,r,t)] = 0.14 ; - -* same assumptions for new plants -upgrade_derate(i,newv,r,t)$[upgrade(i)$ccs(i)$coal(i)$valcap(i,newv,r,t)] = 0.29 ; -upgrade_derate(i,newv,r,t)$[upgrade(i)$ccs(i)$gas(i)$valcap(i,newv,r,t)] = 0.14 ; - -* If a technology is not retired, its upgrade_derate value is zero. -upgrade_derate(i,v,r,t)$[upgrade(i)$(not noret_upgrade_tech(i))] = 0 ; - -if((not Sw_UpgradeDerate), - upgrade_derate(i,v,r,t) = 0 -) ; - - -*============================== -* --- BIOMASS SUPPLY CURVES --- -*============================== - -* supply curves defined by 21 price increments -set bioclass -/ -$offlisting -$include inputs_case%ds%bioclass.csv -$onlisting -/ ; - -set biofeas(r) "regions with biomass supply and biopower"; - -* supply curve derived from 2016 ORNL Billion Ton study -* annual supply of woody biomass available to the power sector (in million dry tons) -* by USDA region at price P (2015$ per dry ton) -table biosupply(usda_region,bioclass,*) "biomass supply (million dry tons) and biomass cost ($/dry ton)" -$offlisting -$ondelim -$include inputs_case%ds%bio_supplycurve.csv -$offdelim -$onlisting -; - -* convert biomass supply from million dry tons to MMBtu -* assuming 13 MMBtu per dry ton based on 2016 ORNL Billion Ton Study -scalar bio_energy_content "MMBtu per dry ton of biomass" / 13 / ; -biosupply(usda_region,bioclass,"cap") = biosupply(usda_region, bioclass,"cap") * 1E6 * bio_energy_content ; - -* multiplier for total biomass supply, set by user via input switch (default is 1) -biosupply(usda_region,bioclass,"cap") = biosupply(usda_region,bioclass,"cap") * Sw_BioSupply ; - -* convert price into $ per MMBtu -* input price ($/ton) / (MMBtu/ton) = $/MMBtu -biosupply(usda_region,bioclass,"price") = biosupply(usda_region, bioclass,"price") / bio_energy_content ; - -* regions with biomass supply -biofeas(r)$[sum{bioclass, sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"cap") } } ] = yes ; - -*removal of bio techs that are not in biofeas(r) -valcap(i,v,r,t)$[(cofire(i) or bio(i))$(not biofeas(r))] = no ; -valgen(i,v,r,t)$[(cofire(i) or bio(i))$(not biofeas(r))] = no ; -valinv(i,v,r,t)$[(cofire(i) or bio(i))$(not biofeas(r))] = no ; - -valgen(i,v,r,t)$[not valcap(i,v,r,t)] = no ; -valinv(i,v,r,t)$[not valcap(i,v,r,t)] = no ; - -scalar bio_transport_cost ; -* biomass transport cost enter in $ per ton, convert to $ per MMBtu -bio_transport_cost = Sw_BioTransportCost / bio_energy_content ; - -* get price of cheapest supply curve bin that has resources (needed for Augur) -* price includes any transport costs for biomass -parameter rep_bio_price_unused(r) "--2004$/MWh-- marginal price of lowest cost available supply curve bin for biofuel" ; -rep_bio_price_unused(r)$[sum{usda_region, 1$r_usda(r,usda_region) }] = - smin{bioclass$[sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"cap") }], - sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"price") } } + bio_transport_cost ; - -parameter cost_curt(t) "--$/MWh-- price paid for curtailed VRE" ; - -cost_curt(t)$[yeart(t)>=model_builds_start_yr] = Sw_CurtMarket ; - -*====================== -* Emissions cap and tax -*====================== - -parameter emit_cap(eall,t) "--metric tons per year-- annual CO2 emissions cap", - yearweight(t) "--unitless-- weights applied to each solve year for the banking and borrowing cap - updated in d_solveprep.gms", - emit_tax(e,r,t) "--$ per metric ton-- tax applied to emissions" ; - -emit_cap(e,t) = 0 ; -emit_tax(e,r,t) = 0 ; - -yearweight(t) = 0 ; -yearweight(t)$tmodel_new(t) = sum{tt$tprev(tt,t), yeart(tt) } - yeart(t) ; -yearweight(t)$tlast(t) = 1 + smax{yearafter, yearafter.val } ; - -* declared over allt to allow for external data files that extend beyond end_year -parameter co2_cap(allt) "--metric tons-- CO2 emissions cap used when Sw_AnnualCap is on" -/ -$offlisting -$ondelim -$include inputs_case%ds%co2_cap.csv -$offdelim -$onlisting -/ ; -if(Sw_AnnualCap = 1, - emit_cap("CO2",t) = co2_cap(t) ; -) ; - -if(Sw_AnnualCap > 1, - emit_cap("CO2e",t) = co2_cap(t) ; -) ; - -parameter co2_tax(allt) "--$/metric ton-- CO2 tax used when Sw_CarbTax is on" -/ -$offlisting -$ondelim -$include inputs_case%ds%co2_tax.csv -$offdelim -$onlisting -/ ; - - -* set the carbon tax based on switch arguments -if(Sw_CarbTax = 1, -emit_tax("CO2",r,t) = co2_tax(t) ; -) ; - -* All emissions are included in reported, but not all emissions need to be modeled -* This set captures only the emissions that need to be included in the model -set emit_modeled(e,r,t) "set of emissions that are necessary to include in the model" ; - -* CO2 for RGGI regions and years -emit_modeled("CO2",r,t)$[ - (yeart(t)>=RGGI_start_yr) - $RGGI_r(r) - $Sw_RGGI] = yes ; - -* CO2 for state cap regions and years -emit_modeled("CO2",r,t)$[ - (yeart(t)>=state_cap_start_yr) - $sum{(tt,st)$r_st(r,st), state_cap(st,tt) } - $Sw_StateCap] = yes ; - -* Emissions with an emission rate limit constraint -emit_modeled(e,r,t)$emit_rate_con(e,r,t) = yes ; - -* Model all emissions with global warming potential -emit_modeled(e,r,t)$gwp(e) = yes ; - -* Emissions associated with bankbarrowcap -emit_modeled(e,r,t)$[ - Sw_BankBorrowCap - $sum{tt, emit_cap(e,tt) }] = yes ; - -* Emissions subject to an emissions tax -emit_modeled(e,r,t)$emit_tax(e,r,t) = yes ; - -* Remove years not modeled -emit_modeled(e,r,t)$[not tmodel_new(t)] = no ; - -* Set of emissions that are being capped -set emit_capped(e) "set of emissions that are included in emission cap depending on Sw_AnnualCap setting" ; -emit_capped(e)$[Sw_AnnualCap=0] = no ; -emit_capped("CO2")$[Sw_AnnualCap=1] = yes ; -emit_capped(e)$[gwp(e)$(not sameas(e, "H2"))$(Sw_AnnualCap=2)] = yes ; -emit_capped(e)$[gwp(e)$(Sw_AnnualCap=3)] = yes ; - -*==================================== -* --- Endogenous Retirements --- -*==================================== - -set valret(i,v) "technologies and classes that can be retired" ; - -set noretire(i) "technologies that will never be retired" -/ -$offlisting -$ondelim -$include inputs_case%ds%noretire.csv -$offdelim -$onlisting -/ ; - -* storage technologies are not appropriately attributing capacity value to CAP variable -* therefore not allowing them to endogenously retire -noretire(i)$[(storage_standalone(i) or hyd_add_pump(i))] = yes ; - -*all existings plants of any technology can be retired if Sw_Retire = 1 -valret(i,v)$[(Sw_Retire=1)$initv(v)$(not noretire(i))] = yes ; - -*only existing coal and gas are retirable if Sw_Retire = 2 -valret(i,v)$[(Sw_Retire=2)$initv(v)$(not noretire(i)) - $(coal(i) or gas(i) or ogs(i))] = yes ; - -*All new and existing nuclear, coal, gas, and hydrogen are retirable if Sw_Retire = 3 -*Existing plants have to meet the min_retire_age before retiring -valret(i,v)$[((Sw_Retire=3) or (Sw_Retire=5))$(not noretire(i)) - $(coal(i) or gas(i) or nuclear(i) or ogs(i) or h2_combustion(i) or h2(i))] = yes ; - -*new and existings plants of any technology can be retired if Sw_Retire = 4 -valret(i,v)$[(Sw_Retire=4)$(not noretire(i))] = yes ; - -retiretech(i,v,r,t)$[valret(i,v)$valcap(i,v,r,t)] = yes ; - -* when Sw_Retire = 3 ensure that plants do not retire before their minimum age -retiretech(i,v,r,t)$[((Sw_Retire=3) or (Sw_Retire=5))$initv(v)$(not noretire(i))$(plant_age(i,v,r,t) <= min_retire_age(i)) - $(coal(i) or gas(i) or nuclear(i) or ogs(i) or h2_combustion(i) or h2(i))] = no ; - -* for sw_retire=5, don't allow nuclear to retire until 2030 -retiretech(i,v,r,t)$[(Sw_Retire=5)$nuclear(i)$(yeart(t)<=2030)] = no ; - -*several states have subsidies for nuclear power, so do not allow nuclear to retire in these states -*before the year specified (see https://www.eia.gov/todayinenergy/detail.php?id=41534) -*Note that Ohio has since repealed their nuclear subsidy, so is no longer included -$onempty -parameter nuclear_subsidies(st) '--year-- the year a nuclear subsidy ends in a given state' -/ -$offlisting -$ondelim -$include inputs_case%ds%nuclear_subsidies.csv -$offdelim -$onlisting -/ -; -$offempty - -retiretech(i,initv,r,t)$[(yeart(t) < sum{st$r_st(r,st), nuclear_subsidies(st) })$valcap(i,initv,r,t)$nuclear(i)] = no ; - -* if Sw_NukeNoRetire is enabled, don't allow nuclear to retire through Sw_NukeNoRetireYear -if(Sw_NukeNoRetire = 1, - retiretech(i,v,r,t)$[nuclear(i)$(yeart(t)<=Sw_NukeNoRetireYear)] = no ; -) ; - - -*Do not allow retirements before they are allowed -retiretech(i,v,r,t)$[(yeart(t)=Sw_Upgradeyear)$(yeart(t)>=Sw_Retireyear)$(Sw_Upgrades = 2) - $sum{ii$[upgrade_from(ii,i)$valcap(ii,v,r,t)], 1 }] = yes ; - -*========================================= -* BEGIN MODEL SPECIFIC PARAMETER CREATION -*========================================= - -parameter m_rsc_dat(r,i,rscbin,sc_cat) "--MW or $/MW-- resource supply curve attributes" ; - -m_rsc_dat(r,i,rscbin,sc_cat) - $[sum{(ii,t)$[rsc_agg(i,ii)$tmodel_new(t)], valcap_irt(ii,r,t) }] - = rsc_dat(i,r,sc_cat,rscbin) ; - -parameter m_rsc_dat_original(r,i,rscbin,sc_cat) "--MW or $/MW-- resource supply curve attributes before any adjustments" ; -*m_rsc_dat_original is used to compare the magnitude of possible adjustments in supply curves. -*It is only used for model validation and debugging purposes. -m_rsc_dat_original(r,i,rscbin,sc_cat) = m_rsc_dat(r,i,rscbin,sc_cat) ; - -*========================================= -* Reduced Resource Switch -*========================================= - -parameter rsc_reduct_frac(pcat,r) "--unitless-- fraction of renewable resource that is reduced from the supply curve" - prescrip_rsc_frac(pcat,r) "--unitless-- fraction of prescribed builds to the resource available" - rsc_capacity_scalar(i,r,t) "--unitless-- resource scalar for any technology that has a change in the supply curve capacity over time" -; - -set rsc_capacity_scalar_i(i) "technologies that have a capacity resource scalar" ; - -rsc_reduct_frac(pcat,r) = 0 ; -prescrip_rsc_frac(pcat,r) = 0 ; -rsc_capacity_scalar(i,r,t) = 0 ; -rsc_capacity_scalar_i(i) = no ; - -* if the Sw_ReducedResource is on, reduce the available resource by reduced_resource_frac -if (Sw_ReducedResource = 1, -*Calculate the fraction of prescribed builds to the available resource -* 2021-05-05 the prescriptions are being applied across all years until we decide a better way to do this - prescrip_rsc_frac(pcat,r)$[sum{(i,rscbin)$prescriptivelink(pcat,i), m_rsc_dat(r,i,rscbin,"cap") } > 0] = - smax(tt,m_required_prescriptions(pcat,r,tt)) / sum{(i,rscbin)$prescriptivelink(pcat,i), m_rsc_dat(r,i,rscbin,"cap") } ; -*Set the default resource reduction fraction - rsc_reduct_frac(pcat,r) = reduced_resource_frac ; -*If the resource reduction fraction will reduce the resource to the point that prescribed builds will be infeasible, -*then replace the resource reduction fraction with the maximum that the resource can be reduced to still have a feasible solution - rsc_reduct_frac(pcat,r)$[prescrip_rsc_frac(pcat,r) > (1 - rsc_reduct_frac(pcat,r))] = 1 - prescrip_rsc_frac(pcat,r) ; - -*In order to avoid small number issues, round down at the 3rd decimal place -*Because the floor function returns an integer, we multiply and divide by 1000 to get proper rounding - rsc_reduct_frac(pcat,r) = rsc_reduct_frac(pcat,r) * 1000 ; - rsc_reduct_frac(pcat,r) = floor(rsc_reduct_frac(pcat,r)) ; - rsc_reduct_frac(pcat,r) = rsc_reduct_frac(pcat,r) / 1000 ; - -*Now reduce the resource by the updated resource reduction fraction -*(only do this for hydro, geothermal, PSH, and CSP; PV and wind have limited resource supply curves) - m_rsc_dat(r,i,rscbin,"cap")$[rsc_i(i)$(csp(i) or hydro(i) or psh(i) or geo(i))] = - m_rsc_dat(r,i,rscbin,"cap") * (1 - sum{pcat$prescriptivelink(pcat,i), rsc_reduct_frac(pcat,r) }) ; -) ; - -*Currently only geothermal and dr_shed have supply curve capacities that change over time -rsc_capacity_scalar(i,r,t) = geo_discovery(i,r,t) + dr_shed_capacity_scalar(i,r,t) ; -rsc_capacity_scalar_i(i)$[sum{(r,t), rsc_capacity_scalar(i,r,t) }] = yes ; - -*convert UPV and PVB interconnection costs from $/MW-AC to $/MW-DC using ILR -m_rsc_dat(r,i,rscbin,"cost")$[m_rsc_dat(r,i,rscbin,"cap")$(upv(i) or pvb(i))] = m_rsc_dat(r,i,rscbin,"cost") / ilr(i) ; - -*Fill in cost_trans for outputs. -m_rsc_dat(r,i,rscbin,"cost_trans")$[m_rsc_dat(r,i,rscbin,"cost")$[not sccapcosttech(i)]] = - m_rsc_dat(r,i,rscbin,"cost") - m_rsc_dat(r,i,rscbin,"cost_cap") ; - -*Ensure sufficient resource is available to cover existing capacity rsc_i capacity -m_rsc_dat(r,i,rscbin,"cap")$[rsc_i(i) - $(m_rsc_dat(r,i,rscbin,"cap") * (1$[not rsc_capacity_scalar_i(i)] + sum{t$tfirst(t), rsc_capacity_scalar(i,r,t) }$rsc_capacity_scalar_i(i)) - < sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], capacity_exog_rsc(ii,v,r,rscbin,tt) })] = -*Use ceiling function to three decimal places so that we don't run into infeasibilities due to rounding later on - ceil(1000 * sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], capacity_exog_rsc(ii,v,r,rscbin,tt) - / (1$[not rsc_capacity_scalar_i(ii)] + rsc_capacity_scalar(ii,r,tt)$rsc_capacity_scalar_i(ii)) } ) / 1000 ; - -*Ensure sufficient resource availability to cover prescribed builds -*while considering existing capacity (capacity_exog_rsc) -*and prescribed capacity (noncumulative_prescriptions). - -*Two types of adjustments: -*1- If at least one element of m_rsc_dat(r,i,rscbin,"cap") is nonzero within a technology group (pcat), -* apply a multiplier to all associated i-classes so that the total available capacity -* meets or exceeds prescribed capacity. -*2- If m_rsc_dat(r,i,rscbin,"cap") is zero for all i-classes within the technology group, -* but prescribed capacity exists, assign prescribed capacity to the first bin at zero cost. - -*Define auxiliary parameters to organize the computation -parameter cap_existing(i,r) "--MW-- amount of existing resource supply curve (rsc) capacity in each region" - cap_prescribed(i,r) "--MW-- amount of prescribed (required builds) rsc capacity in each region" - available_supply(i,r) "--MW-- amount of available rsc supply in each region" -; - -*Initialize the available supply to zero -available_supply(i,r) = 0 ; - -*Get existing capacity -cap_existing(i,r)$exog_rsc(i) = sum{(v,t,rscbin)$[tfirst(t)], capacity_exog_rsc(i,v,r,rscbin,t) } ; - -*Get prescribed capacity -cap_prescribed(i,r)$rsc_i(i) = sum{(pcat,t)$[(sameas(pcat,i) or prescriptivelink(pcat,i)) - $tmodel_new(t)], - noncumulative_prescriptions(pcat,r,t) } ; - -*Loop over all regions -loop(r, -*Loop over non-geothermal rsc technologies - loop(i$[rsc_i(i)$sum{(v,t)$newv(v), valcap(i,v,r,t) }$(not prescriptivelink("geothermal",i))], - -*Get total available supply for all ii associated with pcat of i. -*For example, if i = {upv_2}, then ii = {upv_2, upv_3, ...} and pcat = {UPV}. - available_supply(i,r) = sum{(pcat,ii,rscbin)$[prescriptivelink(pcat,i) - $prescriptivelink(pcat,ii)], - m_rsc_dat(r,ii,rscbin,"cap") } ; - -*Apply multiplier if prescribed capacity exceeds available supply - if ([((cap_existing(i,r) + cap_prescribed(i,r)) > available_supply(i,r))$(available_supply(i,r))], - m_rsc_dat(r,ii,rscbin,"cap")$[sum{pcat$(prescriptivelink(pcat,i)$prescriptivelink(pcat,ii)), 1 }] - = m_rsc_dat(r,ii,rscbin,"cap") * ((cap_existing(i,r) + cap_prescribed(i,r)) / available_supply(i,r)) ; - ) ; - -*Assign prescribed capacity to first bin at no cost if no supply is available - if ([(cap_prescribed(i,r) > 0)$(not available_supply(i,r))] , - m_rsc_dat(r,i,"bin1","cap") = cap_prescribed(i,r) ; - ) ; - ) ; -) ; - -*Compute the difference between m_rsc_dat_original and m_rsc_dat -parameter rsc_cap_diff(r,i,rscbin) "--MW or $/MW-- total supply added to m_rsc_dat to adjust for prescriptions" ; -rsc_cap_diff(r,i,rscbin) = m_rsc_dat(r,i,rscbin,"cap") - m_rsc_dat_original(r,i,rscbin,"cap") ; - -*Round up to the nearest 3rd decimal place -m_rsc_dat(r,i,rscbin,"cap")$m_rsc_dat(r,i,rscbin,"cap") = ceil(m_rsc_dat(r,i,rscbin,"cap") * 1000) / 1000 ; - -*Geothermal is not a tech with sameas(i,pcat), so handle it separately here -*Loop over regions that have geothermal prescribed builds -loop(r$sum{(i,t)$[prescriptivelink("geothermal",i)$tmodel_new(t)], noncumulative_prescriptions("geothermal",r,t) }, -*Then loop over eligible geothermal technologies - loop(i$[prescriptivelink("geothermal",i)$sum{(v,t)$newv(v), valcap(i,v,r,t) }$geo_discovery(i,r,"%startyear%")], -*If capacity is insufficient, add enough capacity to make the model feasible -*Use the 2010 geothermal discovery (geo_discovery) rate for the calculation. That will slightly -*overestimate geothermal resource for any prescribed builds happening after the discovery rate -*begins to increase (currently after 2021) - m_rsc_dat(r,i,"bin1","cap")$[((sum{(rscbin), m_rsc_dat(r,i,rscbin,"cap") } * (1$[not geo_hydro(i)] + geo_discovery(i,r,"%startyear%")$geo_hydro(i))) < sum{t$tmodel_new(t), noncumulative_prescriptions("geothermal",r,t) }) - $(1$[not geo_hydro(i)] + geo_discovery(i,r,"%startyear%")$geo_hydro(i))] = - (sum{t$tmodel_new(t), noncumulative_prescriptions("geothermal",r,t) } - - sum{(rscbin), m_rsc_dat(r,i,rscbin,"cap") } - + m_rsc_dat(r,i,"bin1","cap") - ) / (1$[not geo_hydro(i)] + geo_discovery(i,r,"%startyear%")$geo_hydro(i)) ; - break ; - ) ; -) ; - -* * Apply spur-line cost multiplier for relevant technologies -* m_rsc_dat(r,i,rscbin,"cost")$(pv(i) or pvb(i) or wind(i) or csp(i)) = -* m_rsc_dat(r,i,rscbin,"cost") * Sw_SpurCostMult ; -set m_rsc_con(r,i) "set to detect numeraire rsc techs that have capacity value" ; -m_rsc_con(r,i)$sum{rscbin, m_rsc_dat(r,i,rscbin,"cap") } = yes ; - -m_rscfeas(r,i,rscbin) = no ; -m_rscfeas(r,i,rscbin)$m_rsc_dat(r,i,rscbin,"cap") = yes ; -m_rscfeas(r,i,rscbin)$[sum{ii$tg_rsc_cspagg(ii, i),m_rscfeas(r,ii,rscbin) } - $sum{t$tmodel_new(t), valcap_irt(i,r,t) }] = yes ; -m_rscfeas(r,i,rscbin)$[sum{ii$rsc_agg(ii,i),m_rscfeas(r,ii,rscbin) }$sum{t$tmodel_new(t),valcap_irt(i,r,t) }$psh(i)$Sw_WaterMain] = yes ; -m_rsc_dat(r,i,rscbin,sc_cat)$[sum{ii$rsc_agg(ii,i), m_rsc_dat(r,ii,rscbin,sc_cat) } - $sum{t$tmodel_new(t), valcap_irt(i,r,t) } - $(psh(i) or csp(i)) - $Sw_WaterMain] = - sum{ii$rsc_agg(ii,i), m_rsc_dat(r,ii,rscbin,sc_cat) } ; - - -set force_pcat(pcat,t) "conditional to indicate whether the force prescription equation should be active for pcat" ; - -force_pcat(pcat,t)$[yeart(t) < firstyear_pcat(pcat)] = yes ; -force_pcat(pcat,t)$[sum{r, noncumulative_prescriptions(pcat,r,t) }] = yes ; - -*========================================= -* Decoupled Capacity/Energy Upgrades for hydropower -*========================================= -Parameter -cost_cap_up(i,v,r,rscbin,t) "--2004$/MW-- capacity upgrade costs", -cost_ener_up(i,v,r,rscbin,t) "--2004$/MW-- energy upgrade costs.", -cap_cap_up(i,v,r,rscbin,t) "--MW-- capacity of capacity upgrades", -cap_ener_up(i,v,r,rscbin,t) "--MW-- capacity of energy upgrades", -allow_cap_up(i,v,r,rscbin,t) "i, v, r, and t combinations that are allowed for capacity upsizing", -allow_ener_up(i,v,r,rscbin,t) "i, v, r, and t combinations that are allowed for energy upsizing" -; - -* Adjust available capacity and costs for hydropower upgrades using switch input. -m_rsc_dat(r,'hydUD',rscbin,"cap") = m_rsc_dat(r,'hydUD',rscbin,"cap") * %GSw_HydroUpgradeCapMult% ; -m_rsc_dat(r,'hydUND',rscbin,"cap") = m_rsc_dat(r,'hydUND',rscbin,"cap") * %GSw_HydroUpgradeCapMult% ; -m_rsc_dat(r,'hydUD',rscbin,"cost") = m_rsc_dat(r,'hydUD',rscbin,"cost") * %GSw_HydroUpgradeCostMult% ; -m_rsc_dat(r,'hydUND',rscbin,"cost") = m_rsc_dat(r,'hydUND',rscbin,"cost") * %GSw_HydroUpgradeCostMult% ; - -* Use hydropower upgrade supply curves and multiplier from switch input to define decoupled capacity/energy upgrade costs. -cost_cap_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cost") * %GSw_HydroCostFracCapUp% ; -cost_cap_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cost") * %GSw_HydroCostFracCapUp% ; -cost_ener_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cost") * %GSw_HydroCostFracEnerUp% ; -cost_ener_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cost") * %GSw_HydroCostFracEnerUp% ; - -* Initialize available capacity/energy upgrades to zero to avoid double counting if using coupled capacity/energy upgrades. -cap_cap_up(i,v,r,rscbin,t) = 0 ; -cap_ener_up(i,v,r,rscbin,t) = 0 ; - -* If decoupling hydropower capacity/energy upgrades, use upgrade supply curves to define upgrade resource availability. -$ifthene.hydup2 %GSw_HydroCapEnerUpgradeType% == 2 -* Need to re-multiply by 1000 because inclusion of hydUD and hydUND in the ban(i) set with this setting -* prevents correct scaling of hydro costs. -cost_cap_up(i,v,r,rscbin,t)$cost_cap_up(i,v,r,rscbin,t) = cost_cap_up(i,v,r,rscbin,t) * 1000 ; -cost_ener_up(i,v,r,rscbin,t)$cost_ener_up(i,v,r,rscbin,t) = cost_ener_up(i,v,r,rscbin,t) * 1000 ; - -cap_cap_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cap") + hyd_add_upg_cap(r,'hydUD',rscbin,t) ; -cap_cap_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cap") + hyd_add_upg_cap(r,'hydUND',rscbin,t) ; -cap_ener_up('hydED','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUD',rscbin,"cap") + hyd_add_upg_cap(r,'hydUD',rscbin,t) ; -cap_ener_up('hydEND','init-1',r,rscbin,t) = m_rsc_dat(r,'hydUND',rscbin,"cap") + hyd_add_upg_cap(r,'hydUND',rscbin,t) ; -$endif.hydup2 - -* Use available decoupled upgrade resource to define sets for allowable decoupled capacity/energy upgrades. -allow_cap_up(i,v,r,rscbin,t)$[valcap(i,v,r,t)$cap_cap_up(i,v,r,rscbin,t)$(t.val>=Sw_UpgradeYear)] = yes ; -allow_ener_up(i,v,r,rscbin,t)$[valcap(i,v,r,t)$cap_ener_up(i,v,r,rscbin,t)$(t.val>=Sw_UpgradeYear)] = yes ; - - -* Track the initial amount of m_rsc_dat capacity to compare in e_report -* We adjust upwards by small amounts given potential for infeasibilities -* in very tiny amounts and thus track the extent of the adjustments -parameter m_rsc_dat_init(r,i,rscbin) "--MW-- Initial amount of resource supply curve capacity to compare with final amounts after adjustments" ; -m_rsc_dat_init(r,i,rscbin)$m_rsc_dat(r,i,rscbin,"cap") = m_rsc_dat(r,i,rscbin,"cap") ; - - -*======================================== -* -- CO2 Capture and Storage Network -- -*======================================== -$onempty -set csfeas(cs) "carbon storage sites with available capacity" - r_cs(r,cs) "mapping from BA to carbon storage sites" -/ -$offlisting -$ondelim -$include inputs_case%ds%r_cs.csv -$offdelim -$onlisting -/ , - co2_routes(r,rr) "set of available inter-ba co2 trade relationships" ; - -parameter co2_storage_limit(cs) "--metric tons-- total cumulative storage capacity per carbon storage site", - co2_injection_limit(cs) "--metric tons/hr-- co2 site injection rate upper bound", - cost_co2_pipeline_cap(r,rr,t) "--$2004/(metric ton-mi/hr)-- capital costs associated with investing in co2 pipeline infrastructure", - cost_co2_pipeline_fom(r,rr,t) "--$2004/((metric ton-mi/hr)-yr)-- FO&M costs associated with maintaining co2 pipeline infrastructure", - cost_co2_stor_bec(cs,t) "--$2004/metric ton-- breakeven cost for storing carbon - CF determined by GSw_CO2_BEC", - cost_co2_spurline_cap(r,cs,t) "--$2004/(metric ton-mi/hr)-- capital costs associated with investing in spur lines to injection sites", - cost_co2_spurline_fom(r,cs,t) "--2004/((metric ton-mi/hr)-yr)-- FO&M costs associated with maintaining co2 spurline infrastructure", - r_cs_distance(r,cs) "--mi-- euclidean distance between BA transmission endpoints and storage formations" -/ -$offlisting -$offdigit -$ondelim -$include inputs_case%ds%r_cs_distance_mi.csv -$offdelim -$ondigit -$onlisting -/ , - min_co2_spurline_distance "--mi-- minimum distance for a spur line (used to provide a floor for pipeline distances in r_cs_distance)" -; -$offempty - -* Wherever BA centroids fall within formation boundaries, assume some average spur line distance to connect a CCS or DAC plant with an injection site -min_co2_spurline_distance = 20 ; -r_cs_distance(r,cs)$[r_cs_distance(r,cs) < min_co2_spurline_distance] = min_co2_spurline_distance ; - -* Assign spurline costs -cost_co2_spurline_cap(r,cs,t)$[r_cs(r,cs)$tmodel_new(t)] = Sw_CO2_spurline_cost * r_cs_distance(r,cs) ; - -* CO2 pipelines can be build between any two adjacent BAs -cost_co2_pipeline_cap(r,rr,t)$[routes_adjacent(r,rr)$tmodel_new(t)] = Sw_CO2_pipeline_cost * pipeline_distance(r,rr) ; -cost_co2_pipeline_fom(r,rr,t)$[routes_adjacent(r,rr)$tmodel_new(t)] = Sw_CO2_pipeline_fom * pipeline_distance(r,rr) ; - -co2_routes(r,rr)$routes_adjacent(r,rr) = yes ; - -$onempty -table co2_char(cs,*) "co2 site characteristics including injection rate limit, total storage limit, and break even cost" -$ondelim -$include inputs_case%ds%co2_site_char.csv -$offdelim -; -$offempty - -*note that original units Mton == 'million tons' -co2_storage_limit(cs) = 1e6 * co2_char(cs,"max_stor_cap") ; -co2_injection_limit(cs) = co2_char(cs,"max_inj_rate") ; -cost_co2_stor_bec(cs,t) = co2_char(cs,"bec_%GSw_CO2_BEC%"); - -* only want to consider storage sites that have both available capacity and injection limits -csfeas(cs)$[co2_storage_limit(cs)$co2_injection_limit(cs)] = yes ; -* only want to consider r_cs pairs which have available capacity -r_cs(r,cs)$[not csfeas(cs)] = no ; - -cost_co2_spurline_fom(r,cs,t)$[r_cs(r,cs)$tmodel_new(t)] = Sw_CO2_spurline_fom * r_cs_distance(r,cs) ; - -cost_co2_pipeline_cap(r,rr,t) = %GSw_CO2_CostAdj% * cost_co2_pipeline_cap(r,rr,t); -cost_co2_pipeline_fom(r,rr,t) = %GSw_CO2_CostAdj% * cost_co2_pipeline_fom(r,rr,t); -cost_co2_stor_bec(cs,t) = %GSw_CO2_CostAdj% * cost_co2_stor_bec(cs,t) ; -cost_co2_spurline_fom(r,cs,t) = %GSw_CO2_CostAdj% * cost_co2_spurline_fom(r,cs,t) ; -cost_co2_spurline_cap(r,cs,t) = %GSw_CO2_CostAdj% * cost_co2_spurline_cap(r,cs,t) ; - - -* Parameter tracking for sequential solve -parameter - m_capacity_exog0(i,v,r,t) "--MW-- original value of m_capacity_exog used in d_solveoneyear to make sure upgraded capacity isnt forced into retirement" - z_rep(t) "--$-- objective function value by year" - z_rep_inv(t) "--$-- investment component of objective function by year" - z_rep_op(t) "--$-- operation component of objective function by year" -; -z_rep_inv(t) = 0 ; -z_rep_op(t) = 0 ; - - -*================================================================================================ -*== h- and szn-dependent sets and parameters (declared here, populated in d1_temporal_params) === -*================================================================================================ - -* allh and allszn need to be populated here so they can be used in c_supplymodel and c_supplyobjective -Set allh "all potentially modeled hours" -/ -$offlisting -$include inputs_case%ds%set_allh.csv -$onlisting -/ ; - -Set allszn "all potentially modeled seasons (used as representative days/weks for hourly resolution)" -/ -$offlisting -$include inputs_case%ds%set_allszn.csv -$onlisting -/ ; - -Set -* Timeslices - h(allh) "representative and stress timeslices" - h_preh(allh, allh) "mapping set between one timeslice and all other timeslices earlier in that period" - h_rep(allh) "representative timeslices" - h_stress(allh) "stress timeslices" - h_t(allh,allt) "representative and stress timeslices by model year" - h_stress_t(allh,allt) "stress timeslices by model year" -* "Seasons" (both seasons and representative days/weks) - szn(allszn) "representative and stress periods" - szn_rep(allszn) "representative periods, or seasons if modeling full year" - szn_stress(allszn) "stress periods" - szn_t(allszn,allt) "representative and stress periods by model year" - szn_stress_t(allszn,allt) "stress periods by model year" - szn_actualszn(allszn,allszn) "mapping from rep periods to actual periods" - actualszn(allszn) "actual periods (each is described by a representative period)" -* Mapping between timeslices and "seasons" - h_szn(allh,allszn) "mapping of hour blocks to seasons" - h_szn_start(allszn,allh) "starting hour of each season" - h_szn_end(allszn,allh) "ending hour of each season" - h_szn_t(allh,allszn,allt) "mapping of hour blocks to seasons by model year" - h_actualszn(allh,allszn) "mapping from rep timeslices to actual periods" - nexth_actualszn(allszn,allh,allszn,allh) "mapping between one timeslice and the next for actual periods (szns)" -* Chronology - nexth(allh,allh) "Mapping set between one timeslice (first) and the following (second)" - starting_hour_nowrap(allh) "Flag for whether allh is the first chronological hour by day type" - final_hour(allh) "Flag for whether allh is the last chronological hour in a day type" - final_hour_nowrap(allh) "Flag for whether allh is the last chronological hour in a day type" - nextszn(allszn,allszn) "Mapping between one actual period (allszn) and the next" - nextpartition(allszn,allszn) "Mapping between one partition (allszn) and the next" -* Peak demand - maxload_szn(r,allh,t,allszn) "hour with highest load within each szn" - h_ccseason_prm(allh,ccseason) "peak-load hour for the entire modeled system by ccseason" -* Operating reserves - opres_periods(allszn) "Periods within which the operating reserve constraint applies" - opres_h(allh) "Timeslices within which the operating reserve constraint applies" - dayhours(allh) "daytime hours, used to limit PV capacity to the daytime hours" -* Demand flexibility - flex_h_corr1(flex_type,allh,allh) "correlation set for hours referenced in flexibility constraints" - flex_h_corr2(flex_type,allh,allh) "correlation set for hours referenced in flexibility constraints" -* Minloading - hour_szn_group(allh,allh) "h and hh in the same season - used in minloading constraint" -; - -Parameter -* Hour/period weighting - hours(allh) "--hours-- number of hours in each time block" - numdays(allszn) "--days-- number of days for each season" - numpartitions(allszn) "--days-- number of partitions for each season in timeseries" - hours_daily(allh) "--hours-- number of hours represented by time-slice 'h' during one day" - numhours_nexth(allh,allh) "--hours-- number of times hh follows h throughout year" -* Mapping to quarters - frac_h_quarter_weights(allh,quarter) "--fraction-- fraction of timeslice associated with each quarter" - frac_h_ccseason_weights(allh,ccseason) "--fraction-- fraction of timeslice associated with each ccseason" - szn_quarter_weights(allszn,quarter) "--fraction-- fraction of season associated with each quarter" - szn_ccseason_weights(allszn,ccseason) "--fraction-- fraction of season associated with each ccseason" -* Capacity factor - cf_rsc(i,v,r,allh,t) "--fraction-- capacity factor for rsc tech - t index included for use in CC/curt calculations" - m_cf(i,v,r,allh,t) "--fraction-- modeled capacity factor" - m_cf_szn(i,v,r,allszn,t) "--fraction-- modeled capacity factor, averaged by season" - cf_in(i,r,allh) "--fraction-- capacity factors for renewable technologies" -* Hydropower - cf_hyd(i,allszn,r,allt) "--fraction-- hydro capacity factors by season and year" - climate_hydro_seasonal(r,allszn,allt) "annual/seasonal nondispatchable hydropower availability" - cap_hyd_szn_adj(i,allszn,r) "--fraction-- seasonal max capacity adjustment for dispatchable hydro" - hydmin(i,r,allszn) "minimum hydro loading factors by season and region" -* Availability (forced and scheduled outage rates) - outage_forced_h(i,r,allh) "--fraction-- forced outage rate" - outage_scheduled_h(i,allh) "--fraction-- scheduled outage rate" - avail(i,r,allh) "--fraction-- fraction of capacity available for generation by hour" - seas_cap_frac_delta(i,v,r,allszn,allt) "--scalar-- fractional change in seasonal capacity compared to summer" -* Demand - load_exog(r,allh,t) "--MW-- busbar load" - load_exog0(r,allh,t) "--MW-- original load by region hour and year - unchanged by demand side" - load_allyear(r,allh,allt) "--MW-- end-use load by region, timeslice, and year" - h2_exogenous_demand_regional(r,p,allh,allt) "--metric tons per hour-- exogenous demand for hydrogen at the BA level" -* Peak demand - peak_static_frac(r,ccseason,t) "--fraction-- fraction of peak demand that is static" - peakdem_static_ccseason(r,ccseason,t) "--MW-- busbar peak demand by ccseason" - peak_ccesason(r,ccseason,allt) "--MW-- end-use peak demand by region, ccseason, year" - peakdem_static_h(r,allh,t) "--MW-- busbar peak demand by timeslice" - peak_h(r,allh,allt) "--MW-- busbar peak demand by timeslice" -* Canada and Mexico demand - canmexload(r,allh) "load for canadian and mexican regions" -* Demand flexibility - flex_frac_load(flex_type,r,allh,allt) - flex_demand_frac(flex_type,r,allh,t) "fraction of load able to be considered flexible" - load_exog_flex(flex_type,r,allh,t) "the amount of exogenous load that is flexible" - load_exog_static(r,allh,t) "the amount of exogenous load that is static" - dr_shed_out(i,r,allh) "--fraction-- dr shed capacity availability" -* EVMC storage - evmc_storage_discharge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be discharged (deferred charging) in each timeslice h" - evmc_storage_charge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be charged (add back deferred charging) in each timeslice h" - evmc_storage_energy_hours(i,r,allh,allt) "--hours-- Allowable EV storage SOC (quantity deferred EV charge) [MWh] divided by nameplate EVMC discharge capacity [MW]" -* EVMC load - evmc_baseline_load(r,allh,allt) "--MW-- baseline electricity load from EV charging by timeslice h and year t" - evmc_shape_load(i,r,allh) "--fraction-- fraction of adopted price-responsive (shaped) EV load added by timeslice" - evmc_shape_gen(i,r,allh) "--fraction-- fraction of adopted price-responsive (shaped) EV load subtracted by timeslice" -* Flexible Canadian imports/exports [Sw_Canada=1] - can_imports_szn(r,allszn,t) "--MWh-- [Sw_Canada=1] seasonal imports from Canada by year" - can_imports_szn_frac(allszn) "--fraction-- [Sw_Canada=1] fraction of annual imports that occur in each season" - can_exports_h(r,allh,t) "--MW-- [Sw_Canada=1] timeslice exports to Canada by year" - can_exports_h_frac(allh) "--fraction-- [Sw_Canada=1] fraction of annual exports by timeslice" -* Resource adequacy - prm_year(r) "--fraction-- planning reserve margin for the current solve year" -* Capacity credit - sdbin_size(ccreg,ccseason,sdbin,t) "--MW-- available power capacity by storage duration bin - used to bin the peaking power capacity contribution of storage by duration" - cc_old(i,r,ccseason,t) "--MW-- capacity credit for existing capacity - used in sequential solve similar to heritage reeds" - cc_mar(i,r,ccseason,t) "--fraction-- cc_mar loading initialized to some reasonable value for the 2010 solve" - cc_int(i,v,r,ccseason,t) "--fraction-- average fractional capacity credit - used in intertemporal solve" - cc_excess(i,r,ccseason,t) "--MW-- this is the excess capacity credit when assuming marginal capacity credit in intertemporal solve" - vre_gen_last_year(r,allh,t) "--MW-- generation from VRE generators in the prior solve year" - hybrid_cc_derate(i,r,ccseason,sdbin,t) "--fraction-- derate factor for hybrid PV+battery storage capacity credit" - m_cc_mar(i,r,ccseason,t) "--fraction-- marginal capacity credit", -* Heuristic climate impacts - trans_cap_delta(allh,allt) "--fraction-- fractional adjustment to transmission capacity from climate heuristics" -* Emissions and policies - h_weight_csapr(allh) "hour weights for CSAPR ozone season constraints" -* Water access - watsa(wst,r,allszn,t) "--fraction-- seasonal distribution factors for new water access by year" - watsa_climate(wst,r,allszn,allt) "--fraction-- time-varying fractional seasonal allocation of water" -* Minloading - minloadfrac(r,i,allh) "--fraction-- minimum loading fraction - final used in model" -* Fossil gas supply curve - gasadder_cd(cendiv,t,allh) "--$/MMbtu-- adder for NG census division" - szn_adj_gas(allh) "--fraction-- seasonal adjustment for gas prices" -; - -alias(allh,allhh,allhhh) ; -alias(h,hh,hhh) ; -alias(allszn,allsznn) ; -alias(actualszn,actualsznn,actualsznnn) ; -alias(szn,sznn) ; - -* Initialize some parameters -sdbin_size(ccreg,ccseason,sdbin,"%startyear%") = 1000 ; -cc_int(i,v,r,ccseason,t) = 0 ; -cc_excess(i,r,ccseason,t) = 0 ; -cc_old(i,r,ccseason,t) = 0 ; -m_cc_mar(i,r,ccseason,t) = 0 ; -hybrid_cc_derate(i,r,ccseason,sdbin,t)$[pvb(i)$valcap_irt(i,r,t)] = 1 ; - -* Trim some of the largest matrices to reduce file sizes -cost_vom(i,v,r,t)$[not valgen(i,v,r,t)] = 0 ; -cost_fom(i,v,r,t)$[not valcap(i,v,r,t)] = 0 ; -heat_rate(i,v,r,t)$[not valgen(i,v,r,t)] = 0 ; -m_capacity_exog(i,v,r,t)$[not valcap(i,v,r,t)] = 0 ; -emit_rate(etype,e,i,v,r,t)$[not valgen(i,v,r,t)] = 0 ; - -*============================================================ -* -- Initial state of parameters that change as model runs -- -*============================================================ - -valinv_init(i,v,r,t) = valinv(i,v,r,t) ; -valcap_init(i,v,r,t) = valcap(i,v,r,t) ; diff --git a/c_mga.gms b/c_mga.gms deleted file mode 100644 index 8bf3841b..00000000 --- a/c_mga.gms +++ /dev/null @@ -1,77 +0,0 @@ -*============================================== -* -- Modeling to Generate Alternatives (MGA) -- -*============================================== - -Equation eq_MGA_CostEnvelope(t) "--$-- System cost must be within allowed envelope" ; -eq_MGA_CostEnvelope(t)$[tmodel(t)$Sw_MGA].. - (z_rep_inv(t) + z_rep_op(t)) * (1 + Sw_MGA_CostDelta) - =g= - Z_inv(t) + Z_op(t) -; - -* --------------------------------------------------------------------------- - -$ifthen.mgaobj %GSw_MGA_Objective% == 'capacity' -Equation eq_MGA_Objective "--MW-- Defines generation capacity for MGA" ; -Variable MGA_OBJ "--MW-- Capacity of technology to be minimized/maximied" ; -eq_MGA_Objective$Sw_MGA.. - MGA_OBJ - =e= - sum{(i,v,r,t) - $[tmodel(t) - $valcap(i,v,r,t) - $%GSw_MGA_SubObjective%(i)], - CAP(i,v,r,t) - } -; - -* --------------------------------------------------------------------------- - -$elseif.mgaobj %GSw_MGA_Objective% == 'transmission' -Equation eq_MGA_Objective "--MW-- Defines transmission capacity for MGA" ; -Variable MGA_OBJ "--MW-- Transmission capacity of all types" ; -eq_MGA_Objective$Sw_MGA.. - MGA_OBJ - =e= - sum{(r,rr,trtype,t) - $[tmodel(t) - $routes(r,rr,trtype,t)], - CAPTRAN_ENERGY(r,rr,trtype,t) - } -; - -* --------------------------------------------------------------------------- - -$elseif.mgaobj %GSw_MGA_Objective% == 'rasharing' -Equation eq_MGA_Objective "--MWh-- Defines RA flows for MGA" ; -Variable MGA_OBJ "--MWh-- Flows between NERC regions during stress periods" ; -eq_MGA_Objective$Sw_MGA.. - MGA_OBJ - =e= - sum{(r,rr,h,trtype,nercr,nercrr,t) - $[tmodel(t) - $routes(r,rr,trtype,t) - $routes_prm(r,rr) - $routes_nercr(nercr,nercrr,r,rr) - $h_stress(h)], - FLOW(r,rr,h,t,trtype) * hours(h) - } -; - -* --------------------------------------------------------------------------- - -$elseif.mgaobj %GSw_MGA_Objective% == 'co2' -Equation eq_MGA_Objective "--tonne-- Defines CO2 emissions for MGA" ; -Variable MGA_OBJ "--tonne-- Direct (process) CO2 emissions" ; -eq_MGA_Objective$Sw_MGA.. - MGA_OBJ - =e= - sum{(r,t) - $[tmodel(t)], - EMIT("process","CO2",r,t) - } -; - -* --------------------------------------------------------------------------- - -$endif.mgaobj diff --git a/c_supplymodel.gms b/c_supplymodel.gms deleted file mode 100644 index 029d53f1..00000000 --- a/c_supplymodel.gms +++ /dev/null @@ -1,3976 +0,0 @@ -*Setting the default slash -$setglobal ds \ - -*Change the default slash if in UNIX -$ifthen.unix %system.filesys% == UNIX -$setglobal ds / -$endif.unix - -*======================================== -* -- Supply Side Variable Declaration -- -*======================================== - -positive variables - -* load variable - set equal to load_exog to compute holistic marginal price - LOAD(r,allh,t) "--MW-- busbar load for each balancing region" - FLEX(flex_type,r,allh,t) "--MW-- flexible load shifted to each timeslice" -* PEAK_FLEX(r,ccseason,t) "--MW-- peak busbar load adjustment based on load flexibility" - DROPPED(r,allh,t) "--MW-- dropped load (only allowed before Sw_StartMarkets)" - EXCESS(r,allh,t) "--MW-- excess load (only allowed before Sw_StartMarkets)" - CAP_LOADSITE(r,t) "--MW-- capacity of flexibly sited load" - INV_LOADSITE(r,t) "--MW-- capacity of flexibly sited load installed in model year" - OP_LOADSITE(r,allh,t) "--MW-- operations of flexibly sited load" - -* capacity and investment variables - CAP_SDBIN(i,v,r,ccseason,sdbin,t) "--MW-- generation power capacity by storage duration bin for relevant technologies" - CAP_SDBIN_ENERGY(i,v,r,ccseason,sdbin,t) "--MWh-- generation energy capacity by storage duration bin for relevant technologies" - CAP(i,v,r,t) "--MW-- total generation capacity in MWac (MWdc for PV); PV capacity of hybrid PV+battery; max native, flexible EV load for EVMC" - CAP_ENERGY(i,v,r,t) "--MWh-- battery capacity in terms of energy" - CAP_ABOVE_LIM(tg,r,t) "--MW-- amount of capacity that is deployed above the interconnection queue limits" - CAP_RSC(i,v,r,rscbin,t) "--MW-- total generation capacity in MWac (MWdc for PV) for wind-ons and upv" - GROWTH_BIN(gbin,i,st,t) "--MW-- total new (from INV) generation capacity in each growth bin by state and technology group" - INV(i,v,r,t) "--MW-- generation capacity additions in year t" - INV_ENERGY(i,v,r,t) "--MWh-- generation energy capacity additions in year t" - EXTRA_PRESCRIP(pcat,r,t) "--MW-- builds beyond those prescribed power capacity once allowed in firstyear(pcat) - exceptions for gas-ct, wind-ons, and wind-ofs" - EXTRA_PRESCRIP_ENERGY(pcat,r,t) "--MWh-- builds beyond those prescribed battery energy capacity once allowed in firstyear(pcat)" - INV_CAP_UP(i,v,r,rscbin,t) "--MW-- upsized generation capacity addition in year t" - INV_ENER_UP(i,v,r,rscbin,t) "--MW-- upsized energy addition in year t using capacity factor to convert to capacity units" - INV_REFURB(i,v,r,t) "--MW-- investment in refurbishments of technologies that use a resource supply curve" - INV_RSC(i,v,r,rscbin,t) "--MW-- investment in technologies that use a resource supply curve" - UPGRADES(i,v,r,t) "--MW-- investments in upgraded capacity from ii to i" - UPGRADES_RETIRE(i,v,r,t) "--MW-- upgrades that have been retired - used as a free slack variable in eq_cap_upgrade" - -* The units for all of the operational variables are average MW or MWh/time-slice hours -* generation and storage variables - GEN(i,v,r,allh,t) "--MW-- electricity generation (post-curtailment) in hour h" - GEN_PLANT(i,v,r,allh,t) "--MW-- average plant generation from hybrid generation/storage technologies in hour h" - GEN_STORAGE(i,v,r,allh,t) "--MW-- average generation from hybrid storage technologies in hour h" - STORAGE_IN_PLANT(i,v,r,allh,t) "--MW-- hybrid plant storage charging in hour h that is charging from a coupled technology" - STORAGE_IN_GRID(i,v,r,allh,t) "--MW-- hybrid plant storage charging in hour h that is charging from the grid" - AVAIL_SITE(x,allh,t) "--MW-- available generation from all resources at reV site x" - CURT(r,allh,t) "--MW-- curtailment from vre generators in hour h" - MINGEN(r,allszn,t) "--MW-- minimum generation level in each season" - STORAGE_IN(i,v,r,allh,t) "--MW-- storage charging in hour h that is charging from a given source technology; not used for CSP-TES" - STORAGE_LEVEL(i,v,r,allh,t) "--MWh-- storage level in hour h" - STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) "--MWh-- storage level at hour 0 of the partition" - RAMPUP(i,r,allh,allhh,t) "--MW-- upward change in generation from h to hh" - -* flexible CCS variables - CCSFLEX_POW(i,v,r,allh,t) "--avg MW-- average power consumed for CCS system" - CCSFLEX_POWREQ(i,v,r,allh,t) "--avg MW-- average power requirement for CCS system" - CCSFLEX_STO_STORAGE_LEVEL(i,v,r,allh,t) "--varies-- level of process storage (e.g., chemical solvent) in the CCS system" - CCSFLEX_STO_STORAGE_CAP(i,v,r,t) "--varies-- capacity of process storage (e.g., chemical solvent) in the CCS system" - -* trade variables - FLOW(r,rr,allh,t,trtype) "--MW-- electricity flow on transmission lines in hour h" - OPRES_FLOW(ortype,r,rr,allh,t) "--MW-- interregional trade of operating reserves by operating reserve type" - PRMTRADE(r,rr,trtype,ccseason,t) "--MW-- planning reserve margin capacity traded from r to rr" - -* operating reserve variables - OPRES(ortype,i,v,r,allh,t) "--MW-- operating reserves by type" - -* variable fuel amounts - GASUSED(cendiv,gb,allh,t) "--MMBtu/hour-- total gas used by gas bin", - VGASBINQ_NATIONAL(fuelbin,t) "--MMBtu-- National quantity of gas by bin" - VGASBINQ_REGIONAL(fuelbin,cendiv,t) "--MMBtu-- Regional (census divisions) quantity of gas by bin" - BIOUSED(bioclass,r,t) "--MMBtu-- total biomass used by biomass class" - -* RECS variables - RECS(RPSCat,i,st,ast,t) "--MWh-- renewable energy credits from state st to state ast", - ACP_PURCHASES(RPSCat,st,t) "--MWh-- purchases of ACP credits to meet the RPS constraints", - -* transmission variables - CAPTRAN_ENERGY(r,rr,trtype,t) "--MW-- capacity of transmission for energy trading" - CAPTRAN_PRM(r,rr,trtype,t) "--MW-- capacity of transmission for PRM trading" - CAPTRAN_GRP(transgrp,transgrpp,t) "--MW-- capacity of groups of transmission interfaces" - CAPTRAN_ITL(itlgrp,itlgrpp,t) "--MW-- capacity of groups of transmission interfaces for county and mixed" - INVTRAN(r,rr,trtype,t) "--MW-- investment in transmission capacity (defined for both directions)" - INVTRAN_AC(r,rr,tscbin,t) "--MW-- transmission capacity added to transmission supply curve bin (defined for both directions)" - CAP_CONVERTER(r,t) "--MW-- VSC AC/DC converter capacity" - INV_CONVERTER(r,t) "--MW-- investment in AC/DC converter capacity" - CONVERSION(r,allh,intype,outtype,t) "--MW-- conversion of AC->DC or DC->AC" - CONVERSION_PRM(r,ccseason,intype,outtype,t) "--MW-- planning reserve margin capacity sent through VSC AC/DC converters" - CAP_SPUR(x,t) "--MW-- capacity of spur lines" - INV_SPUR(x,t) "--MW-- investment in spur line capacity" - INV_POI(r,t) "--MW-- investment in new POI capacity (for network reinforcement costs)" - TRAN_CAPEX_BINS(r,rr,tscbin,t) "--$-- transmission capex cost bins (defined for r < rr)" - -* production-, CO2-, and hydrogen-specific variables - PRODUCE(p,i,v,r,allh,t) "--metric tons per hour-- production of hydrogen or DAC capture" - CO2_CAPTURED(r,allh,t) "--metric tons per hour-- amount of CO2 captured from DAC and CCS technologies" - CO2_STORED(r,cs,allh,t) "--metric tons per hour-- amount of CO2 stored underground" - CO2_FLOW(r,rr,allh,t) "--metric tons per hour-- interregional flow of CO2" - CO2_TRANSPORT_INV(r,rr,t) "--metric tons per hour-- investment in interregional CO2 transport capacity" - CO2_SPURLINE_INV(r,cs,t) "--metric tons per hour-- spurline investment from r to carbon storage site (saline storage basin)" - H2_FLOW(r,rr,allh,t) "--metric tons per hour-- interregional flow of hydrogen" - H2_TRANSPORT_INV(r,rr,t) "--metric tons per hour-- investment in interregional hydrogen transmission capacity" - H2_STOR_INV(h2_stor,r,t) "--metric tons-- investment in hydrogen storage capacity" - H2_STOR_CAP(h2_stor,r,t) "--metric tons-- hydrogen storage capacity" - H2_STOR_IN(h2_stor,r,allh,t) "--metric tons per hour-- injection of H2 into storage in a given timeslice" - H2_STOR_OUT(h2_stor,r,allh,t) "--metric tons per hour-- widthdrawal of H2 from storage in a given timeslice" - H2_STOR_LEVEL(h2_stor,r,actualszn,allh,t) "--metric tons-- total storage level of H2 in a timeslice by storage type" - H2_STOR_LEVEL_SZN(h2_stor,r,actualszn,t) "--metric tons-- total storage level of H2 in a period by storage type" - CREDIT_H2PTC(i,v,r,allh,t) "--MW-- generation by resources which qualify for the hydrogen production tax credit, in hour h" - -* water climate variables - WATCAP(i,v,r,t) "--million gallons/year; Mgal/yr-- total water access capacity available in terms of withdraw/consumption per year" - WAT(i,v,w,r,allh,t) "--Mgal-- quantity of water withdrawn or consumed in hour h" - WATER_CAPACITY_LIMIT_SLACK(wst,r,t) "--Mgal/yr-- insufficient water supply in region r, of water type wst, in year t " -; - -Variables -* with negative emissions technologies (e.g. BECCS, DAC) - emissions -* can become negative thus not restricted to the positive domain - EMIT(etype,eall,r,t) "--metric tons-- emissions (broken down to upstream and process) in a region" - -* inter-day storage variables - STORAGE_INTERDAY_DISPATCH(i,v,r,allh,t) "--MW-- net dispatch for storage in hour h" - STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,allszn,t) "--MWh-- maximum relative state of charge on a representative period compared to hour 0 of the rep period" - STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,allszn,t) "--MWh-- minimum relative state of charge on a representative period compared to hour 0 of the rep period" -; - -*======================================== -* -- Supply Side Equation Declaration -- -*======================================== -EQUATION - -* load constraint to compute proper marginal value - eq_loadcon(r,allh,t) "--MW-- load constraint used for computing the marginal energy price" - -* load flexibility constraints - eq_load_flex_day(flex_type,r,allszn,t) "--MWh-- total flexible load in each season is equal to the exogenously-specified flexible load" - eq_load_flex1(flex_type,r,allh,t) "--MWh-- exogenously-specified flexible demand (load_exog_flex) must be served by flexible load (FLEX)" - eq_load_flex2(flex_type,r,allh,t) "--MWh-- flexible load (FLEX) can't exceed exogenously-specified flexible demand (load_exog_flex)" -* eq_load_flex_peak(r,allh,ccseason,t) "--MWh-- adjust peak demand as needed based on the load flexibility (FLEX)" - eq_loadsite_inv(r,t) "--MW-- CAP_LOADSITE accumulates INV_LOADSITE" - eq_loadsite_cap(r,allh,t) "--MW-- Realized load from optimally sited load must be less than optimally sited load capacity" - eq_loadsite_op(r,t) "--MW-- Realized load from optimally sited load must sum to Sw_LoadSiteCF" - eq_loadsite_siting(loadsitereg,t) "--MW-- Optimally sited load capacity must sum to loadsite_annual" - -* capital stock constraints - eq_cap_init_noret(i,v,r,t) "--MW-- Existing capacity that cannot be retired is equal to exogenously-specified amount" - eq_cap_init_retmo(i,v,r,t) "--MW-- Existing capacity that can be retired must be monotonically decreasing" - eq_cap_init_retub(i,v,r,t) "--MW-- Existing capacity that can be retired is less than or equal to exogenously-specified amount" - eq_cap_new_noret(i,v,r,t) "--MW-- New power capacity that cannot be retired is equal to sum of all previous years investment" - eq_cap_energy_new_noret(i,v,r,t) "--MWh-- New energy capacity that cannot be retired is equal to sum of all previous years investment" - eq_cap_new_retmo(i,v,r,t) "--MW-- New capacity that can be retired must be monotonically decreasing unless increased by investment" - eq_cap_new_retub(i,v,r,t) "--MW-- New capacity that can be retired is less than or equal to all previous years investment" - eq_cap_rsc(i,v,r,rscbin,t) "--MW-- Capacity accounting for techs with exogenous capacity tracked by rscbin" - eq_cap_up(i,v,r,rscbin,t) "--MW-- limit on capacity upsizing" - eq_cap_upgrade(i,v,r,t) "--MW-- All purchased upgrades are greater than or equal to the sum of upgraded capacity" - eq_ener_up(i,v,r,rscbin,t) "--MW-- limit on energy upsizing" - eq_forceprescription_power(pcat,r,t) "--MW-- total power investment in prescribed capacity must equal amount from exogenous prescriptions" - eq_forceprescription_energy(pcat,r,t) "--MWh-- total energy investment in prescribed capacity must equal amount from exogenous prescriptions" - eq_refurblim(i,r,t) "--MW-- total refurbishments cannot exceed the amount of capacity that has reached the end of its life" - -* renewable supply curves - eq_rsc_inv_account(i,v,r,t) "--MW-- INV for rsc techs is the sum over all bins of INV_RSC" - eq_rsc_INVlim(r,i,rscbin,t) "--MW-- total investment from each rsc bin cannot exceed the available investment" - -* capacity growth limits - eq_growthlimit_relative(i,st,t) "--MW-- relative growth limit on technologies" - eq_growthbin_limit(gbin,st,tg,t) "--MW-- capacity limit for each growth bin" - eq_growthlimit_absolute(tg,t) "--MW-- absolute growth limit on technologies" - -eq_interconnection_queues(tg,r,t) "--MW-- capacity deployment limit based on interconnection queues" - -* storage capacity credit supply curves - eq_cap_sdbin_balance(i,v,r,ccseason,t) "--MW-- total binned storage power capacity must be greater than total storage capacity" - eq_cap_sdbin_energy_balance(i,v,r,ccseason,t) "--MWh-- total binned storage energy capacity must be greater than total storage capacity" - eq_sdbin_power_energy_link(i,v,r,ccseason,sdbin,t) "--MWh-- binned storage energy capacity equal to binned power storage capacity times bin duration" - eq_sdbin_power_limit(ccreg,ccseason,sdbin,t) "--MW-- binned storage power capacity cannot exceed storage duration bin size" - -* operation and reliability - eq_site_cf(x,allh,t) "--MW-- generation at site x <= CF * capacity of constituent resources" - eq_spurclip(x,allh,t) "--MW-- generation at site x <= spurline capacity to x" - eq_spur_noclip(x,t) "--MW-- spurline capacity to x must equal total generation capacity at x" - eq_capacity_limit(i,v,r,allh,t) "--MW-- generation limited to available capacity" - eq_capacity_limit_hybrid(r,allh,t) "--MW-- generation from hybrid resources limited to available capacity" - eq_capacity_limit_nd(i,v,r,allh,t) "--MW-- generation limited to available capacity for non-dispatchable resources" - eq_curt_gen_balance(r,allh,t) "--MW-- net generation and curtailment must equal gross generation" - eq_dhyd_dispatch(i,v,r,allszn,t) "--MWh-- dispatchable hydro seasonal energy constraint (when not allowing seasonal enregy shifting)" - eq_min_cf(i,r,t) "--MWh-- minimum capacity factor constraint for each generator fleet, applied to (i,r)" - eq_max_daily_cf(i,r,allszn,t) "--MWh-- maximum daily capacity factor constraint for any technology with maxdailycf(i,t) specified" - eq_mingen_fixed(i,v,r,allh,t) "--MW-- Generation in each timeslice must be greater than mingen_fixed * available capacity" - eq_mingen_lb(r,allh,allszn,t) "--MW-- lower bound on minimum generation level" - eq_mingen_ub(r,allh,allszn,t) "--MW-- upper bound on minimum generation level" - eq_minloading(i,v,r,allh,allhh,t) "--MW-- minimum loading across same-season hours" - eq_ramping(i,r,allh,allhh,t) "--MW-- definition of RAMPUP" - eq_reserve_margin(r,ccseason,t) "--MW-- planning reserve margin requirement" - eq_supply_demand_balance(r,allh,t) "--MW-- supply demand balance" - eq_vsc_flow(r,allh,t) "--MW-- DC power flow" - eq_transmission_limit(r,rr,allh,t,trtype) "--MW-- transmission flow limit" - -* operating reserve constraints - eq_OpRes_requirement(ortype,r,allh,t) "--MW-- operating reserve constraint" - eq_ORCap_large_res_frac(ortype,i,v,r,allh,t) "--MW-- operating reserve capacity availability constraint for generators with reserve_frac > 0.5" - eq_ORCap_small_res_frac(ortype,i,v,r,allh,t) "--MW-- operating reserve capacity availability constraint for generators with reserve_frac <= 0.5" - -* regional and national policies - eq_emit_accounting(etype,e,r,t) "--metric tons-- accounting for total emissions in a region" - eq_emit_rate_limit(e,r,t) "--metric tons per MWh-- emission rate limit" - eq_annual_cap(eall,t) "--metric tons-- annual (year-specific) emissions cap", - eq_bankborrowcap(e) "--weighted metric tons-- flexible banking and borrowing cap (to be used w/intertemporal solve only" - eq_RGGI_cap(t) "--metric tons CO2-- RGGI constraint -- Regions' emissions must be less than the RGGI cap" - eq_state_cap(st,t) "--metric tons CO2-- state-level CO2 cap constraint -- used to represent California cap and trade program" - eq_CSAPR_Budget(csapr_group,t) "--metric tons NOx-- CSAPR trading group emissions cannot exceed the budget cap" - eq_CSAPR_Assurance(st,t) "--metric tons NOx-- CSAPR state emissions cannot exceed the assurance cap" - eq_BatteryMandate(st,t) "--MW-- battery storage capacity must be greater than indicated level" - eq_cdr_cap(t) "--metric tons CO2-- CO2 removal (DAC and BECCS) can only offset emissions from fossil+CCS and methane leakage" - eq_caa_max_cf(i,v,r,t) "--MWh-- maximum capacity factors for new gas plants (CCs and CTs) under Clean Air Act Section 111 (BSER)" - eq_caa_rate_standard(st,t) "--metric tons CO2-- maximum coal emissions per state under Clean Air Act Section 111 (rate-based emissions standard)" - -* RPS Policy equations - eq_REC_Generation(RPSCat,i,st,t) "--RECs-- Generation of RECs by state" - eq_REC_Requirement(RPSCat,st,t) "--RECs-- RECs generated plus trade must meet the state's requirement" - eq_REC_ooslim(RPSCat,st,t) "--RECs-- RECs imported cannot exceed a fraction of total requirement for certain states", - eq_REC_launder(RPSCat,st,t) "--RECs-- RECs laundering constraint" - eq_REC_BundleLimit(RPSCat,st,ast,t) "--RECS-- trade in bundle recs must be less than interstate electricity transmission" - eq_REC_unbundledLimit(RPScat,st,t) "--RECS-- unbundled RECS cannot exceed some percentage of total REC requirements" - eq_RPS_OFSWind(st,t) "--MW-- MW of offshore wind capacity must be greater than or equal to RPS amount" - eq_national_gen(t) "--MWh-- e.g. a national RPS or CES. require a certain amount of total generation to be from specified sources." - -* fuel supply curve equations - eq_gasused(cendiv,allh,t) "--MMBtu-- gas used must be from the sum of gas bins" - eq_gasbinlimit(cendiv,gb,t) "--MMBtu-- limit on gas from each bin" - eq_gasbinlimit_nat(gb,t) "--MMBtu-- national limit on gas from each bin" - eq_bioused(r,t) "--MMBtu-- bio used must be from the sum of bio bins" - eq_biousedlimit(bioclass,usda_region,t) "--MMBtu-- limit on bio from each bin in each USDA region" - -* regional natural gas supply curves - eq_gasaccounting_regional(cendiv,t) "--MMBtu-- regional gas consumption cannot exceed the amount used in bins" - eq_gasaccounting_national(t) "--MMBtu-- national gas consumption cannot exceed the amount used in bins" - eq_gasbinlimit_regional(fuelbin,cendiv,t) "--MMBtu-- regional binned gas usage cannot exceed bin capacity" - eq_gasbinlimit_national(fuelbin,t) "--MMBtu-- national binned gas usage cannot exceed bin capacity" - -* hydrogen supply and demand - eq_prod_capacity_limit(i,v,r,allh,t) "--metric tons-- production cannot exceeds its capacity" - eq_h2_demand(p,t) "--metric tons-- production of hydrogen must meet exogenous demand plus H2-CT/CC use" - eq_h2_demand_regional(r,allh,t) "--metric tons per hour-- regional hydrogen supply must equal demand net trade and storage" - eq_h2_transport_caplimit(r,rr,allh,t) "--metric tons per hour-- H2 flow cannot exceed cumulative pipeline investment" - eq_h2_storage_flowlimit(h2_stor,r,allh,t) "--metric tons per hour-- H2 storage injection or withdrawal cannot exceed cumulative storage investment" - eq_h2_storage_capacity(h2_stor,r,t) "--metric tons-- H2 storage capacity is sum of H2 storage investments" - eq_h2_min_storage_cap(r,t) "--metric tons-- H2 storage capacity must be ≥ Sw_H2_MinStorHours * H2 usage capacity" - eq_h2_ptc_region_balance(h2ptcreg,t) "--MWh-- clean generation for hydrogen production must be more than electricity required for electrolytic H2 production in that region and year" - eq_h2_ptc_region_hour_balance(h2ptcreg,allh,t) "--MWh-- clean generation for hydrogen production must be more than electricity required for electrolytic H2 production in that region, hour and year" - eq_h2_ptc_creditgen(i,v,r,allh,t) "--MWh-- total generation must be greater than clean generation for hydrogen production" - eq_h2_storage_caplimit(h2_stor,r,actualszn,allh,t) "--metric tons-- total H2 storage in a storage facility cannot exceed investment capacity" - eq_h2_storage_level(h2_stor,r,actualszn,allh,t) "--metric tons-- tracks H2 storage level by storage type and BA within and across periods" - eq_h2_storage_caplimit_szn(h2_stor,r,actualszn,t) "--metric tons-- total H2 storage in a storage facility cannot exceed investment capacity" - eq_h2_storage_level_szn(h2_stor,r,actualszn,t) "--metric tons-- tracks H2 storage level by storage type and BA within and across periods" - -* CO2 capture and storage - eq_co2_capture(r,allh,t) "--metric tons-- accounting of CO2 captured from DAC and CCS technologies" - eq_co2_injection_limit(cs,allh,t) "--metric tons per hour-- limit on CO2 injection for each carbon site as a rate" - eq_co2_sink(r,allh,t) "--metric tons per hour-- co2 stored or used must exceed co2 captured plus net trade" - eq_co2_transport_caplimit(r,rr,allh,t) "--metric tons-- limit on interregional co2 trade" - eq_co2_spurline_caplimit(r,cs,allh,t) "--metric tons-- limit on transport of CO2 from BA to carbon storage site" - eq_co2_cumul_limit(cs,t) "--cumulative metric tons-- total stored in a reservor cannot exceed capacity" - -* transmission equations - eq_INVTRAN_DC(r,rr,trtype,t) "--MW-- DC transmission additions are assumed to add the same capacity in both directions" - eq_INVTRAN_AC_forward(r,rr,tscbin,t) "--$-- Forward transmission capacity is determined by the cumulative capex invested in the interface" - eq_INVTRAN_AC_reverse(r,rr,tscbin,t) "--$-- Reverse transmission capacity is determined by the cumulative capex invested in the interface" - eq_INVTRAN_AC(r,rr,t) "--MW-- Accumulate investment in tscbins into INVTRAN" - eq_TRAN_CAPEX_BINS(r,rr,tscbin,t) "--$-- Transmission investment bins are limited by the transmission upgrade supply curve" - eq_invtran_exog(r,rr,trtype,t) "--MW-- Exogenous transmission investments are included in INVTRAN" - eq_CAPTRAN_ENERGY(r,rr,trtype,t) "--MW-- capacity accounting for transmission capacity for energy trading" - eq_CAPTRAN_PRM(r,rr,trtype,t) "--MW-- capacity accounting for transmission capacity for PRM trading" - eq_prescribed_transmission(r,rr,trtype,t) "--MW-- investment in transmission up to first year allowed must be less than the exogenous possible transmission", - eq_PRMTRADELimit(r,rr,trtype,ccseason,t) "--MW-- trading of PRM capacity cannot exceed the line's capacity" - eq_transmission_investment_max(t) "--MWmile-- investment in transmission must be <= Sw_TransInvMax" - eq_CAPTRAN_max(r,rr,trtype,t) "--MW-- upper limit for transmission capacity of each trtype across individual interfaces" - eq_CAPTRAN_max_total(r,rr,t) "--MW-- upper limit for transmission capacity of all trtypes across individual interfaces" - eq_CAP_CONVERTER(r,t) "--MW-- capacity accounting for VSC AC/DC converters" - eq_CAP_SPUR(x,t) "--MW-- capacity accounting for spur lines" - eq_converter_max(r,t) "--MW-- upper limit for VSC AC/DC converter capacity in individual BAs" - eq_CONVERSION_limit_energy(r,allh,t) "--MW-- AC/DC energy conversion is limited to converter capacity" - eq_CONVERSION_limit_prm(r,ccseason,t) "--MW-- AC/DC PRM conversion is limited to converter capacity" - eq_PRMTRADE_VSC(r,ccseason,t) "--MW-- PRM capacity can flow through VSC lines but doesn't directly contribute to PRM" - eq_POI_cap(r,t) "--MW-- POI capacity accounting (for network reinforcement costs)" - eq_CAPTRAN_GRP(transgrp,transgrpp,t) "--MW-- combined flow capacity between transmission groups" - eq_transgrp_limit_energy(transgrp,transgrpp,allh,t) "--MW-- limit on combined interface energy flows" - eq_transgrp_limit_prm(transgrp,transgrpp,ccseason,t) "--MW-- limit on combined interface PRM flows" - eq_CAPTRAN_ITL(itlgrp,itlgrpp,t) "--MW-- combined flow capacity between ITL groups" - eq_itlgrp_limit_energy(itlgrp,itlgrpp,allh,t) "--MW-- limit on combined interface energy flows for ITLs" - eq_itlgrp_limit_prm(itlgrp,itlgrpp,ccseason,t) "--MW-- limit on combined interface PRM flows for ITLs" - eq_firm_transfer_limit(nercr,allh,t) "--MW-- limit net firm capacity imports into NERC regions when using stress periods" - eq_firm_transfer_limit_cc(nercr,ccseason,t) "--MW-- limit net firm capacity imports into NERC regions when using capacity credit" - eq_offshore_no_backflow(r,rr,trtype,allh,t) "--MW-- disallow transmission flows from land to offshore zones" - -* storage-specific equations - eq_storage_capacity(i,v,r,allh,t) "--MW-- second storage capacity constraint in addition to eq_capacity_limit" - eq_storage_duration(i,v,r,allh,t) "--MWh-- limit STORAGE_LEVEL based on hours of storage available" - eq_storage_in_cap(i,v,r,allh,t) "--MW-- storage_in must be less than a given fraction of power output capacity" - eq_storage_in_minloading(i,v,r,allh,allhh,t) "--MW-- minimum level for storage_in across same-season hours" - eq_storage_level(i,v,r,allh,t) "--MWh-- storage level inventory balance from one time-slice to the next" - eq_storage_interday_level_max_day(i,v,r,allszn,allh,t) "--MWh-- define the maximum relative SOC on a representative period compared to hour 0 of the rep period" - eq_storage_interday_level_min_day(i,v,r,allszn,allh,t) "--MWh-- define the minimum relative SOC on a representative period compared to hour 0 of the rep period" - eq_storage_interday_min_level_start(i,v,r,allszn,t) "--MWh-- enforce minimun SOC at first period of each partition" - eq_storage_interday_min_level_end(i,v,r,allszn,t) "--MWh-- enforce minimun SOC at last period of each partition" - eq_storage_interday_level(i,v,r,allszn,t) "--MWh-- calculate SOC of each partition at its hour 0" - eq_storage_interday_max_level_start(i,v,r,allszn,t) "--MWh-- enforce maximum SOC at first period of each partition" - eq_storage_interday_max_level_end(i,v,r,allszn,t) "--MWh-- enforce maximum SOC at last period of each partition" - eq_storage_opres(i,v,r,allh,t) "--MWh-- there must be sufficient energy in the storage to be able to provide operating reserves" - eq_storage_thermalres(i,v,r,allh,t) "--MW-- thermal storage contribution to operating reserves is store_in only" - eq_battery_minduration(i,v,r,t) "--MWh-- when power capacity is built, energy capacity should have a minimum capacity" - -* hybrid plant equations - eq_plant_total_gen(i,v,r,allh,t) "--MW-- generation post curtailment = generation from pv (post curtailment) + generation from battery - charging from PV" - eq_hybrid_plant_energy_limit(i,v,r,allh,t) "--MW-- PV energy to storage (no curtailment recovery) + PV energy to inverter <= PV resource" - eq_plant_capacity_limit(i,v,r,allh,t) "--MW-- energy moving through the inverter cannot exceed the inverter capacity" - eq_pvb_itc_charge_reqt(i,v,r,t) "--MWh-- total energy charged from local PV >= ITC qualification fraction * total energy charged" - -* Canadian imports balance - eq_Canadian_Imports(r,allszn,t) "--MWh-- Balance of Canadian imports by season" - -* water usage accounting - eq_water_accounting(i,v,w,r,allh,t) "--Mgal-- water usage accounting" - eq_water_capacity_total(i,v,r,t) "--Mgal-- specify required water access based on generation capacity and water use rate" - eq_water_capacity_limit(wst,r,t) "--Mgal/yr-- total water access must not exceed supply by region and water type" - eq_water_use_limit(i,v,w,r,allszn,t) "--Mgal/yr-- water use must not exceed available access" - -* flexible CCS constraints - eq_ccsflex_byp_ccsenergy_limit(i,v,r,allh,t) "--avg MW-- Limit the CCS power for a bypass system in each time-slice" - eq_ccsflex_sto_ccsenergy_limit_szn(i,v,r,allszn,t) "--MWh-- Limit the CCS power for a storage system across a characteristic day" - eq_ccsflex_sto_ccsenergy_balance(i,v,r,allszn,t) "--MWh-- Total CCS energy requirement can be distributed across a characteristic day" - eq_ccsflex_sto_storage_level(i,v,r,allh,t) "--varies-- Track the level of the CCS storage balance for each time-slice" - eq_ccsflex_sto_storage_level_max(i,v,r,allh,t) "--varies-- Limit the level of the CCS storage system" -; - -*========================== -* --- LOAD CONSTRAINTS --- -*========================== - -* --------------------------------------------------------------------------- - -*the marginal off of this constraint allows you to -*determine the full price of electricity load -*i.e. the price of load with consideration to operating -*reserve and planning reserve margin considered -eq_loadcon(r,h,t)$tmodel(t).. - - LOAD(r,h,t) - - =e= - -*[plus] the static, exogenous load - + load_exog_static(r,h,t) - -*[plus] exogenously defined exports to Canada -* note net canadian load (when Sw_Canada = 2) is included -* in eq_supply_demand since LOAD needs to stay positive -* while net_trade can be negative and cause infeasibilities - + can_exports_h(r,h,t)$[Sw_Canada=1] - -*[plus] load from EV charging (baseline/unmanaged) - + evmc_baseline_load(r,h,t)$Sw_EVMC - -*[plus] shifted load from adopted EVMC shape resources - + sum{(i,v)$[evmc_shape(i)$valcap(i,v,r,t)], evmc_shape_load(i,r,h) * CAP(i,v,r,t)} - -*[plus] load shifted from other timeslices - + sum{flex_type, FLEX(flex_type,r,h,t) }$Sw_EFS_flex - -*[plus] Load created by production activities - only tracked during representative hours -* [metric tons/hour] / [metric tons/MWh] = [MW] - + sum{(p,i,v)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)$(not sameas(i,"dac_gas"))], - PRODUCE(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) }$[Sw_Prod$h_rep(h)] - -*[plus] load for compressors associated with hydrogen storage injections or withdrawals -* metric tons/hour * MWh/metric tons = MW - + sum{h2_stor$h2_stor_r(h2_stor,r), - h2_network_load(h2_stor,t) - * ( H2_STOR_IN(h2_stor,r,h,t) + H2_STOR_OUT(h2_stor,r,h,t) ) }$Sw_H2_CompressorLoad - -* [plus] load for hydrogen pipeline compressors -* metric tons/hour * MWh/metric tons = MW - + sum{rr$h2_routes(r,rr), - h2_network_load("h2_compressor",t) - * (( H2_FLOW(r,rr,h,t) + H2_FLOW(rr,r,h,t) ) / 2) }$Sw_H2_CompressorLoad - -* Operations of flexibly sited load: -* - OP_LOADSITE is used when 0 < Sw_LoadSiteCF < 1 -* - CAP_LOADSITE is used when Sw_LoadSiteCF = 1 because OP_LOADSITE = CAP_LOADSITE -* (the effect is the same but avoiding the h-indexed OP_LOADSITE reduces solve time) - + OP_LOADSITE(r,h,t)$[Sw_LoadSiteCF$(Sw_LoadSiteCF<1)$val_loadsite(r)] - + CAP_LOADSITE(r,t)$[(Sw_LoadSiteCF=1)$val_loadsite(r)] -; - -* --------------------------------------------------------------------------- - -*====================================== -* --- LOAD FLEXIBILITY CONSTRAINTS --- -*====================================== -* CAP_LOADSITE accumulates INV_LOADSITE -eq_loadsite_inv(r,t) - $[tmodel(t) - $Sw_LoadSiteCF - $val_loadsite(r) - $(not Sw_PCM)].. - - CAP_LOADSITE(r,t) - - =e= - - + sum{(tt)$[(yeart(tt) <= yeart(t))$(tmodel(tt) or tfix(tt))], - INV_LOADSITE(r,tt) } -; - -* --------------------------------------------------------------------------- -* Realized load from optimally sited load must be less than optimally sited load capacity -eq_loadsite_cap(r,h,t) - $[tmodel(t) - $Sw_LoadSiteCF - $(Sw_LoadSiteCF<1) - $val_loadsite(r)].. - - CAP_LOADSITE(r,t) - - =g= - - OP_LOADSITE(r,h,t) -; - -* --------------------------------------------------------------------------- -* Realized load from optimally sited load must sum to Sw_LoadSiteCF -eq_loadsite_op(r,t) - $[tmodel(t) - $Sw_LoadSiteCF - $(Sw_LoadSiteCF<1) - $val_loadsite(r)].. - - sum{h, OP_LOADSITE(r,h,t) * hours(h) } - - =g= - - CAP_LOADSITE(r,t) * sum{h, hours(h) } * Sw_LoadSiteCF -; - -* --------------------------------------------------------------------------- -* Optimally sited load capacity must sum to loadsite_annual -eq_loadsite_siting(loadsitereg,t) - $[tmodel(t) - $Sw_LoadSiteCF - $(not Sw_PCM)].. - - sum{r$[r_loadsitereg(r,loadsitereg)$val_loadsite(r)], CAP_LOADSITE(r,t) } - - =e= - - loadsite_annual(loadsitereg,t) -; - -* --------------------------------------------------------------------------- - -*The following 3 equations apply to the flexibility of load in ReEDS, originally developed -*as part of the EFS study in ReEDS heritage and adapted for ReEDS-2.0 here. - -* Additional work has been done to represent flexible load as a generator + storage -* with boundaries on how many timeslices this generator may shift. See equations -* in the DR CONSTRAINTS section for that representation - -* FLEX load in each season equals the total exogenously-specified flexible load in each season -eq_load_flex_day(flex_type,r,szn,t)$[tmodel(t)$Sw_EFS_flex].. - - sum{h$h_szn(h,szn), FLEX(flex_type,r,h,t) * hours(h) } / numdays(szn) - - =e= - - sum{h$h_szn(h,szn), load_exog_flex(flex_type,r,h,t) * hours(h) } / numdays(szn) -; - -* --------------------------------------------------------------------------- - -* for the "previous" flex type: the amount of exogenously-specified load in timeslice "h" -* must be served by FLEX load either in the timeslice h or the timeslice PRECEEDING h -* -* for the "next" flex type: the amount of exogenously-specified load in timeslice "h" -* must be served by FLEX load either in the timeslice h or the timeslice FOLLOWING h -* -* for the "adjacent" flex type: the amount of exogenously-specified load in timeslice "h" -* must be served by FLEX load either in the timeslice h or a timeslice ADJACENT to h - -eq_load_flex1(flex_type,r,h,t)$[tmodel(t)$Sw_EFS_flex].. - - FLEX(flex_type,r,h,t) * hours(h) - - + sum{hh$flex_h_corr1(flex_type,h,hh), FLEX(flex_type,r,hh,t) * hours(hh) } - - =g= - - load_exog_flex(flex_type,r,h,t) * hours(h) -; - -* --------------------------------------------------------------------------- - -* for the "previous" flex type: FLEX load in timeslice "h" cannot exceed the sum of -* exogenously-specified load in timeslice h and the timeslice following h -* -* for the "next" flex type: FLEX load in timeslice "h" cannot exceed the sum of -* exogenously-specified load in timeslice h and the timeslice preceeding h -* -* for the "adjacent" flex type: FLEX load in timeslice "h" cannot exceed the sum of -* exogenously-specified load in timeslice h and the timeslices adjacent to h - -eq_load_flex2(flex_type,r,h,t)$[tmodel(t)$Sw_EFS_flex].. - - load_exog_flex(flex_type,r,h,t) * hours(h) - - + sum{hh$flex_h_corr2(flex_type,h,hh), load_exog_flex(flex_type,r,hh,t) * hours(hh) } - - =g= - - FLEX(flex_type,r,h,t) * hours(h) -; - -* * --------------------------------------------------------------------------- -* This constraint and the associated PEAK_FLEX variable are not currently supported -* but are left in the model in case someone decides to revive them. -* eq_load_flex_peak(r,h,ccseason,t)$[tmodel(t)$Sw_EFS_flex].. -* * peak demand EFS flexibility adjustment is greater than -* PEAK_FLEX(r,ccseason,t)$h_ccseason(h,ccseason) - -* =g= - -* * the static peak in each timeslice -* peakdem_static_h(r,h,t)$h_ccseason(h,ccseason) - -* * PLUS the flexibile load served in each timeslice -* + sum{flex_type, FLEX(flex_type,r,h,t) }$h_ccseason(h,ccseason) - -* * MINUS the static peak demand in the season corresponding to each timeslice -* - peakdem_static_ccseason(r,ccseason,t)$h_ccseason(h,ccseason) -* ; - -* --------------------------------------------------------------------------- - -*========================================================= -* --- EQUATIONS RELATING CAPACITY ACROSS TIME PERIODS --- -*========================================================= - -*==================================== -* -- existing capacity equations -- -*==================================== - -$ontext - -The following six equations dictate how capacity is represented in the model. - -The first three equations handle init-X vintages (those that existed pre-2010) -which are bounded by m_capacity_exog. With retirements (in the second and third -equations), the constraints imply that capacity must be less than or -equal to m_capacity_exog and monotonically decreasing over time - -implying that if endogenous capacity was reduced in the previous year, -it cannot be brought back online. - -New capacity, handled in equations four through six, is the sum of previous -years' greenfield investments and refurbishments. The same logic is present -for retiring capacity, the only difference being that contemporaneous -investment can increase the present-period's capacity. - -Upgraded capacity reduces the total amount of capacity available to the -upgraded-from technology. For example, the model starts with 100MW of -coaloldscr (m_capacity_exog = 100) capacity then upgrades 10MW of that to -coaloldscr_coal-ccs capacity. The remaining amount of available coaloldscr -is thus 90 and coaloldscr_coal-ccs capacity is 10 but both are still less -than the 100 available. As time progresses and exogenous capacity declines, -the model chooses which units to take offline. - -$offtext - -* --------------------------------------------------------------------------- - -eq_cap_init_noret(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$initv(v)$(not upgrade(i)) - $(not retiretech(i,v,r,t))$(not Sw_PCM)].. - - m_capacity_exog(i,v,r,t) - -* Account for capacity upsizing within init vintages - + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } - - =e= - - CAP(i,v,r,t) - - + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,tt) - - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - -* include contemporaneous upgrades when they are intended to -* persist as new bintages with sw_upgrades = 2 - + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], - UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] - - + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,t) }$[Sw_Upgrades = 2] -; - -* --------------------------------------------------------------------------- - -eq_cap_init_retub(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$initv(v)$(not upgrade(i)) - $retiretech(i,v,r,t)$(not Sw_PCM)].. - - m_capacity_exog(i,v,r,t) - -* Account for capacity upsizing within init vintages - + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } - - =g= - - CAP(i,v,r,t) - - + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,tt) - - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - -* include contemporaneous upgrades when they are intended to -* persist as new bintages with sw_upgrades = 2 - + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], - UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] - - + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,t) }$[Sw_Upgrades = 2] - -; - -* --------------------------------------------------------------------------- - -eq_cap_init_retmo(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$initv(v)$(not upgrade(i)) - $retiretech(i,v,r,t)$(not Sw_PCM)].. - - sum{tt$[tprev(t,tt)$valcap(i,v,r,tt)], - - CAP(i,v,r,tt) - - + sum{(ii,ttt)$[(tfix(ttt) or tmodel(ttt))$(yeart(ttt)<=yeart(tt)) - $valcap(ii,v,r,ttt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,ttt) - - (UPGRADES_RETIRE(ii,v,r,ttt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - - + sum{ii$[valcap(ii,v,r,tt)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,tt) }$[Sw_Upgrades = 2] - } - -* Account for capacity upsizing within init vintages - + sum{rscbin$allow_cap_up(i,v,r,rscbin,t), INV_CAP_UP(i,v,r,rscbin,t) } - - =g= - - CAP(i,v,r,t) - - + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,tt) - - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - -* include contemporaneous upgrades when they are intended to -* persist as new bintages with sw_upgrades = 2 - + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], - UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] - - + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,t) }$[Sw_Upgrades = 2] -; - -* --------------------------------------------------------------------------- - -*============================== -* -- new capacity equations -- -*============================== - -* --------------------------------------------------------------------------- - -eq_cap_new_noret(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$(not upgrade(i)) - $(not retiretech(i,v,r,t))$(not Sw_PCM)].. - - sum{tt$[inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))$valcap(i,v,r,tt)], - degrade(i,tt,t) * (INV(i,v,r,tt) + INV_REFURB(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]) - } - -* Account for capacity upsizing within new vintages - + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } - - =e= - - CAP(i,v,r,t) - - + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,tt) - - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - -* include contemporaneous upgrades when they are intended to -* persist as new bintages with sw_upgrades = 2 - + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], - UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] - - + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,t) }$[Sw_Upgrades = 2] - -; - -* --------------------------------------------------------------------------- - -eq_cap_energy_new_noret(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$battery(i)$(not Sw_PCM)].. - - sum{tt$[inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))$valcap(i,v,r,tt)], - degrade(i,tt,t) * INV_ENERGY(i,v,r,tt) - } - - + m_capacity_exog_energy(i,v,r,t) - - =e= - - CAP_ENERGY(i,v,r,t) - -; - -* --------------------------------------------------------------------------- - -eq_cap_new_retub(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$(not upgrade(i)) - $retiretech(i,v,r,t)$(not Sw_PCM)].. - - sum{tt$[inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))$valcap(i,v,r,tt)], - degrade(i,tt,t) * (INV(i,v,r,tt) + INV_REFURB(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]) - } - -* Account for capacity upsizing within new vintages - + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))$allow_cap_up(i,v,r,rscbin,tt)], - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt) } - - =g= - - CAP(i,v,r,t) - - + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,tt) - - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - -* include contemporaneous upgrades when they are intended to -* persist as new bintages with sw_upgrades = 2 - + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], - UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] - - + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,t) }$[Sw_Upgrades = 2] -; - -* --------------------------------------------------------------------------- - -eq_cap_new_retmo(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$(not upgrade(i)) - $retiretech(i,v,r,t)$(not Sw_PCM)].. - - sum{tt$[tprev(t,tt)$valcap(i,v,r,tt)], - degrade(i,tt,t) * CAP(i,v,r,tt) - - + sum{(ii,ttt)$[(tfix(ttt) or tmodel(ttt))$(yeart(ttt)<=yeart(tt)) - $valcap(ii,v,r,ttt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,ttt) - - (UPGRADES_RETIRE(ii,v,r,ttt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - - + sum{ii$[valcap(ii,v,r,tt)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,tt) }$[Sw_Upgrades = 2] - - } - - + INV(i,v,r,t)$valinv(i,v,r,t) - - + INV_REFURB(i,v,r,t)$[valinv(i,v,r,t)$refurbtech(i)$Sw_Refurb] - -* Account for capacity upsizing within new vintages - + sum{rscbin$allow_cap_up(i,v,r,rscbin,t), INV_CAP_UP(i,v,r,rscbin,t) } - - =g= - - CAP(i,v,r,t) - - + sum{(ii,tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t)) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], - UPGRADES(ii,v,r,tt) - - (UPGRADES_RETIRE(ii,v,r,tt)$[not noret_upgrade_tech(ii)])} - $[(Sw_Upgrades = 1)] - -* include contemporaneous upgrades when they are intended to -* persist as new bintages with sw_upgrades = 2 - + sum{(ii)$[upgrade_from(ii,i)$valcap(ii,v,r,t)$(not sameas(ii,'hydEND_hydED'))], - UPGRADES(ii,v,r,t) }$[Sw_Upgrades = 2] - - + sum{ii$[valcap(ii,v,r,t)$upgrade_from(ii,i)$sameas(ii,'hydEND_hydED')], - CAP(ii,v,r,t) }$[Sw_Upgrades = 2] -; - -* --------------------------------------------------------------------------- -* Capacity accounting for rsc techs -eq_cap_rsc(i,v,r,rscbin,t) - $[tmodel(t) - $rsc_i(i)$(not sccapcosttech(i)) - $valcap(i,v,r,t) - $(not Sw_PCM)].. - - sum{tt$[tfirst(tt)$exog_rsc(i)], - capacity_exog_rsc(i,v,r,rscbin,tt) } - - + sum{tt$[(yeart(tt) <= yeart(t))$(tmodel(tt) or tfix(tt)) - $m_rscfeas(r,i,rscbin) - $valinv(i,v,r,tt)], - INV_RSC(i,v,r,rscbin,tt) - } - - =e= - - CAP_RSC(i,v,r,rscbin,t) -; - -* --------------------------------------------------------------------------- - -eq_cap_upgrade(i,v,r,t)$[valcap(i,v,r,t)$upgrade(i)$Sw_Upgrades$tmodel(t)$(not Sw_PCM)].. - -* without peristent upgrades, all upgrades correspond to their original bintage - sum{(tt)$[(tfix(tt) or tmodel(tt)) - $(yeart(tt)<=yeart(t)) - $(yeart(tt)>=Sw_Upgradeyear) - $valcap(i,v,r,tt) - $sum{ii$upgrade_from(i,ii), valcap(ii,v,r,tt) }], - UPGRADES(i,v,r,tt) * (1 - upgrade_derate(i,v,r,tt)) - }$[(Sw_Upgrades = 1)$(not coal(i))] - -* coal cannot upgrade after the retire year - ie no mothballing - + sum{(tt)$[(tfix(tt) or tmodel(tt)) - $(yeart(tt)<=yeart(t)) - $(yeart(tt)>=Sw_Upgradeyear) - $valcap(i,v,r,tt) - $sum{ii$upgrade_from(i,ii), valcap(ii,v,r,tt) } - $(yeart(tt)<=caa_coal_retire_year)], - UPGRADES(i,v,r,tt) * (1 - upgrade_derate(i,v,r,tt)) - }$[(Sw_Upgrades = 1)$coal(i)] - -* all previous years upgrades converted to new bintages of the present year -* NOTE: the 'v' in ivt(i,v,tt) here is an important distinction - -* although we're summing over 'vv' we still only want upgrades -* to be included for the upgrade tech's vintage combination - + sum{(tt,vv)$[(tfix(tt) or tmodel(tt))$(initv(vv) or sameas(v,vv)) - $(yeart(tt)<=yeart(t))$ivt(i,v,tt) - $(yeart(tt)>=Sw_Upgradeyear) - $valcap(i,v,r,tt)$(not sameas(i,'hydEND_hydED')) - $sum{ii$upgrade_from(i,ii), valcap(ii,vv,r,tt) }], - UPGRADES(i,vv,r,tt) * (1 - upgrade_derate(i,vv,r,tt)) - }$[Sw_Upgrades = 2] - - + sum{(tt)$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))$(yeart(tt)>=Sw_Upgradeyear) - $valcap(i,v,r,tt)$sameas(i,'hydEND_hydED')], - UPGRADES(i,v,r,tt) * (1 - upgrade_derate(i,v,r,tt)) - }$[Sw_Upgrades = 2] - - =e= - - CAP(i,v,r,t) - -* note this is equivalent to the previous version that had a =g= -* sign in the corrolary equation - + sum{tt$[(tfix(tt) or tmodel(tt))$valcap(i,v,r,tt)], - UPGRADES_RETIRE(i,v,r,tt)}$[not noret_upgrade_tech(i)] -; - -* --------------------------------------------------------------------------- - -* Capacity upsizing limit -* This uses rscbin to constrain hydropower upsizing using supply curve data. -eq_cap_up(i,v,r,rscbin,t)$[tmodel(t)$allow_cap_up(i,v,r,rscbin,t)$(not Sw_PCM)].. - - cap_cap_up(i,v,r,rscbin,t) - - =g= - - sum{tt$[(tmodel(tt) or tfix(tt))], INV_CAP_UP(i,v,r,rscbin,tt) } -; - -* --------------------------------------------------------------------------- - -*Energy upsizing limit -* This uses rscbin to constrain hydropower upsizing using supply curve data. -eq_ener_up(i,v,r,rscbin,t)$[tmodel(t)$allow_ener_up(i,v,r,rscbin,t)$(not Sw_PCM)].. - - cap_ener_up(i,v,r,rscbin,t) - - =g= - - sum{tt$[(tmodel(tt) or tfix(tt))], INV_ENER_UP(i,v,r,rscbin,tt) } -; - -* --------------------------------------------------------------------------- - -* Prescribe power capacity -eq_forceprescription_power(pcat,r,t) - $[tmodel(t)$force_pcat(pcat,t)$Sw_ForcePrescription - $sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,t) } - $(not Sw_PCM)].. - -*capacity built in the current period or prior - sum{(i,newv,tt)$[valinv(i,newv,r,tt)$prescriptivelink(pcat,i) - $(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], - INV(i,newv,r,tt) + INV_REFURB(i,newv,r,tt)$[refurbtech(i)$Sw_Refurb]} - - =e= - -*must equal the cumulative prescribed amount - sum{tt$[(yeart(tt)<=yeart(t)) - $(tmodel(tt) or tfix(tt))], - noncumulative_prescriptions(pcat,r,tt)} - -* plus any extra power buildouts (no penalty here - used as free slack) -* only on or after the first year the techs are available - + EXTRA_PRESCRIP(pcat,r,t)$[yeart(t)>=firstyear_pcat(pcat)] - -* or in regions where there is a offshore wind requirement - + EXTRA_PRESCRIP(pcat,r,t)$[r_offshore(r,t)$sameas(pcat,'wind-ofs') - $(yeart(t)>=firstyear_RPS) - $sum{st$r_st(r,st), offshore_cap_req(st,t) }] -; - -* --------------------------------------------------------------------------- - -* Prescribe energy capacity -eq_forceprescription_energy(pcat,r,t) - $[tmodel(t)$force_pcat(pcat,t)$Sw_ForcePrescription - $sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,t) } - $(not Sw_PCM)].. - -*energy capacity built in the current period or prior - sum{(i,newv,tt)$[valinv(i,newv,r,tt)$prescriptivelink(pcat,i) - $(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt)) - $battery(i)], - INV_ENERGY(i,newv,r,tt)} - - =e= - -*must equal the cumulative prescribed energy amount - sum{tt$[(yeart(tt)<=yeart(t)) - $(tmodel(tt) or tfix(tt))], - noncumulative_prescriptions_energy(pcat,r,tt)} - -* plus any extra energy buildouts (no penalty here - used as free slack) -* only on or after the first year the techs are available - + EXTRA_PRESCRIP_ENERGY(pcat,r,t)$[yeart(t)>=firstyear_pcat(pcat)] -; - -* --------------------------------------------------------------------------- - -*limit the amount of refurbishments available in specific year -*this is the sum of all previous year's investment that is now beyond the age -*limit (i.e. it has exited service) plus the amount of retired exogenous capacity -*that we begin with -eq_refurblim(i,r,t)$[tmodel(t)$refurbtech(i)$Sw_Refurb$(not Sw_PCM)].. - -*investments that meet the refurbishment requirement (i.e. they've expired) - sum{(vv,tt)$[m_refurb_cond(i,vv,r,t,tt)$(tmodel(tt) or tfix(tt))$valinv(i,vv,r,tt)], - INV(i,vv,r,tt) } - -*[plus] exogenous decay in capacity -*note here that the tfix or tmodel set does not apply -*since we'd want capital that expires in off-years to -*be included in this calculation as well - + sum{(v,tt)$[yeart(tt)<=yeart(t)], - avail_retire_exog_rsc(i,v,r,tt) } - - =g= - -*must exceed the total sum of investments in refurbishments -*that have yet to expire - implying an investment can be refurbished more than once -*if the first refurbishment has exceed its age limit - sum{(vv,tt)$[inv_cond(i,vv,r,t,tt)$(tmodel(tt) or tfix(tt))$valinv(i,vv,r,tt)], - INV_REFURB(i,vv,r,tt) - } -; - -* --------------------------------------------------------------------------- - -eq_rsc_inv_account(i,v,r,t)$[tmodel(t)$valinv(i,v,r,t)$rsc_i(i)$(not Sw_PCM)].. - - sum{rscbin$m_rscfeas(r,i,rscbin), INV_RSC(i,v,r,rscbin,t) } - - =e= - - INV(i,v,r,t) -; - -* --------------------------------------------------------------------------- - -*note that the following equation only restricts inv_rsc and not inv_refurb -*therefore, the capacity indicated by the supply curve may be limiting -*but the plant can still be refurbished -*Also note the flag_eq_rsc_INVlim--its calculation needs to be updated if this equation -*is changed -eq_rsc_INVlim(r,i,rscbin,t)$[tmodel(t) - $rsc_i(i) - $m_rscfeas(r,i,rscbin) - $m_rsc_con(r,i) - $(not flag_eq_rsc_INVlim(r,i,rscbin,t)) - $(not Sw_PCM)].. -*With water constraints, some RSC techs are expanded to include cooling technologies -*but the combination of m_rsc_con and rsc_agg allows for those investments -*to be limited by the numeraire techs' m_rsc_dat - -*capacity indicated by the resource supply curve (scaled by rsc_capacity_scalar) - m_rsc_dat(r,i,rscbin,"cap")$[not evmc(i)] * ( - 1$[not rsc_capacity_scalar_i(i)] + rsc_capacity_scalar(i,r,t)$rsc_capacity_scalar_i(i)) -* available hydro upgrade capacity - + hyd_add_upg_cap(r,i,rscbin,t)$(Sw_HydroCapEnerUpgradeType=1) - - =g= - -*must exceed the cumulative invested capacity in that region/class/bin... - sum{(ii,v,tt)$[valinv(ii,v,r,tt)$(yeart(tt) <= yeart(t))$rsc_agg(i,ii)], - INV_RSC(ii,v,r,rscbin,tt) * resourcescaler(ii) } - -*plus exogenous (pre-start-year) capacity, using its level in the first year (tfirst) - + sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], - capacity_exog_rsc(ii,v,r,rscbin,tt) } - -; - -* --------------------------------------------------------------------------- - -eq_growthlimit_relative(i,st,t)$[sum{r$[r_st(r,st)], valinv_irt(i,r,t) } - $tmodel(t) - $stfeas(st) - $Sw_GrowthPenalties - $(yeart(t)<=Sw_GrowthPenLastYear) - $(yeart(t)>=model_builds_start_yr) - $(not Sw_PCM)].. - -*the annual growth limit - (yeart(t) - sum{tt$[tprev(t,tt)], yeart(tt) }) - * sum{gbin, GROWTH_BIN(gbin,i,st,t) } - - =g= - -* must exceed the current periods investment - sum{(v,r)$[valinv(i,v,r,t)$r_st(r,st)], - INV(i,v,r,t) } -; - -* --------------------------------------------------------------------------- - -eq_growthbin_limit(gbin,st,tg,t)$[valinv_tg(st,tg,t) - $tmodel(t) - $stfeas(st) - $Sw_GrowthPenalties - $(yeart(t)<=Sw_GrowthPenLastYear) - $(yeart(t)>=model_builds_start_yr) - $(not Sw_PCM)].. - -*the growth bin limit - growth_bin_limit(gbin,st,tg,t) - - =g= - -* must exceed the value in the growth bin - sum{i$tg_i(tg,i), GROWTH_BIN(gbin,i,st,t) } -; - -* --------------------------------------------------------------------------- - -eq_growthlimit_absolute(tg,t)$[growth_limit_absolute(tg)$tmodel(t) - $Sw_GrowthAbsCon$(yeart(t)<=Sw_GrowthConLastYear) - $(yeart(t)>=model_builds_start_yr) - $(not Sw_PCM)].. - -* the absolute limit of growth (in MW) - (sum{tt$[tprev(tt,t)], yeart(tt) } - yeart(t)) - * growth_limit_absolute(tg) - - =g= - -* must exceed the total investment - sum{(i,v,r)$[valinv(i,v,r,t)$tg_i(tg,i)], - INV(i,v,r,t) } -; - -* --------------------------------------------------------------------------- -* If using hybrid generators with endogenous spur lines, the available power from -* a reV site is limited by the CF and capacity of constituent resources at that site -eq_site_cf(x,h,t) - $[tmodel(t) - $Sw_SpurScen - $xfeas(x)].. - - sum{(i,v,r) - $[spur_techs(i) - $x_r(x,r) - $valgen(i,v,r,t)], -* Capacity factor of techs with endogenously-modeled spur lines - m_cf(i,v,r,h,t) -* multiplied by total capacity of those techs - * sum{rscbin - $[valcap(i,v,r,t) - $m_rscfeas(r,i,rscbin) - $spurline_sitemap(i,r,rscbin,x)], - CAP_RSC(i,v,r,rscbin,t) - } - } - - =g= - - AVAIL_SITE(x,h,t) -; - -* --------------------------------------------------------------------------- -* If using hybrid generators with endogenous spur lines, each wind and solar generator -* is associated with a specific reV site x. The available generation from all generators -* at site x is limited to the spur-line capacity built to site x. -eq_spurclip(x,h,t) - $[Sw_SpurScen - $xfeas(x) - $tmodel(t)].. - -* Capacity of spur line to reV site limits the available generation at that site - CAP_SPUR(x,t) - - =g= - - AVAIL_SITE(x,h,t) -; - -* --------------------------------------------------------------------------- -* If spur-line sharing is disabled, the capacity of the spur line for site x -* must be >= the capacity of the hybrid resources (wind and solar) installed at site x -eq_spur_noclip(x,t) - $[Sw_SpurScen - $(not Sw_SpurShare) - $xfeas(x) - $tmodel(t)].. - -* Capacity of spur line to site x - CAP_SPUR(x,t) - - =g= - -* must be >= to the wind/solar capacity installed at x -* (Since PV capacity is in DC, we divide CAP_RSC [DC] by ILR [DC/AC] to get AC spur line capacity. -* ILR is 1 for all non-PV techs.) - sum{(i,v,r,rscbin) - $[spurline_sitemap(i,r,rscbin,x) - $valcap(i,v,r,t)], - CAP_RSC(i,v,r,rscbin,t) / ilr(i) - } -; - -* --------------------------------------------------------------------------- - -*capacity must be greater than supply -*dispatchable hydro is accounted for both in this constraint and in eq_dhyd_dispatch -*this constraint does not apply to storage nor hybrid plant -* limits for storage (including storage of hybrid plants) are tracked in eq_storage_capacity -* limits for plant of Hybrid Plant are tracked in eq_plant_energy_balance -* limits for hybrid techs with shared spur lines are treated in eq_capacity_limit_hybrid -eq_capacity_limit(i,v,r,h,t) - $[tmodel(t)$valgen(i,v,r,t) - $(not spur_techs(i)) - $(not storage_standalone(i))$(not storage_hybrid(i)$(not csp(i)))$(not nondispatch(i))].. - -*total amount of dispatchable, non-hydro capacity - avail(i,r,h)$[dispatchtech(i)$(not hydro_d(i))] - * derate_geo_vintage(i,v) - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - * CAP(i,v,r,t) - -*total amount of dispatchable hydro capacity - + avail(i,r,h)$hydro_d(i) - * CAP(i,v,r,t) - * sum{szn$h_szn(h,szn), cap_hyd_szn_adj(i,szn,r) } - * (1 + hydro_capcredit_delta(i,t)$[h_stress(h)]) - -*sum of non-dispatchable capacity multiplied by its rated capacity factor, -*only vre technologies are curtailable. -* This term accounts for energy-only and capacity-only upsizing, -* which is initially implemented only for hydro. - + (m_cf(i,v,r,h,t) - * (CAP(i,v,r,t) -*add energy embedded in energy-only upsizing - + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))], - INV_ENER_UP(i,v,r,rscbin,tt)$allow_ener_up(i,v,r,rscbin,tt) -*subtract energy that would be embedded in a capacity-only upsizing - - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt)$allow_cap_up(i,v,r,rscbin,tt) }) - )$[not dispatchtech(i)] -*add EVMC shape generation - + (evmc_shape_gen(i,r,h) * CAP(i,v,r,t)) - - =g= - -*must exceed generation - GEN(i,v,r,h,t) - -*[plus] sum of operating reserves by type - + sum{ortype$[Sw_OpRes$reserve_frac(i,ortype)$opres_h(h)$opres_model(ortype)], - OPRES(ortype,i,v,r,h,t) } - -*[plus] power consumed for flexible ccs - + CCSFLEX_POW(i,v,r,h,t) $[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)] -; - -* --------------------------------------------------------------------------- -* For hybrid resources, the sum of generation from constituent resources is -* limited by the available generation at that site, which is defined in -* eq_site_cf and eq_spurclip. -eq_capacity_limit_hybrid(r,h,t) - $[tmodel(t) - $Sw_SpurScen].. - -* Sum of available generation across reV sites in BA - sum{x$[x_r(x,r)$xfeas(x)], AVAIL_SITE(x,h,t)} - - =g= - -* is >= the actual generation and operating reserves from all the hybrid resources in that BA - sum{(i,v) - $[spur_techs(i) - $valgen(i,v,r,t)], - GEN(i,v,r,h,t) - + sum{ortype$[Sw_OpRes$opres_model(ortype)$reserve_frac(i,ortype)$opres_h(h)], - OPRES(ortype,i,v,r,h,t)} - } -; - -* --------------------------------------------------------------------------- - -eq_capacity_limit_nd(i,v,r,h,t)$[tmodel(t)$valgen(i,v,r,t)$nondispatch(i)].. - -*sum of non-dispatchable capacity multiplied by its rated capacity factor, - + m_cf(i,v,r,h,t) * CAP(i,v,r,t) - - =e= - -*must be equal to generation - GEN(i,v,r,h,t) - -*[plus] sum of operating reserves by type - + sum{ortype$[Sw_OpRes$opres_model(ortype)$reserve_frac(i,ortype)$opres_h(h)], - OPRES(ortype,i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -eq_curt_gen_balance(r,h,t)$tmodel(t).. - -*total potential generation - sum{(i,v)$[valcap(i,v,r,t)$(vre(i) or storage_hybrid(i)$(not csp(i)))$(not nondispatch(i))], - m_cf(i,v,r,h,t) * CAP(i,v,r,t) } - -*[minus] curtailed generation - - CURT(r,h,t)$Sw_CurtMarket - - =g= - -*must exceed realized generation; exclude hybrid plants - sum{(i,v)$[valgen(i,v,r,t)$vre(i)$(not nondispatch(i))], GEN(i,v,r,h,t) } - -*[plus] realized generation from hybrid plant - + sum{(i,v)$[valgen(i,v,r,t)$storage_hybrid(i)$(not csp(i))$(not nondispatch(i))], GEN_PLANT(i,v,r,h,t) }$Sw_HybridPlant - -*[plus] sum of operating reserves by type - + sum{(ortype,i,v)$[Sw_OpRes$reserve_frac(i,ortype)$opres_h(h)$valgen(i,v,r,t)$vre(i)$(not nondispatch(i))$opres_model(ortype)], - OPRES(ortype,i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- -* Generation in each timeslice must be greater than mingen_fixed * available capacity -eq_mingen_fixed(i,v,r,h,t) - $[Sw_MingenFixed$tmodel(t)$mingen_fixed(i)$valgen(i,v,r,t) - $(yeart(t)>=Sw_StartMarkets)].. - - GEN(i,v,r,h,t) - - =g= - - mingen_fixed(i) * avail(i,r,h) * CAP(i,v,r,t) -; - -* --------------------------------------------------------------------------- - -eq_mingen_lb(r,h,szn,t)$[h_szn(h,szn)$(yeart(t)>=this_year) - $tmodel(t)$Sw_Mingen].. - -*minimum generation level in a season - MINGEN(r,szn,t) - - =g= - -*must be greater than the minimum generation level in each time slice in that season - sum{(i,v)$[valgen(i,v,r,t)$minloadfrac(r,i,h)], GEN(i,v,r,h,t) * minloadfrac(r,i,h) } -; - -* --------------------------------------------------------------------------- - -eq_mingen_ub(r,h,szn,t)$[h_szn(h,szn)$(yeart(t)>=this_year) - $tmodel(t)$Sw_Mingen].. - -*generation in each timeslice in a season - sum{(i,v)$[valgen(i,v,r,t)$minloadfrac(r,i,h)], GEN(i,v,r,h,t) } - - =g= - -*must be greater than the minimum generation level - MINGEN(r,szn,t) -; - -* --------------------------------------------------------------------------- - -*requirement for fleet of a given tech to have a minimum annual capacity factor -eq_min_cf(i,r,t)$[minCF(i,t)$tmodel(t)$valgen_irt(i,r,t)$Sw_MinCF].. - - sum{(v,h)$[valgen(i,v,r,t)$h_rep(h)], hours(h) * GEN(i,v,r,h,t) } - - =g= - - sum{v$valgen(i,v,r,t), CAP(i,v,r,t) } * sum{h$h_rep(h), hours(h) } * minCF(i,t) -; - -* Maximum allowed daily capacity factor -eq_max_daily_cf(i,r,szn,t)$[maxdailycf(i,t) - $sum{h$h_szn(h,szn), avail(i,r,h)} - $tmodel(t)$valgen_irt(i,r,t)$Sw_MaxDailyCF].. - - sum{v$valgen(i,v,r,t), CAP(i,v,r,t) } * sum{h$h_szn(h,szn), hours(h) * avail(i,r,h) } * maxdailycf(i,t) - - =g= - - sum{(v,h)$[valgen(i,v,r,t)$h_szn(h,szn)], hours(h) * GEN(i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -* Seasonal energy constraint for dispatchable hydropower -eq_dhyd_dispatch(i,v,r,szn,t) - $[tmodel(t)$hydro_d(i)$valgen(i,v,r,t) - ].. - -*seasonal hours [times] seasonal capacity factor [times] total hydro capacity [times] seasonal capacity adjustment - sum{h$[h_szn(h,szn)], hours(h) } - * (CAP(i,v,r,t) + sum{(tt,rscbin)$[(tmodel(tt) or tfix(tt))], - INV_ENER_UP(i,v,r,rscbin,tt)$allow_ener_up(i,v,r,rscbin,tt) - - degrade(i,tt,t) * INV_CAP_UP(i,v,r,rscbin,tt)$allow_cap_up(i,v,r,rscbin,tt) }) - * m_cf_szn(i,v,r,szn,t) - - =g= - -*total seasonal generation plus fraction of energy for regulation - sum{h$[h_szn(h,szn)], - hours(h) - * (GEN(i,v,r,h,t) - + reg_energy_frac * ( - OPRES("reg",i,v,r,h,t)$[Sw_OpRes=1] - + OPRES("combo",i,v,r,h,t)$[Sw_OpRes=2] - )$[opres_h(h)] - ) - } -; - -* --------------------------------------------------------------------------------------- -* Limit near-term capacity deployments by tech and region based on interconnection queues -eq_interconnection_queues(tg,r,t) - $[tmodel(t)$(yeart(t)>=model_builds_start_yr) - $(sum{(tgg,rr), cap_limit(tgg,rr,t)}) - $sum{(i,newv)$tg_i(tg,i), valinv(i,newv,r,t)} - $(not Sw_PCM)].. - -* the capacity limit from the interconnection queue data -* (with CAP_ABOVE_LIM as a slack variable to address infeasibilities) - cap_limit(tg,r,t) + CAP_ABOVE_LIM(tg,r,t) - - =g= - -* must be greater than the total capacity deployed since the -* start of the interconnection queue data - sum{(i,newv,tt)$[valinv(i,newv,r,tt)$tg_i(tg,i) - $(yeart(tt)>=interconnection_start) - $(tmodel(tt) or tfix(tt))], - INV(i,newv,r,tt) + INV_REFURB(i,newv,r,tt)$[refurbtech(i)$Sw_Refurb] } -; - -*=============================== -* --- SUPPLY DEMAND BALANCE --- -*=============================== - -* --------------------------------------------------------------------------- - -* The treatment of power flow along DC lines depends on the type of AC/DC converter used. -* LCC DC lines are single point-to-point lines connected to the AC grid on either end, and -* as such are treated like AC lines (with different costs/losses). -* VSC DC lines are part of a multi-terminal DC network; DC power can flow through a node -* without converting to AC and incurring DC/AC/DC losses. Power flow along VSC lines is -* therefore treated separately through the CONVERSION variable and eq_vsc_flow equation. -eq_supply_demand_balance(r,h,t)$tmodel(t).. - -* generation from all land-based sources, including storage discharge - sum{(i,v)$[valgen(i,v,r,t)$land(r)], GEN(i,v,r,h,t) } - -* [plus] net AC and LCC DC transmission with imports reduced by losses - + sum{(trtype,rr)$[routes(rr,r,trtype,t)$notvsc(trtype)], - (1-tranloss(rr,r,trtype)) * FLOW(rr,r,h,t,trtype) } - - sum{(trtype,rr)$[routes(r,rr,trtype,t)$notvsc(trtype)], - FLOW(r,rr,h,t,trtype) } - -* [plus] net AC/DC conversion through VSC converter stations -* Note that we only need "AC" in the CONVERSION variable (not LCC, B2B, etc) -* since all it does here is act as a catch-all for "not VSC" - + (CONVERSION(r,h,"VSC","AC",t) * converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] - - (CONVERSION(r,h,"AC","VSC",t) / converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] - -* [minus] storage charging; not hybrid+storage - - sum{(i,v)$[valcap(i,v,r,t)$(storage_standalone(i) or hyd_add_pump(i))], STORAGE_IN(i,v,r,h,t) } - -* [minus] energy into storage for hybrid+storage from grid - - sum{(i,v)$[valcap(i,v,r,t)$storage_hybrid(i)$(not csp(i))], STORAGE_IN_GRID(i,v,r,h,t) }$Sw_HybridPlant - -* [plus] dropped/excess load ONLY if before Sw_StartMarkets - + DROPPED(r,h,t)$[(yeart(t)=model_builds_start_yr) - $tmodel(t)$hour_szn_group(h,hh)$Sw_MinLoading].. - - GEN(i,v,r,h,t) - - =g= - - GEN(i,v,r,hh,t) * minloadfrac(r,i,hh) -; - -* RAMPUP is used in the calculation of startup/ramping costs -* Because RAMPUP has a positive cost, RAMPUP will always either be 0 -* when the RHS is negative, or will be exactly equal to the RHS when -* the RHS is positive. -eq_ramping(i,r,h,hh,t) - $[Sw_StartCost$tmodel(t)$startcost(i)$numhours_nexth(h,hh)$valgen_irt(i,r,t)].. - - RAMPUP(i,r,h,hh,t) - - =g= - - sum{v$valgen(i,v,r,t), GEN(i,v,r,hh,t) - GEN(i,v,r,h,t) } -; - -*======================================= -* --- OPERATING RESERVE CONSTRAINTS --- -*======================================= - -* --------------------------------------------------------------------------- - -*generation must occur at some point during the szn (i.e., day) -*in order to procure operating reserves from that resource -*ORPRES for storage is limited by the storage capacity per the constraint "eq_storage_capacity" -eq_ORCap_large_res_frac(ortype,i,v,r,h,t) - $[tmodel(t)$valgen(i,v,r,t)$Sw_OpRes$opres_model(ortype)$opres_h(h) - $(reserve_frac(i,ortype)>0.5)$(not storage_standalone(i))$(not hyd_add_pump(i))].. - -*the reserve_frac times... - reserve_frac(i,ortype) * ( -* the amount of committed capacity available for a season is assumed to be the amount -* of generation from the timeslice that has the highest demand - sum{(szn,hh)$[h_szn(h,szn)$maxload_szn(r,hh,t,szn)], - GEN(i,v,r,hh,t) }) - - =g= - -*note the reserve_frac applies to each opres by type - OPRES(ortype,i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - -*for plants with reserve_frac <= 0.5 (but nonzero), generation must occur during the timeslice -*in which reserves are provided -eq_ORCap_small_res_frac(ortype,i,v,r,h,t) - $[tmodel(t)$valgen(i,v,r,t)$Sw_OpRes$opres_model(ortype)$opres_h(h) - $(reserve_frac(i,ortype)<=0.5)$reserve_frac(i,ortype)].. - -*generation - GEN(i,v,r,h,t) - - =g= - -*must be greater than the operating reserves procured - OPRES(ortype,i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - -*operating reserves must meet the operating reserves requirement (by ortype) -eq_OpRes_requirement(ortype,r,h,t) - $[tmodel(t)$Sw_OpRes$opres_model(ortype)$opres_h(h)].. - -*operating reserves from technologies that can produce them (i.e. those w/ramp rates) - - sum{(i,v)$[valgen(i,v,r,t)$reserve_frac(i,ortype)], - OPRES(ortype,i,v,r,h,t) } - -*[plus] net transmission of operating reserves (while including losses for imports) - + sum{rr$opres_routes(rr,r,t), (1 - tranloss(rr,r,"AC")) * OPRES_FLOW(ortype,rr,r,h,t) } - - sum{rr$opres_routes(r,rr,t), OPRES_FLOW(ortype,r,rr,h,t) } - -*[plus] dropped load (operating reserves) ONLY if before Sw_StartMarkets - + DROPPED(r,h,t)$[(yeart(t)=model_builds_start_yr) - $Sw_PRM_CapCredit - $(not Sw_PCM)].. - -* forced_retire is used here because forced_retire capacity is removed from valgen -* but not valcap. It remains in valcap to allow for upgrades, but if it is not upgraged -* it should not be allowed to provide capacity to meet the reserve margin constraint. -* Because it can provide no services, it will either upgrade or retire. - -*[plus] sum of all non-rsc and non-storage capacity - + sum{(i,v)$[valcap(i,v,r,t)$(not vre(i))$(not hydro(i))$(not storage(i))$(not consume(i))$(not forced_retire(i,r,t))], - CAP(i,v,r,t) - * (1 + ccseason_cap_frac_delta(i,v,r,ccseason,t)) - } - -*[plus] firm capacity from existing VRE or CSP -*only used in sequential solve case (otherwise cc_old = 0) - + sum{i$[(vre(i) or csp(i) or pvb(i))$(not forced_retire(i,r,t))], - cc_old(i,r,ccseason,t) - } - -*[plus] marginal capacity credit of VRE and csp times new investment -*only used in sequential solve case (otherwise m_cc_mar = 0) -*Note: new distpv is included with cc_old - + sum{(i,v)$[(vre(i) or csp(i) or pvb(i))$valinv(i,v,r,t)$(not forced_retire(i,r,t))], - m_cc_mar(i,r,ccseason,t) * (INV(i,v,r,t) + INV_REFURB(i,v,r,t)$[refurbtech(i)$Sw_Refurb]) - } - -*[plus] firm capacity contribution from all binned storage capacity -*battery and pumped-hydro -*excludes hydro upgraded to add pumps - + sum{(i,v,sdbin)$[(storage_standalone(i) or hyd_add_pump(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], - cc_storage(i,sdbin) * CAP_SDBIN(i,v,r,ccseason,sdbin,t) - } -*hybrid PV+battery - + sum{(i,v,sdbin)$[storage_hybrid(i)$(not csp(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], - cc_storage(i,sdbin) * hybrid_cc_derate(i,r,ccseason,sdbin,t) * CAP_SDBIN(i,v,r,ccseason,sdbin,t) - } - -*[plus] average capacity credit times capacity of VRE and storage -*used in rolling window and full intertemporal solve (otherwise cc_int = 0) - + sum{(i,v)$[(vre(i) or storage(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], - cc_int(i,v,r,ccseason,t) * CAP(i,v,r,t) - } - -*[plus] excess capacity credit -*used in rolling window and full intertemporal solve when using marginals for cc_int (otherwise cc_excess = 0) - + sum{i$[(vre(i) or storage(i))$(not forced_retire(i,r,t))], - cc_excess(i,r,ccseason,t) - } - -*[plus] firm capacity of non-dispatchable hydro -* nb: hydro_nd generation does not fluctuate -* within a seasons set of hours - + sum{(i,v,h)$[hydro_nd(i)$valgen(i,v,r,t)$h_ccseason_prm(h,ccseason)], - GEN(i,v,r,h,t) - } - -*[plus] dispatchable hydro firm capacity -* include hydro upgraded to add pumps - + sum{(i,v)$[(hydro_d(i) or hyd_add_pump(i))$valcap(i,v,r,t)$(not forced_retire(i,r,t))], - CAP(i,v,r,t) * cap_hyd_ccseason_adj(i,ccseason,r) * (1 + hydro_capcredit_delta(i,t)) - } - -*[plus] imports of firm capacity through AC and LCC DC lines - + sum{(rr,trtype)$[routes(rr,r,trtype,t)$routes_prm(rr,r)$notvsc(trtype)], - (1 - tranloss(rr,r,trtype)) * PRMTRADE(rr,r,trtype,ccseason,t) - } - -*[minus] exports of firm capacity through AC and LCC DC lines - - sum{(rr,trtype)$[routes(r,rr,trtype,t)$routes_prm(r,rr)$notvsc(trtype)], - PRMTRADE(r,rr,trtype,ccseason,t) - } - -*[plus] net AC/DC conversion of firm capacity through VSC converters - + (CONVERSION_PRM(r,ccseason,"VSC","AC",t) * converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] - - (CONVERSION_PRM(r,ccseason,"AC","VSC",t) / converter_efficiency_vsc)$[Sw_VSC$val_converter(r,t)] - - =g= - -*[plus] the peak demand times the planning reserve margin - + ( - peakdem_static_ccseason(r,ccseason,t) - -* + PEAK_FLEX(r,ccseason,t)$Sw_EFS_flex - -* [plus] only steam methane reforming technologies are assumed to increase peak demand -* contribution to peak demand based on weighted-average across timeslices in each ccseason -* [metric tons/hour] / [metric tons/MWh] * [hours] / [hours] = [MW] - + (sum{(p,i,v,h)$[smr(i)$valcap(i,v,r,t)$frac_h_ccseason_weights(h,ccseason) - $(sameas(p,"H2"))$i_p(i,p)$(not sameas(i,"dac_gas"))$h_rep(h)], - PRODUCE(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) - * hours(h) * frac_h_ccseason_weights(h,ccseason) } - / sum{h$[frac_h_ccseason_weights(h,ccseason)$h_rep(h)], - hours(h) * frac_h_ccseason_weights(h,ccseason) } - )$Sw_Prod - - ) * (1 + prm(r,t)) -; - -* --------------------------------------------------------------------------- - -*================================ -* --- TRANSMISSION CAPACITY --- -*================================ - -* DC transmission additions are assumed to add the same capacity in both directions -eq_INVTRAN_DC(r,rr,trtype,t) - $[routes_inv(r,rr,trtype,t) - $(not aclike(trtype)) - $tmodel(t) - $(not Sw_PCM)].. - - INVTRAN(r,rr,trtype,t) - - =e= - - INVTRAN(rr,r,trtype,t) -; - -* --------------------------------------------------------------------------- -* Added AC transmission capacity (INVTRAN) is determined by the cumulative capex invested -* in the interface (TRAN_CAPEX_BINS). This backwards approach is used because investment -* in the interface (corresponding to a particular upgraded line or transformer) can add a different -* amount of MW to the forward and reverse directions, and because the bins must be filled -* in order. -* This approach is only used for AC capacity; DC and B2B capacity additions are tracked directly. -* Defined only for interfaces (r < rr) -eq_INVTRAN_AC_forward(r,rr,tscbin,t) - $[routes_inv(r,rr,"AC",t) - $tmodel(t) - $tsc_binwidth(r,rr,tscbin) - $(not Sw_PCM)].. -* Transmission capacity additions [MW] times bin cost [$/MW] = [$] - sum{tt - $[(yeart(tt) <= yeart(t)) - $(tmodel(tt) or tfix(tt)) - $routes_inv(r,rr,"AC",tt)], - INVTRAN_AC(r,rr,tscbin,tt) * tsc_forward(r,rr,tscbin) - } - - =e= -* Cumulative transmission capacity investments: [$] - TRAN_CAPEX_BINS(r,rr,tscbin,t) -; - -* --------------------------------------------------------------------------- -* Defined only for interfaces (r < rr) -eq_INVTRAN_AC_reverse(r,rr,tscbin,t) - $[routes_inv(r,rr,"AC",t) - $tmodel(t) - $tsc_binwidth(r,rr,tscbin) - $(not Sw_PCM)].. -* Transmission capacity additions [MW] times bin cost [$/MW] = [$] - sum{tt - $[(yeart(tt) <= yeart(t)) - $(tmodel(tt) or tfix(tt)) - $routes_inv(r,rr,"AC",tt)], - INVTRAN_AC(rr,r,tscbin,tt) * tsc_reverse(r,rr,tscbin) - } - - =e= -* Cumulative transmission capacity investments: [$] - TRAN_CAPEX_BINS(r,rr,tscbin,t) -; - -* --------------------------------------------------------------------------- -* Defined for both directions (r < rr and r > rr) -eq_INVTRAN_AC(r,rr,t) - $[routes_inv(r,rr,"AC",t) - $tmodel(t) - $(not Sw_PCM)].. - - INVTRAN(r,rr,"AC",t) - - =e= - - sum{tscbin, INVTRAN_AC(r,rr,tscbin,t) } -; - -* --------------------------------------------------------------------------- -* AC transmission investment bins are limited by the transmission upgrade supply curve -* Defined only for interfaces (r < rr) -eq_TRAN_CAPEX_BINS(r,rr,tscbin,t) - $[routes_inv(r,rr,"AC",t) - $tmodel(t) - $tsc_binwidth(r,rr,tscbin) - $(not Sw_PCM)].. - - tsc_binwidth(r,rr,tscbin) - - =g= - - TRAN_CAPEX_BINS(r,rr,tscbin,t) -; - -* --------------------------------------------------------------------------- -* Exogenous transmission investments are included in INVTRAN -* Defined for both directions (r < rr and r > rr) -eq_invtran_exog(r,rr,trtype,t) - $[routes_inv(r,rr,trtype,t) - $tmodel(t) - $invtran_exog(r,rr,trtype,t) - $(not Sw_PCM)].. - - INVTRAN(r,rr,trtype,t) - - =g= - - invtran_exog(r,rr,trtype,t) -; - -* --------------------------------------------------------------------------- -* Transmission capacity accumulates capacity investments from years up to present -* Defined for both directions (r < rr and r > rr) -eq_CAPTRAN_ENERGY(r,rr,trtype,t) - $[routes(r,rr,trtype,t) - $tmodel(t) - $(not Sw_PCM)].. - - CAPTRAN_ENERGY(r,rr,trtype,t) - - =e= - -* [plus] initial transmission capacity - + trancap_init_energy(r,rr,trtype) - -* [plus] capacity additions up to and including the present year - + sum{tt - $[(yeart(tt) <= yeart(t)) - $(tmodel(tt) or tfix(tt)) - $routes_inv(r,rr,trtype,tt)], - INVTRAN(r,rr,trtype,tt) - } -; - -* --------------------------------------------------------------------------- -* Transmission capacity for PRM trading (derated by Sw_TransInvPRMderate) -* accumulates capacity investments from years up to present. -* Defined for both directions (r < rr and r > rr) -eq_CAPTRAN_PRM(r,rr,trtype,t) - $[routes(r,rr,trtype,t) - $routes_prm(r,rr) - $tmodel(t) - $(not Sw_PCM)].. - - CAPTRAN_PRM(r,rr,trtype,t) - - =e= - -* [plus] initial transmission capacity - + trancap_init_prm(r,rr,trtype) - -* [plus] capacity additions up to and including the present year, -* derated by Sw_TransInvPRMderate - + sum{tt - $[(yeart(tt) <= yeart(t)) - $(tmodel(tt) or tfix(tt)) - $routes_inv(r,rr,trtype,tt)], - INVTRAN(r,rr,trtype,tt) - * (1 - Sw_TransInvPRMderate) - } -; - -* --------------------------------------------------------------------------- - -eq_prescribed_transmission(r,rr,trtype,t) - $[routes_inv(r,rr,trtype,t) - $tmodel(t)$(yeart(t)=RGGI_start_yr)$Sw_RGGI].. - - RGGI_cap(t) - - =g= - - sum{r$RGGI_r(r), EMIT("process","CO2",r,t) } -; - -* --------------------------------------------------------------------------- - -eq_state_cap(st,t) - $[tmodel(t) - $(yeart(t)>=state_cap_start_yr) - $sum{tt, state_cap(st,tt) } - $Sw_StateCap].. - - state_cap(st,t) - - =g= - - sum{r$r_st(r,st), EMIT("process","CO2",r,t) } - -* Import emissions intensity is taken from the previous solve year. -* Here the receiving regions (r) are the cap regions and the sending -* regions (rr) are those that have connection with cap regions. - + sum{(h,r,rr,trtype) - $[r_st(r,st)$(not r_st(rr,st))$routes(rr,r,trtype,t) - $h_rep(h) -* If there is a national zero-carbon cap in the present year, -* set emissions intensity of imports to zero. - $(not ((Sw_AnnualCap>0) and not emit_cap("CO2",t) and not emit_cap("CO2e",t)))], - hours(h) * FLOW(rr,r,h,t,trtype) - * sum{tt$tprev(t,tt), co2_emit_rate_r(rr,tt) } - } -; - -* --------------------------------------------------------------------------- - -* traded emissions among states in each trading group need -* to be less than the sum of all the state caps within that trading group -eq_CSAPR_Budget(csapr_group,t)$[Sw_CSAPR$tmodel(t)$(yeart(t)>=csapr_startyr)].. - -*the accumulation of states csapr cap for the budget category - sum{st$[stfeas(st)$csapr_group_st(csapr_group,st)], csapr_cap(st,"budget",t) } - - =g= - -*must exceed the summed-over-state hourly-weighted nox emissions by csapr group - sum{st$csapr_group_st(csapr_group,st), - sum{(i,v,h,r)$[r_st(r,st)$valgen(i,v,r,t)$h_rep(h)], - h_weight_csapr(h) * hours(h) * emit_rate("process","NOX",i,v,r,t) * GEN(i,v,r,h,t) - } - } -; - -* --------------------------------------------------------------------------- - -* along with the cap on trading groups, each state has -* a maximum amount of NOX emissions during ozone season -eq_CSAPR_Assurance(st,t)$[stfeas(st)$(yeart(t)>=csapr_startyr) - $csapr_cap(st,"Assurance",t)$tmodel(t)].. - -*the state level assurance cap - csapr_cap(st,"assurance",t) - - =g= - -*must exceed the csapr-hourly-weighted nox emissions by state - sum{(i,v,h,r)$[r_st(r,st)$valgen(i,v,r,t)$h_rep(h)], - h_weight_csapr(h) * hours(h) * emit_rate("process","NOX",i,v,r,t) * GEN(i,v,r,h,t) - } -; - -* --------------------------------------------------------------------------- -* This constraint has no input data and is currently unused -eq_emit_rate_limit(e,r,t)$[emit_rate_con(e,r,t)$tmodel(t)].. - - emit_rate_limit(e,r,t) * ( - sum{(i,v,h)$[valgen(i,v,r,t)$h_rep(h)], hours(h) * GEN(i,v,r,h,t) } - ) - - =g= - - EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream -; - -* --------------------------------------------------------------------------- -* This equation enforces emission caps for both the CO2 (Sw_AnnualCap = 1) -* and CO2e emission (Sw_AnnualCap =2 or Sw_AnnualCap =3) scenarios -eq_annual_cap(eall,t)$[sum{tt, emit_cap(eall,tt) }$tmodel(t)$(Sw_AnnualCap>0)].. - -* exogenous CO2 cap (Sw_AnnualCap = 1) or CO2e cap (Sw_AnnualCap > 1) - emit_cap(eall,t) - - =g= - -* must exceed annual endogenous emissions by CO2 pollutant (when Sw_AnnualCap=1) or CO2e pollutants (CO2, CH4, NO2 pollutants when Sw_AnnualCap=2 and additional H2 leakage when Sw_AnnualCap=3) - sum{(e,r)$emit_capped(e), (EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream) * gwp(e) } -; - -* --------------------------------------------------------------------------- - -eq_bankborrowcap(e)$[Sw_BankBorrowCap$sum{t, emit_cap(e,t) }].. - -*weighted exogenous emissions - sum{t$[tmodel(t)$emit_cap(e,t)], - yearweight(t) * emit_cap(e,t) } - - =g= - -* must exceed weighted endogenous emissions - sum{(r,t)$[tmodel(t)$emit_cap(e,t)], - yearweight(t) * (EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream)} -; - -* --------------------------------------------------------------------------- - -eq_cdr_cap(t) - $[tmodel(t) - $(Sw_AnnualCap>0) - $Sw_NoFossilOffsetCDR].. - -*** GHG emissions from fossil CCS... -* CO2 emissions from fossil CCS... - + sum{(i,v,r,h)$[valgen(i,v,r,t)$ccs(i)$(not beccs(i))$h_rep(h)$(Sw_AnnualCap<2)], - hours(h) * GEN(i,v,r,h,t) * (emit_rate("process","CO2",i,v,r,t) + emit_rate("upstream","CO2",i,v,r,t)$Sw_Upstream) } -* GHG emissions * global warming potential - + sum{(i,v,r,h)$[valgen(i,v,r,t)$ccs(i)$(not beccs(i))$h_rep(h)$(Sw_AnnualCap>=2)], - hours(h) * GEN(i,v,r,h,t) * sum{e, (emit_rate("process",e,i,v,r,t) + emit_rate("upstream",e,i,v,r,t)$Sw_Upstream) * gwp(e) } } - =g= - -*** ...must be greater than emissions offset by CDR (negative emissions so negative signs here) -** DAC - - sum{(p,i,v,r,h)$[valcap(i,v,r,t)$i_p(i,p)$dac(i)$sameas(p,"DAC")$h_rep(h)], - hours(h) * (prod_emit_rate("process","CO2",i,t) + prod_emit_rate("upstream","CO2",i,t)$Sw_Upstream) * PRODUCE(p,i,v,r,h,t) } -** BECCS - - sum{(i,v,r,h)$[valgen(i,v,r,t)$beccs(i)$h_rep(h)], - hours(h) * (emit_rate("process","CO2",i,v,r,t) + emit_rate("upstream","CO2",i,v,r,t)$Sw_Upstream) * GEN(i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -*Under the Clean Air Act Section 111, new gas plants (CCs or CTs) can operate above caa_gas_max_cf percent before caa_coal_retire_year -*During and after caa_coal_retire_year, they need to either A) operate at <= caa_gas_max_cf percent CF, B) upgrade with CCS or C) retire -eq_caa_max_cf(i,v,r,t)$[tmodel(t)$valgen(i,v,r,t) - $gas(i)$(not ccs(i)) - $heat_rate(i,v,r,t) - $(firstyear_v(i,v)>=caa_first_year) - $(yeart(t)>=caa_coal_retire_year) - $Sw_Clean_Air_Act].. - -*fraction of annual generation capacity - sum{h$h_rep(h), hours(h)} * caa_gas_max_cf * CAP(i,v,r,t) - - - =g= - -*must exceed total annual generation - sum{h$h_rep(h), hours(h) * GEN(i,v,r,h,t)} -; - -* --------------------------------------------------------------------------- - -*Under the Clean Air Act Section 111, the emissions from existing coal plants per state must be less than or equal to a rate-based emissions standard - -*The rate is equivalent to average coal CCS emissions assuming 90% capture rate [metric tons CO2 / MWh] -eq_caa_rate_standard(st,t)$[tmodel(t) - $(yeart(t)>=caa_coal_retire_year) - $Sw_Clean_Air_Act].. - -*rate equivalent to average coal CCS emissions assuming 90% capture rate [metric tons CO2 / MWh] - caa_rate_emis_standard - -*coal generation in that state [MWh] - * sum{(i,v,r,h)$[valgen(i,v,r,t)$coal(i)$(not cofire(i))$r_st(r,st)], - GEN(i,v,r,h,t)} - =g= - -*coal emissions in that state [metric tons CO2] - sum{(i,v,r,h)$[valgen(i,v,r,t)$coal(i)$(not cofire(i))$r_st(r,st)], - GEN(i,v,r,h,t) * emit_rate("process","CO2",i,v,r,t)} -; - -*========================== -* --- RPS CONSTRAINTS --- -*========================== - -* --------------------------------------------------------------------------- - -eq_REC_Generation(RPSCat,i,st,t)$[stfeas(st)$(not tfirst(t))$tmodel(t) - $Sw_StateRPS$(yeart(t)>=firstyear_RPS) - $(not sameas(RPSCat,"RPS_Bundled")) - $(not sameas(RPSCat,"CES_Bundled")) - $RecTech(RPSCat,i,st,t) - ].. - -*RECS are computed as the total annual generation from a technology -*hydro is the only technology adjusted by RPSTechMult -*because GEN can only generate a H2 PTC credit or a REC, not both, subtract out the generation which produces a hydrogen PTC credit -*because GEN from pvb(i) includes grid charging, subtract out its grid charging - + sum{(v,r,h)$[valgen(i,v,r,t)$r_st(r,st)$h_rep(h)], - RPSTechMult(RPSCat,i,st) * hours(h) - * (GEN(i,v,r,h,t) - - CREDIT_H2PTC(i,v,r,h,t)$[valgen_h2ptc(i,v,r,t)$Sw_H2_PTC] - - (STORAGE_IN_GRID(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] ) - } - - =g= - -* Generation must be greater than RECS sent to all states that can trade - + sum{ast$[RecMap(i,RPSCat,st,ast,t)$(stfeas(ast) or sameas(ast,"voluntary"))], - RECS(RPSCat,i,st,ast,t) } -* RPS_Bundled RECS and RPS_All RECS can meet the same requirement -* therefore lumping them together to avoid double-counting - + sum{ast$[RecMap(i,"RPS_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("RPS_Bundled",i,st,ast,t) }$[sameas(RPSCat,"RPS_All")] - -*same logic as bundled RPS RECS is applied to the bundled CES RECS - + sum{ast$[RecMap(i,"CES_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("CES_Bundled",i,st,ast,t) }$[sameas(RPSCat,"CES")] -; - -* --------------------------------------------------------------------------- - -* note that the bundled rpscat can be included -* to comply with the RPS_All categeory -* but it is not in itself explicit requirement -eq_REC_Requirement(RPSCat,st,t)$[RecPerc(RPSCat,st,t)$(not tfirst(t)) - $tmodel(t)$Sw_StateRPS$(yeart(t)>=firstyear_RPS) - $(stfeas(st) or sameas(st,"voluntary")) - $(not sameas(RPSCat,"RPS_Bundled")) - $(not sameas(RPSCat,"CES_Bundled"))].. - -* RECs owned (i.e. imported and generated/used in state minus exports) - + sum{(i,ast)$[RecMap(i,RPSCat,ast,st,t)$stfeas(ast)], - RECS(RPSCat,i,ast,st,t) } - - sum{(i,ast)$[RecMap(i,RPSCat,st,ast,t)$stfeas(ast)$(not sameas(st,ast))], - RECS(RPSCat,i,st,ast,t) } - -* bundled RECS can also be used to meet the RPS_All requirements (imports minus exports) - + sum{(i,ast)$[RecMap(i,"RPS_Bundled",ast,st,t)$stfeas(ast)$(not sameas(ast,st))], - RECS("RPS_Bundled",i,ast,st,t) }$[sameas(RPSCat,"RPS_All")] - - sum{(i,ast)$[RecMap(i,"RPS_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("RPS_Bundled",i,st,ast,t) }$[sameas(RPSCat,"RPS_All")] - -* bundled CES credits can also be used to meet the CES requirements (imports minus exports) - + sum{(i,ast)$[RecMap(i,"CES_Bundled",ast,st,t)$stfeas(ast)$(not sameas(ast,st))], - RECS("CES_Bundled",i,ast,st,t) }$[sameas(RPSCat,"CES")] - - sum{(i,ast)$[RecMap(i,"CES_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("CES_Bundled",i,st,ast,t) }$[sameas(RPSCat,"CES")] - -* ACP credits can also be purchased - + ACP_PURCHASES(rpscat,st,t)$(not acp_disallowed(st,RPSCat)) - -* Exports to Canada are assumed to be clean, and therefore consume CES credits - - sum{(r,h)$[r_st(r,st)$h_rep(h)], - can_exports_h(r,h,t) * hours(h) }$[(Sw_Canada=1)$sameas(RPSCat,"CES")] - - =g= - -* note here we do not pre-define the rec requirement since load_exog(r,h,t) -* changes when sent to/from the demand side - RecPerc(RPSCat,st,t) * sum{(r,h)$[r_st_rps(r,st)$h_rep(h)], hours(h) * ( -* RecStyle(st,RPSCat)=0 means end-use sales. - ( (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) * (1.0 - distloss) - )$(RecStyle(st,RPSCat)=0) - -* RecStyle(st,RPSCat)=1 means bus-bar sales. - + ( LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) } - )$(RecStyle(st,RPSCat)=1) - -* RecStyle(st,RPSCat)=2 means generation (including distPV), but subtracting canadian exports -*for CES (similar to the left-hand-side). Also, because GEN from pvb(i) includes grid charging, -*subtract out its grid charging (see eq_REC_Generation above). - + ( sum{(i,v)$[valgen(i,v,r,t)$(not storage_standalone(i))], GEN(i,v,r,h,t) - - (distloss * GEN(i,v,r,h,t))$(distpv(i)) - - (STORAGE_IN_GRID(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] } - - can_exports_h(r,h,t)$[(Sw_Canada=1)$sameas(RPSCat,"CES")] - )$(RecStyle(st,RPSCat)=2) - )} -; - -* --------------------------------------------------------------------------- - -eq_REC_BundleLimit(RPSCat,st,ast,t)$[stfeas(st)$stfeas(ast)$tmodel(t) - $(not sameas(st,ast))$Sw_StateRPS - $(sum{i,RecMap(i,RPSCat,st,ast,t) }) - $(sameas(RPSCat,"RPS_Bundled") or sameas(RPSCat,"CES_Bundled")) - $(yeart(t)>=firstyear_RPS)].. - -*amount of net transmission flows from state st to state ast - sum{(h,r,rr,trtype)$[r_st(r,st)$r_st(rr,ast)$routes(r,rr,trtype,t)$h_rep(h)], - hours(h) * FLOW(r,rr,h,t,trtype) - } - - =g= -* must be greater than bundled RECS - sum{i$RecMap(i,RPSCat,st,ast,t), - RECS(RPSCat,i,st,ast,t) } -; - -* --------------------------------------------------------------------------- - -eq_REC_unbundledLimit(RPSCat,st,t)$[st_unbundled_limit(RPScat,st)$tmodel(t)$stfeas(st) - $(yeart(t)>=firstyear_RPS)$Sw_StateRPS - $(sameas(RPSCat,"RPS_All") or sameas(RPSCat,"CES"))].. -*the limit on unbundled RECS times the REC requirement (based on end-use sales) - REC_unbundled_limit(RPSCat,st,t) * RecPerc(RPSCat,st,t) * - sum{(r,h)$[r_st(r,st)$h_rep(h)], - hours(h) * - (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) * (1.0 - distloss) - } - =g= - -*needs to be greater than the unbundled recs -*NB unbundled RECS are computed as all imported RECS minus bundled RECS - sum{(i,ast)$[RecMap(i,RPSCat,ast,st,t)$stfeas(ast)$(not sameas(st,ast))], - RECS(RPSCat,i,ast,st,t) } - - - sum{(i,ast)$[RecMap(i,"RPS_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("RPS_Bundled",i,ast,st,t) }$sameas(RPSCat,"RPS_All") - - - sum{(i,ast)$[RecMap(i,"CES_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("CES_Bundled",i,ast,st,t) }$sameas(RPSCat,"CES") -; - -* --------------------------------------------------------------------------- - -eq_REC_ooslim(RPSCat,st,t)$[RecPerc(RPSCat,st,t)$(yeart(t)>=firstyear_RPS) - $RPS_oosfrac(st)$stfeas(st)$tmodel(t)$Sw_StateRPS - $(not sameas(RPSCat,"RPS_Bundled")) - $(not sameas(RPSCat,"CES_Bundled"))].. - -*the fraction of imported recs times the requirement (based on end-use sales) - RPS_oosfrac(st) * RecPerc(RPSCat,st,t) * - sum{(r,h)$[r_st(r,st)$h_rep(h)], - hours(h) * - (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) * (1.0 - distloss) - } - =g= - -*imported RECs - note that the not sameas(st,ast) indicates they are not generated in-state - sum{(i,ast)$[RecMap(i,RPSCat,ast,st,t)$stfeas(ast)$(not sameas(st,ast))], - RECS(RPSCat,i,ast,st,t) - } - - + sum{(i,ast)$[RecMap(i,"RPS_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("RPS_Bundled",i,ast,st,t) - }$sameas(RPSCat,"RPS_All") - - + sum{(i,ast)$[RecMap(i,"CES_Bundled",ast,st,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("CES_Bundled",i,ast,st,t) - }$sameas(RPSCat,"CES") -; - -* --------------------------------------------------------------------------- - -*exports must be less than RECS generated -eq_REC_launder(RPSCat,st,t)$[RecStates(RPSCat,st,t)$(not tfirst(t))$(yeart(t)>=firstyear_RPS) - $tmodel(t)$stfeas(st)$Sw_StateRPS - $(not sameas(RPSCat,"RPS_Bundled")) - $(not sameas(RPSCat,"CES_Bundled"))].. - -*in-state REC generation - + sum{(i,v,r,h)$[valgen(i,v,r,t)$RecTech(RPSCat,i,st,t)$r_st(r,st)$h_rep(h)], - hours(h) * (GEN(i,v,r,h,t) - CREDIT_H2PTC(i,v,r,h,t)$[valgen_h2ptc(i,v,r,t)$Sw_H2_PTC]) - } - -*minus ACP_PURCHASES with a 10x multiplier (the multiplier discourages the model from -*exporting RECs when it is buying ACP credits) - - ACP_PURCHASES(RPSCat,st,t)$(not acp_disallowed(st,RPSCat)) * 10 - - =g= - -*exported RECS - NB the conditional that st!=ast - + sum{(i,ast)$[RecMap(i,RPSCat,st,ast,t)$(stfeas(ast) or sameas(ast,"voluntary"))$(not sameas(st,ast))], - RECS(RPSCat,i,st,ast,t) } - - + sum{(i,ast)$[RecMap(i,"RPS_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("RPS_Bundled",i,st,ast,t) - }$sameas(RPSCat,"RPS_All") - - + sum{(i,ast)$[RecMap(i,"CES_Bundled",st,ast,t)$stfeas(ast)$(not sameas(st,ast))], - RECS("CES_Bundled",i,st,ast,t) - }$sameas(RPSCat,"CES") - -; - -* --------------------------------------------------------------------------- - -eq_RPS_OFSWind(st,t)$[tmodel(t)$stfeas(st)$offshore_cap_req(st,t)$Sw_StateRPS - $sum{(i,v,r)$[r_st(r,st)$ofswind(i)], valcap(i,v,r,t) } - $(yeart(t)>=firstyear_RPS)$(not Sw_PCM)].. - -* existing capacity of wind - sum{(i,v,r)$[r_st(r,st)$ofswind(i)], m_capacity_exog(i,v,r,t) } - -* investments over time - + sum{(i,v,r,tt)$[r_st(r,st)$ofswind(i)$inv_cond(i,v,r,t,tt)$(tmodel(tt) or tfix(tt))], - INV(i,v,r,tt) + INV_REFURB(i,v,r,tt)$[refurbtech(i)$Sw_Refurb] } - - =g= - -*exogenously-specified requirement for offshore wind capacity - offshore_cap_req(st,t) -; - -* --------------------------------------------------------------------------- - -eq_batterymandate(st,t) - $[tmodel(t)$batterymandate(st,t)$(yeart(t)>=firstyear_battery) - $Sw_BatteryMandate - $(not Sw_PCM)].. -*battery capacity - sum{(i,v,r)$[r_st(r,st)$valcap(i,v,r,t)$(battery(i) or pvb(i))], bcr(i) * CAP(i,v,r,t) } - - =g= - -*must be greater than the required level - batterymandate(st,t) -; - -* --------------------------------------------------------------------------- - -eq_national_gen(t)$[tmodel(t)$national_gen_frac(t)$Sw_GenMandate].. - -*generation from renewables (already post-curtailment) - sum{(i,v,r,h)$[nat_gen_tech_frac(i)$valgen(i,v,r,t)$h_rep(h)], - GEN(i,v,r,h,t) * hours(h) * nat_gen_tech_frac(i) } - - =g= - -*must exceed the mandated percentage [times] - national_gen_frac(t) * ( - -* if Sw_GenMandate = 1, then apply the fraction to the bus bar load - ( -* load - sum{(r,h)$h_rep(h), LOAD(r,h,t) * hours(h) } -* [plus] transmission losses - + sum{(rr,r,h,trtype)$[routes(rr,r,trtype,t)$h_rep(h)], (tranloss(rr,r,trtype) * FLOW(rr,r,h,t,trtype) * hours(h)) } -* [plus] storage losses - + sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)$h_rep(h)], STORAGE_IN(i,v,r,h,t) * hours(h) } - - sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)$h_rep(h)], GEN(i,v,r,h,t) * hours(h) } - )$[Sw_GenMandate = 1] - -* if Sw_GenMandate = 2, then apply the fraction to the end use load - + (sum{(r,h)$h_rep(h), - hours(h) * - ( (LOAD(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1]) * (1.0 - distloss) - sum{v$valgen("distpv",v,r,t), GEN("distpv",v,r,h,t) }) - })$[Sw_GenMandate = 2] - ) -; - -* --------------------------------------------------------------------------- - -*==================================== -* --- FUEL SUPPLY CURVES --- -*==================================== - -* --------------------------------------------------------------------------- - -*gas used from each bin is the sum of all gas used -eq_gasused(cendiv,h,t)$[tmodel(t)$((Sw_GasCurve=0) or (Sw_GasCurve=3))].. - - sum{gb,GASUSED(cendiv,gb,h,t) } - - =e= - - sum{(i,v,r)$[valgen(i,v,r,t)$gas(i)$r_cendiv(r,cendiv)], - heat_rate(i,v,r,t) * (GEN(i,v,r,h,t) + CCSFLEX_POW(i,v,r,h,t)$[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)]) } / gas_scale - - + (sum{(v,r)$[valcap("dac_gas",v,r,t)$r_cendiv(r,cendiv)], - dac_gas_cons_rate("dac_gas",v,t) * PRODUCE("DAC","dac_gas",v,r,h,t) } / gas_scale)$Sw_DAC_Gas - -; - -* --------------------------------------------------------------------------- - -* gas from each bin needs to less than its capacity -eq_gasbinlimit(cendiv,gb,t)$[tmodel(t)$(Sw_GasCurve=0)].. - - gaslimit(cendiv,gb,t) - - =g= - - sum{h, hours(h) * GASUSED(cendiv,gb,h,t) } -; - -* --------------------------------------------------------------------------- - -eq_gasbinlimit_nat(gb,t)$[tmodel(t)$(Sw_GasCurve=3)].. - - gaslimit_nat(gb,t) - - =g= - - sum{(h,cendiv), - hours(h) * GASUSED(cendiv,gb,h,t) - } -; - -* --------------------------------------------------------------------------- - -eq_gasaccounting_regional(cendiv,t)$[tmodel(t)$(Sw_GasCurve=1)].. - - sum{fuelbin, VGASBINQ_REGIONAL(fuelbin,cendiv,t) } - - =e= - - sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)$r_cendiv(r,cendiv)], - hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) - } -; - -* --------------------------------------------------------------------------- - -eq_gasaccounting_national(t)$[tmodel(t)$(Sw_GasCurve=1)].. - - sum{fuelbin,VGASBINQ_NATIONAL(fuelbin,t) } - - =e= - - sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)], - hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) - } -; - -* --------------------------------------------------------------------------- - -eq_gasbinlimit_regional(fuelbin,cendiv,t)$[tmodel(t)$(Sw_GasCurve=1)].. - - Gasbinwidth_regional(fuelbin,cendiv,t) - - =g= - - VGASBINQ_REGIONAL(fuelbin,cendiv,t) -; - -* --------------------------------------------------------------------------- - -eq_gasbinlimit_national(fuelbin,t)$[tmodel(t)$(Sw_GasCurve=1)].. - - Gasbinwidth_national(fuelbin,t) - - =g= - - VGASBINQ_NATIONAL(fuelbin,t) -; - -* --------------------------------------------------------------------------- - -*============================== -* -- Bioenergy Supply Curve -- -*============================== - -* --------------------------------------------------------------------------- - -eq_bioused(r,t)$[sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,t) }$tmodel(t)].. - - sum{bioclass, BIOUSED(bioclass,r,t) } - - =e= - -*biopower generation - + sum{(i,v,h)$[valgen(i,v,r,t)$bio(i)], - hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) } - - -*portion of cofire generation that is from bio resources - + sum{(i,v,h)$[cofire(i)$valgen(i,v,r,t)], - bio_cofire_perc * hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -* biomass consumption limit is annual -eq_biousedlimit(bioclass,usda_region,t)$tmodel(t).. - - biosupply(usda_region,bioclass,"cap") - - =g= - - sum{r$[r_usda(r,usda_region)$sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,t) }], BIOUSED(bioclass,r,t) } -; - -* --------------------------------------------------------------------------- - -*============================ -* --- STORAGE CONSTRAINTS --- -*============================ - -* --------------------------------------------------------------------------- - -*storage use cannot exceed capacity -*this constraint does not apply to CSP+TES or hydro pump upgrades -eq_storage_capacity(i,v,r,h,t)$[valgen(i,v,r,t) - $(storage_standalone(i)$(not evmc_storage(i)) - or evmc_storage(i) - $[evmc_storage_charge_frac(i,r,h,t)$evmc_storage_discharge_frac(i,r,h,t)] - or storage_hybrid(i)$(not csp(i))) - $tmodel(t)].. - -* [plus] Capacity of all storage technologies - (CAP(i,v,r,t) * bcr(i) * avail(i,r,h) - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - )$valcap(i,v,r,t) - - =g= - -* [plus] Generation from storage, excluding hybrid+storage and adjusting evmc_storage for time-varying discharge (deferral) availability - GEN(i,v,r,h,t)$(not storage_hybrid(i)$(not csp(i))) / (1$(not evmc_storage(i)) + evmc_storage_discharge_frac(i,r,h,t)$evmc_storage(i)) - -* [plus] Generation from battery of hybrid+storage - + GEN_STORAGE(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] - -* [plus] Storage charging -* excludes hybrid plant+storage and adjusting evmc_storage for time-varying charge (add back deferred EV load) availability - + STORAGE_IN(i,v,r,h,t)$[not storage_hybrid(i)$(not csp(i))] / (1$(not evmc_storage(i)) + evmc_storage_charge_frac(i,r,h,t)$evmc_storage(i)) - -* hybrid+storage plant: plant generation - + STORAGE_IN_PLANT(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$dayhours(h)$Sw_HybridPlant] -* hybrid+storage plant: Grid generation - + STORAGE_IN_GRID(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] - -* [plus] Operating reserves - + sum{ortype$[Sw_OpRes$opres_model(ortype)$opres_h(h)], - reserve_frac(i,ortype) * OPRES(ortype,i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -* The daily storage level in the next time-slice (h+1) must equal the -* daily storage level in the current time-slice (h) -* plus daily net charging in the current time-slice (accounting for losses). -* CSP with storage energy accounting is also covered by this constraint. -* Does not apply for storage technologies that allow cross-season energy arbitrage. -* When inter-day linkage is used, the nexth will be disabled, and this equation will -* be used to calculate intra-day storage dispatch for further inter-day linkage use. -eq_storage_level(i,v,r,h,t)$[valgen(i,v,r,t)$storage(i)$tmodel(t)].. - -*[plus] storage level in h+1 - sum{(hh)$[nexth(h,hh)], STORAGE_LEVEL(i,v,r,hh,t)}$(not storage_interday(i)) - -*[plus] the net dispatch of inter-day storage technologies - + STORAGE_INTERDAY_DISPATCH(i,v,r,h,t)$(storage_interday(i)) * hours_daily(h) - - =e= - -* only want to include storage_level from periods that have had a previous storage_level -* otherwise it becomes a free variable, implying you can charge storage without bound - STORAGE_LEVEL(i,v,r,h,t)$(not storage_interday(i)) - -*[plus] storage charging - + storage_eff(i,t) * hours_daily(h) * ( -*energy into stand-alone storage (not CSP-TES) and hydropower that adds pumping - STORAGE_IN(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)] - -*energy into storage from CSP field - + (CAP(i,v,r,t) * csp_sm(i) * m_cf(i,v,r,h,t) - )$[CSP_Storage(i)$valcap(i,v,r,t)] - ) -*[plus] water inflow energy available for hydropower that adds pumping - + (CAP(i,v,r,t) * avail(i,r,h) * hours_daily(h) * - sum{szn$h_szn(h,szn), m_cf_szn(i,v,r,szn,t) } - )$hyd_add_pump(i) - -*[plus] energy into hybrid plant storage -*hybrid+storage plant: plant charging - + storage_eff_pvb_p(i,t) * hours_daily(h) - * STORAGE_IN_PLANT(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$dayhours(h)$Sw_HybridPlant] - -*hybrid+storage plant: grid charging - + storage_eff_pvb_g(i,t) * hours_daily(h) - * STORAGE_IN_GRID(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] - -*[minus] generation from stand-alone storage (discharge) and CSP -*exclude hybrid+storage plant because GEN refers to output from both the plant and the battery - - hours_daily(h) * GEN(i,v,r,h,t)$[not storage_hybrid(i)$(not csp(i))] - -*[minus] Generation from Battery (discharge) of hybrid+storage plant - - hours_daily(h) * GEN_STORAGE(i,v,r,h,t) $[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] - -*[minus] losses from reg reserves (only half because only charging half -*the time while providing reg reserves) - - (hours_daily(h) - * (OPRES("reg",i,v,r,h,t)$[Sw_OpRes=1] + OPRES("combo",i,v,r,h,t)$[Sw_OpRes=2]) - * (1 - storage_eff(i,t)) / 2 * reg_energy_frac - )$[opres_h(h)] -; - -* --------------------------------------------------------------------------- - -*there must be sufficient energy in storage to provide operating reserves -eq_storage_opres(i,v,r,h,t) - $[valgen(i,v,r,t)$tmodel(t)$Sw_OpRes$opres_h(h) - $(storage_standalone(i) or storage_hybrid(i)$(not csp(i)) or hyd_add_pump(i))].. - -*[plus] initial storage level - STORAGE_LEVEL(i,v,r,h,t) - -*[minus] generation that occurs during this timeslice - - hours_daily(h) * GEN(i,v,r,h,t) $[not storage_hybrid(i)$(not csp(i))] - -*[minus] generation that occurs during this timeslice - - hours_daily(h) * GEN_STORAGE(i,v,r,h,t) $[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] - -*[minus] losses from reg reserves (only half because only charging half -*the time while providing reg reserves) - - hours_daily(h) * (OPRES("reg",i,v,r,h,t)$[Sw_OpRes = 1] + OPRES("combo",i,v,r,h,t)$[Sw_OpRes = 2]) - * (1 - storage_eff(i,t)) / 2 * reg_energy_frac - - =g= - -*[plus] energy reserved for operating reserves - + hours_daily(h) * sum{ortype$opres_model(ortype), OPRES(ortype,i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -*storage charging must exceed OR contributions for thermal storage -eq_storage_thermalres(i,v,r,h,t) - $[valgen(i,v,r,t)$Thermal_Storage(i) - $tmodel(t)$Sw_OpRes$opres_h(h)].. - - STORAGE_IN(i,v,r,h,t) - - =g= - - sum{ortype$[opres_model(ortype)], - reserve_frac(i,ortype) * OPRES(ortype,i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -*batteries and CSP-TES are limited by their duration for each normalized hour per season -*seas_cap_frac_delta is not applied here because we assume that the storage energy capacity is -*constant across the year. -eq_storage_duration(i,v,r,h,t)$[valgen(i,v,r,t)$valcap(i,v,r,t) - $storage(i) - $tmodel(t) - $(not storage_interday(i))].. - -* [plus] storage duration times storage capacity for fixed-duration techs - storage_duration(i) * CAP(i,v,r,t) * (1$CSP_Storage(i) + 1$psh(i) + bcr(i)$pvb(i)) - -* [plus] EVMC storage has time-varying energy capacity - + evmc_storage_energy_hours(i,r,h,t) * CAP(i,v,r,t) * (bcr(i)$evmc_storage(i)) - -* [plus] battery storage capacity - + CAP_ENERGY(i,v,r,t)$battery(i) - - =g= - - STORAGE_LEVEL(i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - -* Charging power must be less than a specified fraction of power output capacity -* This is required in addition to eq_storage_capacity for facilities where input capacity < output capacity. -* If storinmaxfrac were applied to CAP in eq_storage_capacity, it would also limit output capacity. -eq_storage_in_cap(i,v,r,h,t)$[(storage_standalone(i) or hyd_add_pump(i))$valgen(i,v,r,t)$valcap(i,v,r,t) - $tmodel(t)$(storinmaxfrac(i,v,r) < 1)].. - -*[plus] maximum storage input capacity as a fraction of output capacity and accounting for availability -* for evmc_storage this adjust for time-varying availability of charging (add back deferred EV load) - avail(i,r,h) * storinmaxfrac(i,v,r) - * CAP(i,v,r,t) - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - * (1$(not evmc_storage(i)) + evmc_storage_charge_frac(i,r,h,t)$evmc_storage(i)) - - =g= - -*[plus] storage input - STORAGE_IN(i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - -* the charging power in any time slice cannot exceed the minstorfrac times the -* charging power from any other hour within the same season (via hour_szn_group(h,hh)) -eq_storage_in_minloading(i,v,r,h,hh,t)$[(storage_standalone(i) or hyd_add_pump(i))$tmodel(t) - $minstorfrac(i,v,r)$valgen(i,v,r,t)$hour_szn_group(h,hh)$Sw_MinLoading].. - - STORAGE_IN(i,v,r,h,t) - - =g= - - STORAGE_IN(i,v,r,hh,t) * minstorfrac(i,v,r) -; - -* --------------------------------------------------------------------------- -* for batteries -* when power capacity is built, energy capacity must be greater than the minimum duration -eq_battery_minduration(i,v,r,t)$[valcap(i,v,r,t)$tmodel(t)$newv(v)$battery(i)].. - - CAP_ENERGY(i,v,r,t) - - =g= - - minbatteryduration * CAP(i,v,r,t) -; - -* --------------------------------------------------------------------------- - -* Constraints for building inter-day storage linkage -* The storage level at hour 0 of next partition -* equals to the storage level at hour 0 of current partition -* plus number of period of current partition multiply by storage net change of the rep period -eq_storage_interday_level(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. - - sum{allsznn$[nextpartition(allszn,allsznn)],STORAGE_INTERDAY_LEVEL(i,v,r,allsznn,t)} - - =e= - - STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) - - + numpartitions(allszn) - * sum{(szn)$[szn_actualszn(szn,allszn)$numpartitions(allszn)], - sum{h$[h_szn(h,szn)], (STORAGE_INTERDAY_DISPATCH(i,v,r,h,t) * hours_daily(h))}} -; - -* --------------------------------------------------------------------------- - -* Constraints for building inter-day storage linkage -* Define the maximum relative soc on a representative period compared to its hour 0 -* It's noted that STORAGE_INTERDAY_LEVEL_MAX_DAY can be nagative since it represent a relative value -eq_storage_interday_level_max_day(i,v,r,szn,h,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$h_szn(h,szn)].. - - STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,szn,t) - - =g= - - sum{(hh)$[h_preh(h,hh)], (STORAGE_INTERDAY_DISPATCH(i,v,r,hh,t) * hours_daily(h))} -; - -* --------------------------------------------------------------------------- - -* Constraints for building inter-day storage linkage -* Define the minimum relative soc on a representative period compared to its hour 0 -* It's noted that STORAGE_INTERDAY_LEVEL_MIN_DAY can be nagative since it represent a relative value -eq_storage_interday_level_min_day(i,v,r,szn,h,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$h_szn(h,szn)].. - - sum{(hh)$[h_preh(h,hh)], (STORAGE_INTERDAY_DISPATCH(i,v,r,hh,t) * hours_daily(h))} - - =g= - - STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,szn,t) -; - -* --------------------------------------------------------------------------- - -* Constraints for building inter-day storage linkage -* For all partitions, the soc at their hour 0 plus minimum soc of first period should greater than 0 -* This is to make sure not only their hour 0 but also the lowest point of the first period of each partition is greater than 0 -eq_storage_interday_min_level_start(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. - - STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) - - + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,szn,t)} - - =g= - - 0 -; - -* --------------------------------------------------------------------------- - -* Constraints for building inter-day storage linkage -* For all partitions, the soc at their hour 0 plus minimum soc of last period should greater than 0 -* This is to make sure not only their hour 0 but also the lowest point of the last period of each partition is greater than 0 -eq_storage_interday_min_level_end(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. - - STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) - - + (numpartitions(allszn) - 1) - * sum{(szn)$[szn_actualszn(szn,allszn)], - sum{h$[h_szn(h,szn)], (STORAGE_INTERDAY_DISPATCH(i,v,r,h,t) * hours_daily(h))}} - - + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MIN_DAY(i,v,r,szn,t)} - - =g= - - 0 -; - -* --------------------------------------------------------------------------- - -* Constraints for building inter-day storage linkage -* For all partitions, the soc at their hour 0 plus maximum soc of first period should lower than total storage capacity -* This is to make sure not only their hour 0 but also the highest point of the first period of each partition is lower than maximum capacity -eq_storage_interday_max_level_start(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. - -* Fixed-duration storage - storage_duration(i) * CAP(i,v,r,t)$(not battery(i)) -* Variable-duration storage - + CAP_ENERGY(i,v,r,t)$battery(i) - - =g= - - STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) - - + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,szn,t)} -; - -* --------------------------------------------------------------------------- - -* Constraints for building inter-day storage linkage -* For all partitions, the soc at their hour 0 plus maximum soc of last period should lower than total storage capacity -* This is to make sure not only their hour 0 but also the highest point of the last period of each partition is greater than maximum capacity -eq_storage_interday_max_level_end(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)$tmodel(t)$numpartitions(allszn)].. - - storage_duration(i) * CAP(i,v,r,t)$(not battery(i)) - - + CAP_ENERGY(i,v,r,t)$battery(i) - - =g= - - STORAGE_INTERDAY_LEVEL(i,v,r,allszn,t) - - + (numpartitions(allszn) - 1) - * sum{(szn)$[szn_actualszn(szn,allszn)], - sum{h$[h_szn(h,szn)], (STORAGE_INTERDAY_DISPATCH(i,v,r,h,t) * hours_daily(h))}} - - + sum{(szn)$[szn_actualszn(szn,allszn)], STORAGE_INTERDAY_LEVEL_MAX_DAY(i,v,r,szn,t)} -; - -* --------------------------------------------------------------------------- - -*=============================== -* --- Hybrid Plant --- -*=============================== - -* --------------------------------------------------------------------------- - -*Generation post curtailment = -* + generation from hybrid storage plant + generation from storage - storage charging from hybrid storage plant -eq_plant_total_gen(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$tmodel(t)$valgen(i,v,r,t)$Sw_HybridPlant].. - - + GEN_PLANT(i,v,r,h,t) - - + GEN_STORAGE(i,v,r,h,t) - -*[minus] charging from hybrid storage plant - - STORAGE_IN_PLANT(i,v,r,h,t)$dayhours(h) - - =e= - - GEN(i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - -*Energy to storage from hybrid storage palnt + hybrid storage plant generation <= hybrid storage plant maximum production for a resource -*capacity factor is adjusted to include inverter losses, clipping losses, and low voltage losses -eq_hybrid_plant_energy_limit(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$tmodel(t)$valgen(i,v,r,t)$valcap(i,v,r,t)$Sw_HybridPlant].. - -* [plus] plant output - m_cf(i,v,r,h,t) * CAP(i,v,r,t) - - =g= - -*[plus] charging from hybrid plant - + STORAGE_IN_PLANT(i,v,r,h,t)$dayhours(h) - -*[plus] generation from hybrid plant - + GEN_PLANT(i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - -*Energy moving through the inverter cannot exceed the inverter capacity -eq_plant_capacity_limit(i,v,r,h,t)$[storage_hybrid(i)$(not csp(i))$tmodel(t)$valgen(i,v,r,t)$valcap(i,v,r,t)$Sw_HybridPlant].. - -*[plus] inverter capacity [AC] = panel capacity [DC] / ILR [DC/AC] - + CAP(i,v,r,t) / ilr(i) - - =g= - -* [plus] Output from plant - + GEN_PLANT(i,v,r,h,t) - -* [plus] Output form storage - + GEN_STORAGE(i,v,r,h,t) - -*[plus] battery charging from grid - + STORAGE_IN_GRID(i,v,r,h,t) - -*[plus] battery operating reserves - + sum{ortype$[Sw_OpRes$opres_h(h)$opres_model(ortype)], OPRES(ortype,i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -*Total energy charged from local PV >= ITC qualification fraction * total energy charged - -eq_pvb_itc_charge_reqt(i,v,r,t)$[pvb(i)$tmodel(t)$valgen(i,v,r,t)$pvb_itc_qual_frac$Sw_PVB].. - -* [plus] battery charging from PV - + sum{h$[dayhours(h)$h_rep(h)], STORAGE_IN_PLANT(i,v,r,h,t) * hours(h) } - - =g= - - + pvb_itc_qual_frac * ( - -* [plus] battery charging from PV - + sum{h$[dayhours(h)$h_rep(h)], STORAGE_IN_PLANT(i,v,r,h,t) * hours(h) } - -* [plus] battery charging from Grid - + sum{h$h_rep(h), STORAGE_IN_GRID(i,v,r,h,t) * hours(h) } - ) -; - -* --------------------------------------------------------------------------- - -*=================================== -* --- CANADIAN IMPORTS EQUATIONS --- -*=================================== - -* --------------------------------------------------------------------------- - -eq_Canadian_Imports(r,szn,t)$[can_imports_szn(r,szn,t)$tmodel(t)$(Sw_Canada=1)].. - - can_imports_szn(r,szn,t) - - =g= - - sum{(i,v,h)$[canada(i)$valgen(i,v,r,t)$h_szn(h,szn)], GEN(i,v,r,h,t) * hours(h) } -; - -* --------------------------------------------------------------------------- - -*========================== -* --- WATER CONSTRAINTS --- -*========================== - -* --------------------------------------------------------------------------- - -*water accounting for all valid power plants for generation where usage is both for cooling and/or non-cooling purposes -eq_water_accounting(i,v,w,r,h,t)$[i_water(i)$valgen(i,v,r,t)$h_rep(h)$tmodel(t)$Sw_WaterMain].. - - WAT(i,v,w,r,h,t) - - =e= - -*division by 1E6 to convert gal of water_rate(i,w) to Mgal - GEN(i,v,r,h,t) * hours(h) * water_rate(i,w) / 1E6 -; - -* --------------------------------------------------------------------------- - -*total water access is determined by total capacity -eq_water_capacity_total(i,v,r,t)$[tmodel(t)$valcap(i,v,r,t) - $(i_water_cooling(i) or psh(i))$Sw_WaterMain$Sw_WaterCapacity].. - - WATCAP(i,v,r,t) - - =e= - -*require enough water capacity to allow 100% capacity factor (8760 hour operation) -*division by 1E6 to convert gal of water_rate(i,w) to Mgal - sum{h$h_rep(h), hours(h) - * sum{w$i_w(i,w), - CAP(i,v,r,t) * water_rate(i,w) } - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - } / 1E6 - -*require enough water capacity to fill PSH reservoir. -*uses investment so that term is only applied in the single investment year -* as a proxy for water needs during construction phase. - + sum{rscbin$[m_rscfeas(r,i,rscbin)$psh(i)], INV_RSC(i,v,r,rscbin,t) * water_req_psh(r,rscbin) }$Sw_PSHwatercon -; - -* --------------------------------------------------------------------------- - -*total water access must not exceed supply -eq_water_capacity_limit(wst,r,t)$[tmodel(t)$Sw_WaterMain$Sw_WaterCapacity].. - - m_watsc_dat(wst,"cap",r,t) - - + WATER_CAPACITY_LIMIT_SLACK(wst,r,t) - - =g= - - sum{(i,v)$[i_wst(i,wst)$valcap(i,v,r,t)], WATCAP(i,v,r,t) } -; - -* --------------------------------------------------------------------------- - -*water use must not exceed available access -eq_water_use_limit(i,v,w,r,szn,t)$[i_water_cooling(i)$valgen(i,v,r,t)$tmodel(t) - $i_w(i,w)$Sw_WaterMain$Sw_WaterCapacity$Sw_WaterUse].. - - WATCAP(i,v,r,t) *sum{wst$i_wst(i,wst), watsa(wst,r,szn,t) } - - =g= - - sum{h$h_szn(h,szn), WAT(i,v,w,r,h,t) } -; - -* --------------------------------------------------------------------------- - -*============================== -* -- H2 and DAC Constraints -- -*============================== - -* --------------------------------------------------------------------------- - -eq_prod_capacity_limit(i,v,r,h,t) - $[tmodel(t) - $consume(i) - $valcap(i,v,r,t) - $Sw_Prod - $h_rep(h)].. - -* available capacity [times] the conversion rate of metric ton / MW - CAP(i,v,r,t) * avail(i,r,h) - * (prod_conversion_rate(i,v,r,t)$[not sameas(i,"dac_gas")] - + 1$sameas(i,"dac_gas")) - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - - =g= - -* production in that timeslice - sum{p$i_p(i,p), PRODUCE(p,i,v,r,h,t) } -; - -* --------------------------------------------------------------------------- - -* H2 demand balance; national and annual. Active only when Sw_H2=1. -eq_h2_demand(p,t)$[(sameas(p,"H2"))$tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2=1)].. - -* annual metric tons of production - sum{(i,v,r,h)$[h2(i)$valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], - PRODUCE(p,i,v,r,h,t) * hours(h) } - - =e= - -* annual demand - h2_exogenous_demand(p,t) - -* assuming here that h2 production and use in H2_COMBUSTION can be temporally asynchronous -* that is, the hydrogen does not need to produced in the same hour it is consumed by h2-ct/cc's - + sum{(i,v,r,h)$[valgen(i,v,r,t)$h2_combustion(i)$h_rep(h)], - GEN(i,v,r,h,t) * hours(h) * h2_combustion_intensity * heat_rate(i,v,r,t) - } -; - -* --------------------------------------------------------------------------- - -* H2 demand balance; regional and by timeslice w/ H2 transport network and storage. -* Active only when Sw_H2=2 [metric tons/hour] -eq_h2_demand_regional(r,h,t) - $[tmodel(t)$(Sw_H2=2)$(yeart(t)>=h2_demand_start)$h_rep(h)].. - -* endogenous supply of hydrogen - sum{(i,v,p)$[h2(i)$valcap(i,v,r,t)$i_p(i,p)], - PRODUCE(p,i,v,r,h,t) } - -* net hydrogen trade with imports reduced by H2 transmission losses - + sum{rr$h2_routes(rr,r), H2_FLOW(rr,r,h,t) } * (1 - h2_tranloss) - - sum{rr$h2_routes(r,rr), H2_FLOW(r,rr,h,t) } - -* net storage injections / withdrawls in a BA - + sum{h2_stor$[h2_stor_r(h2_stor,r)], H2_STOR_OUT(h2_stor,r,h,t)} - - sum{h2_stor$[h2_stor_r(h2_stor,r)], H2_STOR_IN(h2_stor,r,h,t)} - - =e= - -* annual demand in [metric tons/hour] - sum{p, h2_exogenous_demand_regional(r,p,h,t) } - -* region-specific H2 consumption from H2-CT/CCs -* [MW] * [metric ton/MMBtu] * [MMBtu/MWh] = [metric tons/hour] - + sum{(i,v)$[valgen(i,v,r,t)$h2_combustion(i)], - GEN(i,v,r,h,t) * h2_combustion_intensity * heat_rate(i,v,r,t) - } -; - -* --------------------------------------------------------------------------- - -eq_h2_transport_caplimit(r,rr,h,t)$[h2_routes(r,rr)$(Sw_H2=2) - $tmodel(t)$(yeart(t)>=h2_demand_start)].. - -*capacity computed as cumulative investments of h2 pipelines up to the current year - sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt)) - $(yeart(tt)>=h2_demand_start)], - H2_TRANSPORT_INV(r,rr,tt)$h2_routes_inv(r,rr) - + H2_TRANSPORT_INV(rr,r,tt)$h2_routes_inv(rr,r) } - - =g= - -*bi-directional flow of h2 - H2_FLOW(rr,r,h,t) + H2_FLOW(r,rr,h,t) -; - -* --------------------------------------------------------------------------- - -* link H2 storage level between timeslices of actual periods, or hours when running a chronological year -eq_h2_storage_level(h2_stor,r,actualszn,h,t) - $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=2) - $h2_stor_r(h2_stor,r)$(Sw_H2=2)$h_actualszn(h,actualszn)].. - -*[plus] H2 storage level in next timeslice - sum{(hh,actualsznn)$[nexth_actualszn(actualszn,h,actualsznn,hh)], - H2_STOR_LEVEL(h2_stor,r,actualsznn,hh,t) } - - =e= - -* H2 storage level in current timeslice - H2_STOR_LEVEL(h2_stor,r,actualszn,h,t) - -*[plus] h2 storage injection minus withdrawal (currently assumed to be lossless) - + hours_daily(h) * (H2_STOR_IN(h2_stor,r,h,t) - H2_STOR_OUT(h2_stor,r,h,t)) -; - -* --------------------------------------------------------------------------- - -* link H2 storage level between seasons -eq_h2_storage_level_szn(h2_stor,r,actualszn,t) - $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=1) - $h2_stor_r(h2_stor,r)$(Sw_H2=2)].. - -*[plus] H2 storage level at start of next season - sum{actualsznn$[nextszn(actualszn,actualsznn)], - H2_STOR_LEVEL_SZN(h2_stor,r,actualsznn,t) } - - =e= - -* H2 storage level at start of current season - H2_STOR_LEVEL_SZN(h2_stor,r,actualszn,t) - -*[plus] h2 storage injection minus withdrawal (currently assumed to be lossless) - + sum{h$h_actualszn(h,actualszn), - hours_daily(h) * (H2_STOR_IN(h2_stor,r,h,t) - H2_STOR_OUT(h2_stor,r,h,t)) } -; - -* --------------------------------------------------------------------------- - -* H2 storage capacity [metric tons] -eq_h2_storage_capacity(h2_stor,r,t) - $[tmodel(t)$(Sw_H2=2) - $h2_stor_r(h2_stor,r)$(yeart(t)>=h2_demand_start) - $(not Sw_PCM)].. - -* [metric tons] - sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], - H2_STOR_INV(h2_stor,r,tt)} - - =e= - -* [metric tons] - H2_STOR_CAP(h2_stor,r,t) -; - -* --------------------------------------------------------------------------- - -eq_h2_min_storage_cap(r,t)$[tmodel(t)$(Sw_H2=2)$Sw_H2_MinStorHours$(not Sw_PCM)].. - -* [metric tons] - sum{h2_stor$h2_stor_r(h2_stor,r), H2_STOR_CAP(h2_stor,r,t) } - - =g= - -* [MW] * [MMBtu/MWh] * [metric tons/MMBtu] * [hours] = [metric tons] - sum{(i,v)$[h2_combustion(i)$valcap(i,v,r,t)], - CAP(i,v,r,t) * heat_rate(i,v,r,t) * h2_combustion_intensity * Sw_H2_MinStorHours - } -; - -* --------------------------------------------------------------------------- - -* H2 storage investment capacity -* [metric tons/hour] -eq_h2_storage_flowlimit(h2_stor,r,h,t) - $[tmodel(t) - $(Sw_H2=2) - $h2_stor_r(h2_stor,r) - $(yeart(t)>=h2_demand_start) - $h_rep(h)].. - -*storage capacity computed as cumulative investments of H2 storage up to the current year -*H2 storage costs estimated for a fixed duration, so using this to link storage capacity and injection rates -* [metric tons] / [hours] = [metric tons/hour] - H2_STOR_CAP(h2_stor,r,t) / h2_storage_duration - - =g= - -*H2 storage injection [metric tons/hour] - H2_STOR_IN(h2_stor,r,h,t) - -*[plus] H2 storage withdrawal [metric tons/hour] - + H2_STOR_OUT(h2_stor,r,h,t) -; - -* --------------------------------------------------------------------------- - -* total level of H2 storage cannot exceed storage investment for all days -* [metric tons] -eq_h2_storage_caplimit(h2_stor,r,actualszn,h,t) - $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=2) - $h2_stor_r(h2_stor,r)$(Sw_H2=2)$h_actualszn(h,actualszn)].. - -* total storage investment [metric tons] - H2_STOR_CAP(h2_stor,r,t) - - =g= - -* storage level of H2 [metric tons] - H2_STOR_LEVEL(h2_stor,r,actualszn,h,t) -; - -* --------------------------------------------------------------------------- - -* total level of H2 storage at the beginning of the day cannot exceed storage investment -* [metric tons] -eq_h2_storage_caplimit_szn(h2_stor,r,actualszn,t) - $[tmodel(t)$(yeart(t)>=h2_demand_start)$(Sw_H2_StorTimestep=1) - $h2_stor_r(h2_stor,r)$(Sw_H2=2)].. - -* total storage investment [metric tons] - H2_STOR_CAP(h2_stor,r,t) - - =g= - -* storage level of H2 [metric tons] - H2_STOR_LEVEL_SZN(h2_stor,r,actualszn,t) -; - -* --------------------------------------------------------------------------- - -* Hydrogen production tax credit - before h2_ptc_temporal_match_year, electric generation must occur in the same -* region ('h2ptcreg' level) and year that the electrolyzer produces the hydrogen, to qualify for the credit -eq_h2_ptc_region_balance(h2ptcreg,t)$[tmodel(t) - $h2_ptc_years(t) - $(yeart(t)>=h2_demand_start) - $(Sw_H2_PTC) - $(yeart(t)=h2_demand_start) - $(Sw_H2_PTC) - $(yeart(t)>=h2_ptc_temporal_match_year) - ].. - -* generation from clean technologies which qualify to receive hydrogen production tax credits [MW] - sum{(i,v,r)$[r_h2ptcreg(r,h2ptcreg)$valgen_h2ptc(i,v,r,t)], CREDIT_H2PTC(i,v,r,h,t)} - - =e= - -* amount of generation needed to produce hydrogen via electrolysis [MW] - sum{(v,r)$[r_h2ptcreg(r,h2ptcreg)$valcap("electrolyzer",v,r,t)$prod_conversion_rate("electrolyzer",v,r,t)], - PRODUCE("H2","electrolyzer",v,r,h,t) / prod_conversion_rate("electrolyzer",v,r,t) } -; - -* --------------------------------------------------------------------------- - -* Hydrogen production tax credit - total generation must be greater than generation going towards electrolyzers to receive the tax credit -eq_h2_ptc_creditgen(i,v,r,h,t)$[valgen_h2ptc(i,v,r,t) - $h_rep(h) - $tmodel(t) - $Sw_H2_PTC - $h2_ptc_years(t) - $(yeart(t)>=h2_demand_start)].. - -* total generation [MW] - GEN(i,v,r,h,t) - - =g= - -* generation going towards electrolyzers to receive the tax credit [MW] - CREDIT_H2PTC(i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - - -*================================= -* -- CO2 transport and storage -- -*================================= - - -eq_co2_capture(r,h,t) - $[tmodel(t) - $Sw_CO2_Detail - $(yeart(t)>=co2_detail_startyr) - $h_rep(h)].. - - CO2_CAPTURED(r,h,t) - - =e= - -*capture from CCS technologies - sum{(i,v)$[capture_rate("CO2",i,v,r,t)$valgen(i,v,r,t)], capture_rate("CO2",i,v,r,t) - * GEN(i,v,r,h,t) } - -*capture from SMR CCS for H2 production - + sum{(p,i,v)$[i_p("smr_ccs",p)$valcap("smr_ccs",v,r,t)], smr_capture_rate * smr_co2_intensity - * PRODUCE(p,"smr_ccs",v,r,h,t) }$Sw_H2 - -* capture from DAC - + sum{(i,v)$[dac(i)$valcap(i,v,r,t)$i_p(i,"DAC")], PRODUCE("DAC",i,v,r,h,t) }$Sw_DAC -; - -* --------------------------------------------------------------------------- - -eq_co2_transport_caplimit(r,rr,h,t)$[co2_routes(r,rr)$Sw_CO2_Detail - $tmodel(t)$(yeart(t)>=co2_detail_startyr)].. - -*capacity computed as cumulative investments of co2 pipelines up to the current year - sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt)) - $(yeart(tt)>=co2_detail_startyr)], - CO2_TRANSPORT_INV(r,rr,tt) + CO2_TRANSPORT_INV(rr,r,tt) } - - =g= - -*bi-directional flow of co2 - CO2_FLOW(rr,r,h,t) + CO2_FLOW(r,rr,h,t) -; - -* --------------------------------------------------------------------------- - -eq_co2_spurline_caplimit(r,cs,h,t)$[Sw_CO2_Detail$r_cs(r,cs)$tmodel(t)$(yeart(t)>=co2_detail_startyr)].. - -*capacity computed as cumulative investments of co2 spurlines up to the current year - sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))$(yeart(tt)>=co2_detail_startyr)], - CO2_SPURLINE_INV(r,cs,tt) } - - =g= - - CO2_STORED(r,cs,h,t) -; - -* --------------------------------------------------------------------------- - -eq_co2_sink(r,h,t)$[tmodel(t)$Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)].. - -*the amount of co2 stored from r in all of its cs sites - sum{cs$r_cs(r,cs), CO2_STORED(r,cs,h,t) } - - =e= - -* local CO2 entering storage -* note we can substitute the equation above into here -* and avoid the creation of a new variable -* -but- it is nice to have this for reporting/tracking - CO2_CAPTURED(r,h,t) - -* net trade - + sum{rr$co2_routes(r,rr), CO2_FLOW(rr,r,h,t) - CO2_FLOW(r,rr,h,t) } -; - -* --------------------------------------------------------------------------- - -eq_co2_injection_limit(cs,h,t)$[Sw_CO2_Detail$tmodel(t)$(yeart(t)>=co2_detail_startyr)$csfeas(cs)].. - -* exogenously defined injection limit - co2_injection_limit(cs) - - =g= - -* must exceed metric tons per hour entering storage - sum{r$r_cs(r,cs), CO2_STORED(r,cs,h,t) } -; - -* --------------------------------------------------------------------------- - -eq_co2_cumul_limit(cs,t)$[tmodel(t)$Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)$csfeas(cs)].. - -*capacity by co2 bin for injections - co2_storage_limit(cs) - - =g= - -*cumulative amount stored over time - sum{(r,h,tt) - $[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))$(yeart(tt)>=co2_detail_startyr) - $r_cs(r,cs)$h_rep(h)], - yearweight(tt) * hours(h) * CO2_STORED(r,cs,h,tt) } -; -* --------------------------------------------------------------------------- - -*=================== -* -- FLEXIBLE CCS -- -*=================== - -* --------------------------------------------------------------------------- - -eq_ccsflex_byp_ccsenergy_limit(i,v,r,h,t)$[tmodel(t)$valgen(i,v,r,t)$ccsflex_byp(i)$Sw_CCSFLEX_BYP].. - CCSFLEX_POW(i,v,r,h,t) =l= ccsflex_powlim(i,t) * (GEN(i,v,r,h,t) + CCSFLEX_POW(i,v,r,h,t)) -; - -* --------------------------------------------------------------------------- - -eq_ccsflex_sto_ccsenergy_limit_szn(i,v,r,szn,t)$[tmodel(t)$valgen(i,v,r,t)$ccsflex_sto(i)$szn_rep(szn)$Sw_CCSFLEX_STO].. - sum{h$h_szn(h,szn), hours(h) * CCSFLEX_POW(i,v,r,h,t)} =l= ccsflex_powlim(i,t) * sum{h$h_szn(h,szn), hours(h) * (GEN(i,v,r,h,t) + CCSFLEX_POW(i,v,r,h,t))} -; - -* --------------------------------------------------------------------------- - -eq_ccsflex_sto_ccsenergy_balance(i,v,r,szn,t)$[valgen(i,v,r,t)$ccsflex_sto(i)$tmodel(t)$szn_rep(szn)$Sw_CCSFLEX_STO$(Sw_CCSFLEX_STO_LEVEL=0)].. - sum{h$h_szn(h,szn), hours(h) * CCSFLEX_POWREQ (i,v,r,h,t) } =e= sum{h$h_szn(h,szn), hours(h) * CCSFLEX_POW(i,v,r,h,t) } ; -; - -* --------------------------------------------------------------------------- - -eq_ccsflex_sto_storage_level(i,v,r,h,t)$[valgen(i,v,r,t)$ccsflex_sto(i)$tmodel(t)$Sw_CCSFLEX_STO$(Sw_CCSFLEX_STO_LEVEL=1)].. - -*[plus] storage level in h+1 - sum{(hh)$[nexth(h,hh)], CCSFLEX_STO_STORAGE_LEVEL(i,v,r,hh,t) } - - =e= - -* only want to include storage_level from periods that have had a previous storage_level -* otherwise it becomes a free variable, implying you can charge storage without bound - CCSFLEX_STO_STORAGE_LEVEL(i,v,r,h,t) - -*[plus] storage charging - + ccsflex_sto_storage_eff(i,t) * hours_daily(h) * CCSFLEX_POWREQ(i,v,r,h,t) - -*[minus] storage discharge -*exclude hybrid PV+battery because GEN refers to output from both the PV and the battery - - hours_daily(h) * CCSFLEX_POW(i,v,r,h,t) -; - -* --------------------------------------------------------------------------- - -eq_ccsflex_sto_storage_level_max(i,v,r,h,t)$[valgen(i,v,r,t)$valcap(i,v,r,t)$ccsflex(i)$tmodel(t)$Sw_CCSFLEX_STO$(Sw_CCSFLEX_STO_LEVEL=1)].. - -* [plus] storage duration times storage capacity - ccsflex_sto_storage_duration(i) * CCSFLEX_STO_STORAGE_CAP(i,v,r,t) - - =g= - - CCSFLEX_STO_STORAGE_LEVEL(i,v,r,h,t) -; - -* --------------------------------------------------------------------------- diff --git a/c_supplyobjective.gms b/c_supplyobjective.gms deleted file mode 100644 index 38a5b0b3..00000000 --- a/c_supplyobjective.gms +++ /dev/null @@ -1,369 +0,0 @@ -$ontext -No globals needed for this file -$offtext - -scalar cost_scale "scaling parameter for the objective function" /1/ ; - -Equation -* objective function calculation - eq_ObjFn "--$s-- Objective function calculation" - eq_ObjFn_inv(t) "--$s-- Calculation of investment component of the objective function" - eq_Objfn_op(t) "--$s-- Calculation of operations component of the objective function" -; - -* note these are not restricited to positive domain -Variable Z "--$-- total cost of operations and investment, scale varies based on cost_scale" - Z_op(t) "--$-- total cost of operations", - Z_inv(t) "--$-- total cost of operations" -; - -* objective function is the sum over modeled years of the investment -* and operations components -eq_ObjFn.. Z =e= cost_scale * sum{t$tmodel(t), Z_inv(t) + Z_op(t) } ; - -*======================================================= -* -- Investment component of the objective function -- -*======================================================= - -eq_ObjFn_inv(t)$tmodel(t).. - - Z_inv(t) - - =e= - - pvf_capital(t) * - - ( -* --- investment costs --- - + sum{(i,v,r)$valinv(i,v,r,t), - cost_cap_fin_mult(i,r,t) * cost_cap(i,t) * INV(i,v,r,t) - } - - + sum{(i,v,r)$[valinv(i,v,r,t)$battery(i)], - cost_cap_fin_mult(i,r,t) * cost_cap_energy(i,t) * INV_ENERGY(i,v,r,t) - } - -* --- penalty for exceeding interconnection queue limit --- - + sum{(tg,r), cap_penalty(tg) * CAP_ABOVE_LIM(tg,r,t) } - -* --- growth penalties --- - + sum{(gbin,i,st)$[sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)], - cost_growth(i,st,t) * growth_penalty(gbin) * (yeart(t) - sum{tt$[tprev(t,tt)], yeart(tt) }) * GROWTH_BIN(gbin,i,st,t) - }$[(yeart(t)>=model_builds_start_yr)$Sw_GrowthPenalties$(yeart(t)<=Sw_GrowthPenLastYear)] - -* --- cost of upgrading--- - + sum{(i,v,r)$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], - cost_upgrade(i,v,r,t) * cost_cap_fin_mult(i,r,t) * UPGRADES(i,v,r,t) } - -* --- costs of resource supply curve spur line investment if not modeling explicitly--- -*Note that cost_cap for hydro, pumped-hydro, and geo techs are zero -*but hydro and geo rsc_fin_mult is equal to the same value as cost_cap_fin_mult -* Note: for OSW, export cable, inter-array and POI/substations are eligible for ITC. The rest are not. -* However we apply the ITC to all transmission costs to be consistent with LBW format - + sum{(i,v,r,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$(not spur_techs(i))], - m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) * sum{ii$rsc_agg(i,ii), INV_RSC(ii,v,r,rscbin,t) } } - -* ---cost of spur lines modeled explicitly--- -* NOTE: no rsc_fin_mult(i,r,t) here, but it's 1 for upv and wind-ons anyway - + sum{x$[Sw_SpurScen$xfeas(x)], - spurline_cost(x) * Sw_SpurCostMult * INV_SPUR(x,t) } - -* --- cost of intra-zone network reinforcement (a.k.a. point-of-interconnection capacity or POI) -* Sw_TransIntraCost is in $/kW, so multiply by 1000 to convert to $/MW - + sum{r$Sw_TransIntraCost, - trans_cost_cap_fin_mult(t) * Sw_TransIntraCost * 1000 * INV_POI(r,t) } - -* --- cost of water access--- - + [ (8760/1E6) * sum{ (i,v,w,r)$[i_w(i,w)$valinv(i,v,r,t)], sum{wst$i_wst(i,wst), - m_watsc_dat(wst,"cost",r,t) } * water_rate(i,w) * - ( INV(i,v,r,t) + INV_REFURB(i,v,r,t)$[refurbtech(i)$Sw_Refurb] ) } - + sum{(rscbin,i,v,r)$[m_rscfeas(r,i,rscbin)$psh(i)], - sum{wst$i_wst(i,wst), m_watsc_dat(wst,"cost",r,t) } * - ( INV_RSC(i,v,r,rscbin,t) * water_req_psh(r,rscbin) ) }$Sw_PSHwatercon - ]$Sw_WaterMain - -*slack variable to update water source type (wst) in the unit database -*Note that existing wst data is not consistent with availability of water source in the region - + sum{(wst,r), 1E6 * WATER_CAPACITY_LIMIT_SLACK(wst,r,t) }$[Sw_WaterMain$Sw_WaterCapacity] - -* --- cost of refurbishments of RSC tech--- - + sum{(i,v,r)$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], - cost_cap_fin_mult(i,r,t) * cost_cap(i,t) * INV_REFURB(i,v,r,t) - } - -* --- cost of interzonal AC transmission--- - + sum{(r,rr,tscbin)$[routes_inv(r,rr,"AC",t)$tsc_binwidth(r,rr,tscbin)], - trans_cost_cap_fin_mult(t) * TRAN_CAPEX_BINS(r,rr,tscbin,t) } - -* --- cost of interzonal HVDC transmission--- -* transmission lines: 1 MW adds 1 MW to both INVTRAN(r,rr) and INVTRAN(rr,r) so divide by 2 - + sum{(r,rr,trtype)$[routes_inv(r,rr,trtype,t)$(not aclike(trtype))], - trans_cost_cap_fin_mult(t) - * transmission_cost_nonac(r,rr,trtype) - * INVTRAN(r,rr,trtype,t) - / 2 } - -* LCC and B2B AC/DC converter stations: each interface has two, one on either side of the interface, -* but each interface shows up in both INVTRAN(r,rr) and INVTRAN(rr,r) so don't multiply by 2 - + sum{(r,rr,trtype)$[lcclike(trtype)$routes_inv(r,rr,trtype,t)], - trans_cost_cap_fin_mult(t) * cost_acdc_lcc * INVTRAN(r,rr,trtype,t) } - -* VSC AC/DC converter stations - + sum{r, - trans_cost_cap_fin_mult(t) * cost_acdc_vsc * INV_CONVERTER(r,t) } - -* --- storage capacity credit--- -*small cost penalty to incentivize solver to fill shorter-duration bins first - + sum{(i,v,r,ccseason,sdbin)$[valcap(i,v,r,t)$(storage(i) or hyd_add_pump(i))$(not csp(i))$Sw_PRM_CapCredit$Sw_StorageBinPenalty], - bin_penalty(sdbin) * CAP_SDBIN(i,v,r,ccseason,sdbin,t) } - -* cost of capacity upsizing - + sum{(i,v,r,rscbin)$allow_cap_up(i,v,r,rscbin,t), - cost_cap_fin_mult(i,r,t) * INV_CAP_UP(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } - -* cost of energy upsizing - + sum{(i,v,r,rscbin)$allow_ener_up(i,v,r,rscbin,t), - cost_cap_fin_mult(i,r,t) * INV_ENER_UP(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } - -* H2 transport network investment costs - + sum{(r,rr)$h2_routes_inv(r,rr), cost_h2_transport_cap(r,rr,t) * H2_TRANSPORT_INV(r,rr,t) }$(Sw_H2 = 2) - -* H2 storage investment costs - + sum{(h2_stor,r)$h2_stor_r(h2_stor,r), cost_h2_storage_cap(h2_stor,t) * H2_STOR_INV(h2_stor,r,t) }$(Sw_H2 = 2) - -* CO2 pipeline investment costs - + sum{(r,rr)$co2_routes(r,rr), cost_co2_pipeline_cap(r,rr,t) * CO2_TRANSPORT_INV(r,rr,t) - }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] - - + sum{(r,cs)$[csfeas(cs)$r_cs(r,cs)], cost_co2_spurline_cap(r,cs,t) * CO2_SPURLINE_INV(r,cs,t) - }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] - -*end to multiplier by pvf_capital - ) -; - -*======================================================= -* -- Operational component of the objective function -- -*======================================================= - -eq_Objfn_op(t)$tmodel(t).. - - Z_op(t) - - =e= - - pvf_onm(t) * ( - -* --- variable O&M costs--- -* all technologies except hybrid plant and DAC - sum{(i,v,r,h)$[valgen(i,v,r,t)$cost_vom(i,v,r,t)$(not storage_hybrid(i)$(not csp(i)))], - hours(h) * cost_vom(i,v,r,t) * GEN(i,v,r,h,t) } - -* hybrid plant (plant) - + sum{(i,v,r,h)$[valgen(i,v,r,t)$cost_vom_pvb_p(i,v,r,t)$storage_hybrid(i)$(not csp(i))], - hours(h) * cost_vom_pvb_p(i,v,r,t) * GEN_PLANT(i,v,r,h,t) }$Sw_HybridPlant - -* hybrid plant (Battery) - + sum{(i,v,r,h)$[valgen(i,v,r,t)$cost_vom_pvb_b(i,v,r,t)$storage_hybrid(i)$(not csp(i))], - hours(h) * cost_vom_pvb_b(i,v,r,t) * GEN_STORAGE(i,v,r,h,t) }$Sw_HybridPlant - -* --- fixed O&M costs--- -* generation - + sum{(i,v,r)$[valcap(i,v,r,t)], - cost_fom(i,v,r,t) * CAP(i,v,r,t) } - - + sum{(i,v,r)$[valcap(i,v,r,t)$battery(i)], - cost_fom_energy(i,v,r,t) * CAP_ENERGY(i,v,r,t) } - -* transmission lines - + sum{(r,rr,trtype)$routes(r,rr,trtype,t), - transmission_line_fom(r,rr,trtype) * CAPTRAN_ENERGY(r,rr,trtype,t) } - -* LCC and B2B AC/DC converter stations - + sum{(r,rr,trtype)$[lcclike(trtype)$routes(r,rr,trtype,t)], - cost_acdc_lcc * 2 * trans_fom_frac * CAPTRAN_ENERGY(r,rr,trtype,t) } - -* VSC AC/DC converter stations - + sum{r, - cost_acdc_vsc * trans_fom_frac * CAP_CONVERTER(r,t) } - -* spur lines modeled as part of supply curve - + sum{(i,v,r,rscbin) - $[m_rscfeas(r,i,rscbin)$valcap(i,v,r,t) - $rsc_i(i)$(not spur_techs(i))$(not sccapcosttech(i))], - m_rsc_dat(r,i,rscbin,"cost_trans") * trans_fom_frac * CAP_RSC(i,v,r,rscbin,t) } - -* spur lines modeled explicitly - + sum{x$[Sw_SpurScen$xfeas(x)], - spurline_cost(x) * trans_fom_frac * CAP_SPUR(x,t) } - -* intra-zone network reinforcement (only for new capacity; don't include it for existing POI -* capacity because it's not a great estimate of the actual FOM cost of all existing transmission) - + sum{r$Sw_TransIntraCost, - Sw_TransIntraCost * 1000 * trans_fom_frac - * sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], INV_POI(r,tt) } } - -* --- penalty for retiring a technology (represents friction in retirements)--- - - sum{(i,v,r)$[valcap(i,v,r,t)$retiretech(i,v,r,t)$Sw_RetirePenalty], - cost_fom(i,v,r,t) * retire_penalty(t) * - (CAP(i,v,r,t) - - INV(i,v,r,t)$valinv(i,v,r,t) - - INV_REFURB(i,v,r,t)$[valinv(i,v,r,t)$refurbtech(i)$Sw_Refurb] - - UPGRADES(i,v,r,t)$[upgrade(i)$Sw_Upgrades] ) - } - -* ---operating reserve costs--- - + sum{(i,v,r,h,ortype)$[Sw_OpRes$valgen(i,v,r,t)$cost_opres(i,ortype,t)$reserve_frac(i,ortype)$opres_model(ortype)$opres_h(h)], - hours(h) * cost_opres(i,ortype,t) * OPRES(ortype,i,v,r,h,t) } - - -* --- cost of coal, nuclear, and other fixed-price fuels (except coal used for cofiring), -* plus cost of H2 fuel when using fixed price (Sw_H2=0) or during stress periods. -* When using endogenous H2 price (Sw_H2=1 or Sw_H2=2), H2 fuel cost is captured elsewhere -* via the capex + opex costs of H2 production and its associated electricity demand. - + sum{(i,v,r,h)$[valgen(i,v,r,t)$heat_rate(i,v,r,t) - $(not gas(i))$(not bio(i))$(not cofire(i)) - $((not h2_combustion(i)) or h2_combustion(i)$[(Sw_H2=0) or h_stress(h)])], - hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN(i,v,r,h,t) } - -* --- startup/ramping costs - + sum{(i,r,h,hh)$[Sw_StartCost$startcost(i)$numhours_nexth(h,hh)$valgen_irt(i,r,t)], - startcost(i) * numhours_nexth(h,hh) * RAMPUP(i,r,h,hh,t) } - -* --cofire coal consumption--- -* cofire bio consumption already accounted for in accounting of BIOUSED - + sum{(i,v,r,h)$[valgen(i,v,r,t)$cofire(i)$heat_rate(i,v,r,t)], - (1-bio_cofire_perc) * hours(h) * heat_rate(i,v,r,t) - * fuel_price("coal-new",r,t) * GEN(i,v,r,h,t) } - -* --- cost of natural gas--- -*Sw_GasCurve = 2 (static natural gas prices) -*first - gas consumed for electricity generation - + sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)$(Sw_GasCurve = 2)], - hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN(i,v,r,h,t) } - -*second - gas consumed by gas-powered DAC - + sum{(v,r,h)$[valcap("dac_gas",v,r,t)$(Sw_GasCurve = 2)], - hours(h) * dac_gas_cons_rate("dac_gas",v,t) * PRODUCE("DAC","dac_gas",v,r,h,t) }$Sw_DAC_Gas - -*Sw_GasCurve = 0 (census division supply curves natural gas prices) - + sum{(cendiv,gb), sum{h, hours(h) * GASUSED(cendiv,gb,h,t) } - * gasprice(cendiv,gb,t) - }$(Sw_GasCurve = 0) - -*Sw_GasCurve = 3 (national supply curve for natural gas prices with census division multipliers) - + sum{(h,cendiv,gb), hours(h) * GASUSED(cendiv,gb,h,t) - * gasadder_cd(cendiv,t,h) + gasprice_nat_bin(gb,t) - }$(Sw_GasCurve = 3) - -*Sw_GasCurve = 1 (national and census division supply curves for natural gas prices) -*first - anticipated costs of gas consumption given last year's amount - + (sum{(i,r,v,cendiv,h)$[valgen(i,v,r,t)$gas(i)], - gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * - hours(h) * heat_rate(i,v,r,t) * GEN(i,v,r,h,t) } - -*second - adjustments based on changes from last year's consumption at the regional and national level - + sum{(fuelbin,cendiv), - gasbinp_regional(fuelbin,cendiv,t) * VGASBINQ_REGIONAL(fuelbin,cendiv,t) } - - + sum{(fuelbin), - gasbinp_national(fuelbin,t) * VGASBINQ_NATIONAL(fuelbin,t) } - - )$[Sw_GasCurve = 1] - -* ---cost of biofuel consumption and biomass transport--- - + sum{(r,bioclass)$[sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,t) }], - BIOUSED(bioclass,r,t) * - (sum{usda_region$r_usda(r,usda_region), biosupply(usda_region, bioclass, "price") } + bio_transport_cost) } - -* --- hurdle costs for transmission flow --- - + sum{(r,rr,h,trtype)$[routes(r,rr,trtype,t)$cost_hurdle(r,rr,t)], - cost_hurdle(r,rr,t) * FLOW(r,rr,h,t,trtype) * hours(h) } - -* --- taxes on emissions--- - + sum{(e,r), (EMIT("process",e,r,t) + EMIT("upstream",e,r,t)$Sw_Upstream) * emit_tax(e,r,t) } - -* --cost of CO2 transport and storage from CCS-- - + sum{(i,v,r,h)$[valgen(i,v,r,t)], - hours(h) * capture_rate("CO2",i,v,r,t) * GEN(i,v,r,h,t) * Sw_CO2_Storage }$[not Sw_CO2_Detail] - -* --cost of CO2 transport and storage from SMR CCS-- - + sum{(p,v,r,h)$[i_p("smr_ccs",p)$valcap("smr_ccs",v,r,t)], - hours(h) * smr_capture_rate * smr_co2_intensity * PRODUCE(p,"smr_ccs",v,r,h,t) * Sw_CO2_Storage }$[Sw_H2$(not Sw_CO2_Detail)] - -* --cost of CO2 transport and storage from DAC-- - + sum{(p,i,v,r,h)$[dac(i)$valcap(i,v,r,t)$i_p(i,p)], - hours(h) * PRODUCE(p,i,v,r,h,t) * Sw_CO2_Storage }$[Sw_DAC$(not Sw_CO2_Detail)] - -* ---State RPS alternative compliance payments--- - + sum{(RPSCat,st)$[(stfeas(st) or sameas(st,"voluntary"))$RecPerc(RPSCat,st,t)$(not acp_disallowed(st,RPSCat))], - acp_price(st,t) * ACP_PURCHASES(RPSCat,st,t) - }$[(yeart(t)>=firstyear_RPS)$Sw_StateRPS] - -* --- revenues from purchases of curtailed VRE--- - - sum{(r,h), CURT(r,h,t) * hours(h) * cost_curt(t) }$Sw_CurtMarket - -* --- dropped/excess load (ONLY if before Sw_StartMarkets) - + sum{(r,h)$[(yeart(t)=co2_detail_startyr)] - -* --- CO2 spurline fixed OM costs - + sum{(r,cs)$[csfeas(cs)$r_cs(r,cs)], cost_co2_spurline_fom(r,cs,t) - * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], - CO2_SPURLINE_INV(r,cs,tt) } }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] - -* --- CO2 injection break even costs - + sum{(r,cs,h)$r_cs(r,cs), hours(h) * CO2_STORED(r,cs,h,t) * cost_co2_stor_bec(cs,t) }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] - -* --- Tax credit for CO2 stored --- -* note conversion to 12-year CRF given length of CO2 captured incentive payments - - sum{(i,v,r,h)$[valgen(i,v,r,t)$co2_captured_incentive(i,v,r,t)], - (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * capture_rate("CO2",i,v,r,t) * GEN(i,v,r,h,t)} - -* --- Tax credit for CO2 stored for DAC --- - - sum{(p,i,v,r,h)$[dac(i)$valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], - (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * PRODUCE(p,i,v,r,h,t)} - -* --- PTC value for electric power generation --- - - sum{(i,v,r,h)$[valgen(i,v,r,t)$ptc_value_scaled(i,v,t)], - hours(h) * ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t) * - (GEN(i,v,r,h,t) - (STORAGE_IN_GRID(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[pvb(i)$Sw_PVB]) - } - -* --- PTC value for hydrogen production --- -* Note: all electrolyzers which produce H2 are assuming to be receiving the hydrogen production tax credit during eligible years - - sum{(p,v,r,h)$[valcap("electrolyzer",v,r,t)$(sameas(p,"H2"))$h2_ptc("electrolyzer",v,r,t)$h_rep(h)], - hours(h) * PRODUCE(p,"electrolyzer",v,r,h,t) * - (crf(t) / crf_h2_incentive(t)) * h2_ptc("electrolyzer",v,r,t) * 1e3} - $[Sw_H2_PTC$Sw_H2$h2_ptc_years(t)$(yeart(t) >= h2_demand_start)] - -*end multiplier for pvf_onm - ) -; diff --git a/cbc.opt b/cbc.opt deleted file mode 100644 index 28df6f39..00000000 --- a/cbc.opt +++ /dev/null @@ -1,3 +0,0 @@ -startalg barrier -crash off -threads 8 \ No newline at end of file diff --git a/cplex.op2 b/cplex.op2 deleted file mode 100644 index e01f19dd..00000000 --- a/cplex.op2 +++ /dev/null @@ -1,71 +0,0 @@ -*** https://www.gams.com/latest/docs/S_CPLEX.html -*** threads (integer): global default thread count -threads = 8 - -*** lpmethod (integer): algorithm to be used for LP problems [4 = barrier] -lpmethod = 4 - -*** advind (integer): advanced basis use [0 = do not use advanced basis] -advind 0 - -*** reslim (integer): solve time limit in seconds [172800 seconds = 2 days] -reslim 172800 - -*** scaind (integer): matrix scaling on/off [1 = modified, more aggressive scaling method] -scaind 1 - -*** aggind (integer): aggregator on/off [5 = aggregator will be applied 5 times] -aggind 5 - -*** iis (boolean): run the conflict refiner also known as IIS finder if the problem is infeasible -iis 1 - -*** eprhs (float): feasibility tolerance [default = 1e-6] -eprhs = 1e-5 - -*** epopt (float): optimality tolerance [default = 1e-6] -epopt = 1e-5 - -*** memoryemphasis (boolean): reduces use of memory but writes TBs of files to disk -* memoryemphasis 1 - -*** epmrk (float): Markowitz pivot tolerance -*epmrk = 0.1 - -*** barepcomp (float): tolerance on complementarity for convergence of the barrier algorithm [default 1e-8] -barepcomp = 1e-10 - -*############################################################################################ -*## The settings below can reduce solve time substantially, but their impacts on the solution -*## have not yet been fully characterized, and some of them might do nothing. Use at your own risk. -*## Some background details are available at -*## https://www.slideshare.net/IEA-ETSAP/improving-the-solution-time-of-times-by-playing-with-cplexbarrier -*## https://iea-etsap.org/webinar/CPLEX%20options%20for%20running%20TIMES%20models.pdf - -*** numericalemphasis (boolean): emphasizes precision in numerically unstable or difficult problems [default 0] -* numericalemphasis 1 - -*** depind (integer): dependency checker on/off. 3 = turn on at beginning and end of preprocessing [default -1] -* depind 3 - -*** barcolnz (integer): specifies the number of entries in columns to be considered as dense [default 0] -*** commenting this out (i.e., setting back to zero) can improve solve time when running with hydrogen transport (GSw_H2=2) -barcolnz 100 - -*** barorder (integer): row ordering algorithm selection. 1 = approximate minimum degree (AMD) [default 0] -* barorder 1 - -*** baralg (integer): barrier algorithm selection. 1 = infeasibility-estimate start, 3 = standard barrier [default 0] -* baralg 3 - -*** barstartalg (integer): barrier starting point algorithm. 2 = default primal, estimate dual [default 1] -* barstartalg 2 - -*** scaind (described above) -* scaind 0 - -*** Can use bardisplay=2 to print more diagnostics about the barrier method and choice of barcolnz -* bardisplay 2 - -*** solutiontype (integer): type of solution (basic or non basic): 0 does basic with crossover, 2 skips crossover -* solutiontype 2 diff --git a/cplex.opt b/cplex.opt deleted file mode 100644 index 0a6c3ea6..00000000 --- a/cplex.opt +++ /dev/null @@ -1,71 +0,0 @@ -*** https://www.gams.com/latest/docs/S_CPLEX.html -*** threads (integer): global default thread count -threads = 8 - -*** lpmethod (integer): algorithm to be used for LP problems [4 = barrier] -lpmethod = 4 - -*** advind (integer): advanced basis use [0 = do not use advanced basis] -advind 0 - -*** reslim (integer): solve time limit in seconds [172800 seconds = 2 days] -reslim 172800 - -*** scaind (integer): matrix scaling on/off [1 = modified, more aggressive scaling method] -scaind 1 - -*** aggind (integer): aggregator on/off [5 = aggregator will be applied 5 times] -aggind 5 - -*** iis (boolean): run the conflict refiner also known as IIS finder if the problem is infeasible -iis 1 - -*** eprhs (float): feasibility tolerance [default = 1e-6] -eprhs = 1e-5 - -*** epopt (float): optimality tolerance [default = 1e-6] -epopt = 1e-6 - -*** memoryemphasis (boolean): reduces use of memory but writes TBs of files to disk -memoryemphasis 0 - -*** epmrk (float): Markowitz pivot tolerance -*epmrk = 0.1 - -*** barepcomp (float): tolerance on complementarity for convergence of the barrier algorithm [default 1e-8] -barepcomp = 1e-8 - -*############################################################################################ -*## The settings below can reduce solve time substantially, but their impacts on the solution -*## have not yet been fully characterized, and some of them might do nothing. Use at your own risk. -*## Some background details are available at -*## https://www.slideshare.net/IEA-ETSAP/improving-the-solution-time-of-times-by-playing-with-cplexbarrier -*## https://iea-etsap.org/webinar/CPLEX%20options%20for%20running%20TIMES%20models.pdf - -*** numericalemphasis (boolean): emphasizes precision in numerically unstable or difficult problems [default 0] -* numericalemphasis 1 - -*** depind (integer): dependency checker on/off. 3 = turn on at beginning and end of preprocessing [default -1] -* depind 3 - -*** barcolnz (integer): specifies the number of entries in columns to be considered as dense [default 0] -*** 0 lets CPLEX choose; values ranging from 30-300 can sometimes shorten runtime -barcolnz 30 - -*** barorder (integer): row ordering algorithm selection. 1 = approximate minimum degree (AMD) [default 0] -* barorder 1 - -*** baralg (integer): barrier algorithm selection. 1 = infeasibility-estimate start, 3 = standard barrier [default 0] -* baralg 1 - -*** barstartalg (integer): barrier starting point algorithm. 2 = default primal, estimate dual [default 1] -* barstartalg 2 - -*** scaind (described above) -* scaind 0 - -*** Can use bardisplay=2 to print more diagnostics about the barrier method and choice of barcolnz -* bardisplay 2 - -*** solutiontype (integer): type of solution (basic or non basic): 0 does basic with crossover, 2 skips crossover -* solutiontype 2 diff --git a/d1_financials.gms b/d1_financials.gms deleted file mode 100644 index e6e6815f..00000000 --- a/d1_financials.gms +++ /dev/null @@ -1,156 +0,0 @@ -* --- Ingest tax credit phaseout mult --- * - -$gdxin outputs%ds%tc_phaseout_data%ds%tc_phaseout_mult_%cur_year%.gdx -$loaddcr tc_phaseout_mult_t_load = tc_phaseout_mult_t -$gdxin - -tc_phaseout_mult_t(i,t)$tload(t) = tc_phaseout_mult_t_load(i,t) ; - -* If tcphaseout is enabled, overwrite initialization -* This requires re-calculating cost_cap_fin_mult and its various permutations -if(Sw_TCPhaseout > 0, -* Apply the phaseout multiplier of the current level for current year -* and all future builds. Note that the value will remain the same for -* the cur_year-available vintage but can be updated for vintages whose -* first year hasn't solved yet. i.e. tc_phaseout_mult will remain constant for all -* current and historically-buildable plants but future plants may get updated. -tc_phaseout_mult(i,v,t)$[tload(t)$(firstyear_v(i,v)>=%cur_year%)] = - tc_phaseout_mult_t(i,t) ; -); - - -* --- Start calculations of cost_cap_fin_mult family of parameters --- * - -cost_cap_fin_mult(i,r,t) = ccmult(i,t) / (1.0 - tax_rate(t)) - * (1.0-tax_rate(t) * (1.0 - (itc_frac_monetized(i,t) * itc_energy_comm_bonus(i,r) * tc_phaseout_mult_t(i,t)/2.0) ) - * pv_frac_of_depreciation(i,t) - itc_frac_monetized(i,t) * tc_phaseout_mult_t(i,t)) - * degradation_adj(i,t) * financing_risk_mult(i,t) * (1 + reg_cap_cost_diff(i,r)) - * eval_period_adj_mult(i,t) ; - -cost_cap_fin_mult_noITC(i,r,t) = ccmult(i,t) / (1.0 - tax_rate(t)) - * (1.0-tax_rate(t)*pv_frac_of_depreciation(i,t)) * degradation_adj(i,t) - * financing_risk_mult(i,t) * (1 + reg_cap_cost_diff(i,r)) * eval_period_adj_mult(i,t) ; - -cost_cap_fin_mult_no_credits(i,r,t) = ccmult(i,t) * (1 + reg_cap_cost_diff(i,r)) ; - -* Assign the PV portion of PVB the value of UPV -cost_cap_fin_mult_pvb_p(i,r,t)$pvb(i) = - sum{ii$[upv(ii)$rsc_agg(ii,i)], cost_cap_fin_mult(ii,r,t) } ; - -cost_cap_fin_mult_pvb_p_noITC(i,r,t)$pvb(i) = - sum{ii$[upv(ii)$rsc_agg(ii,i)], cost_cap_fin_mult_noITC(ii,r,t) } ; - -cost_cap_fin_mult_pvb_p_no_credits(i,r,t)$pvb(i) = - sum{ii$[upv(ii)$rsc_agg(ii,i)], cost_cap_fin_mult_no_credits(ii,r,t) } ; - -* In the financing module (python), PVB refers to the battery portion of the hybrid. -* This convention is used to estimate the ITC benefit for the battery. -* Assign the battery portion of PVB the value computed in the financing module for PVB -cost_cap_fin_mult_pvb_b(i,r,t)$pvb(i) = cost_cap_fin_mult(i,r,t) ; -cost_cap_fin_mult_pvb_b_noITC(i,r,t)$pvb(i) = cost_cap_fin_mult_noITC(i,r,t) ; -cost_cap_fin_mult_pvb_b_no_credits(i,r,t)$pvb(i) = cost_cap_fin_mult_no_credits(i,r,t) ; - -* Assign "cost_cap_fin_mult" for PVB to be the weighted average of the PV and battery portions -* The weighting is based on: -* (1) the cost of each portion: PV=cost_cap_pvb_p; Battery=cost_cap_pvb_b -* (2) the relative size of each portion: PV=1; Battery=bcr -* The "-1" and "+1" values are needed because the multipliers are adjustments off of 1.0 -cost_cap_fin_mult(i,r,t)$pvb(i) = - ( (cost_cap_fin_mult_pvb_p(i,r,t) - 1) * cost_cap_pvb_p(i,t) - + bcr(i) * (cost_cap_fin_mult_pvb_b(i,r,t) - 1) * cost_cap_pvb_b(i,t) ) - / (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) + 1 ; - -cost_cap_fin_mult_noITC(i,r,t)$pvb(i) = - ( (cost_cap_fin_mult_pvb_p_noITC(i,r,t) - 1) * cost_cap_pvb_p(i,t) - + bcr(i) * (cost_cap_fin_mult_pvb_b_noITC(i,r,t) - 1) * cost_cap_pvb_b(i,t) ) - / (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) + 1 ; - -cost_cap_fin_mult_no_credits(i,r,t)$pvb(i) = - ((cost_cap_fin_mult_pvb_p_no_credits(i,r,t) - 1) * cost_cap_pvb_p(i,t) - + bcr(i) * (cost_cap_fin_mult_pvb_b_no_credits(i,r,t) - 1) * cost_cap_pvb_b(i,t)) - / (cost_cap_pvb_p(i,t) + bcr(i) * cost_cap_pvb_b(i,t)) + 1 ; - -* --- Upgrades --- -* Assign upgraded techs the same multipliers as the techs they are upgraded from -* This assignment must take place after expanding for water techs, if applicable. - -if(Sw_WaterMain=1, -cost_cap_fin_mult(i,r,t)$i_water_cooling(i) = - sum{(ii)$[ctt_i_ii(i,ii)], cost_cap_fin_mult(ii,r,t) } ; - -cost_cap_fin_mult_noITC(i,r,t)$i_water_cooling(i) = - sum{(ii)$[ctt_i_ii(i,ii)], cost_cap_fin_mult_noITC(ii,r,t) } ; - -cost_cap_fin_mult_no_credits(i,r,t)$i_water_cooling(i) = - sum{(ii)$[ctt_i_ii(i,ii)], cost_cap_fin_mult_no_credits(ii,r,t) } ; -) ; - -cost_cap_fin_mult(i,r,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), cost_cap_fin_mult(ii,r,t) } ; -cost_cap_fin_mult_noITC(i,r,t)$upgrade(i) = sum{ii$upgrade_to(i,ii), cost_cap_fin_mult_noITC(ii,r,t) } ; - -* --- Nuclear Ban --- -* Assign increased cost multipliers to regions with state nuclear bans -if(Sw_NukeStateBan = 2, - cost_cap_fin_mult(i,r,t)$[nuclear(i)$nuclear_ba_ban(r)] = - cost_cap_fin_mult(i,r,t) * nukebancostmult ; - - cost_cap_fin_mult_noITC(i,r,t)$[nuclear(i)$nuclear_ba_ban(r)] = - cost_cap_fin_mult_noITC(i,r,t) * nukebancostmult ; -) ; - -* Start by setting all multipliers to 1 -rsc_fin_mult(i,r,t)$[valcap_irt(i,r,t)$rsc_i(i)] = 1 ; -rsc_fin_mult_noITC(i,r,t)$[valcap_irt(i,r,t)$rsc_i(i)] = 1 ; - -* Hydro, pumped-hydro, and dr-shed have capital costs included in the supply curve, -* so change their multiplier to be the same as cost_cap_fin_mult adjusted by their -* capital cost multipliers. -rsc_fin_mult(i,r,t)$hydro(i) = cost_cap_fin_mult('hydro',r,t) * hydrocapmult(t,i) ; -rsc_fin_mult_noITC(i,r,t)$hydro(i) = cost_cap_fin_mult_noITC('hydro',r,t) * hydrocapmult(t,i) ; -rsc_fin_mult(i,r,t)$psh(i) = cost_cap_fin_mult(i,r,t) * hydrocapmult(t,i) ; -rsc_fin_mult_noITC(i,r,t)$psh(i) = cost_cap_fin_mult_noITC(i,r,t) * hydrocapmult(t,i) ; -rsc_fin_mult(i,r,t)$dr_shed(i) = cost_cap_fin_mult(i,r,t)* dr_shed_capmult(i,r,t) ; -rsc_fin_mult_noITC(i,r,t)$dr_shed(i) = cost_cap_fin_mult_noITC(i,r,t)* dr_shed_capmult(i,r,t) ; - -* Create a new parameter to hold capital financing multipliers with and without ITC for OSW transmission costs inside the resource supply curve cost -* Currently, OSW receives federal incentives in both its capital and transmission costs, hence this custom application for OSW -rsc_fin_mult(i,r,t)$ofswind(i) = cost_cap_fin_mult(i,r,t) * ofswind_rsc_mult(t,i) ; -rsc_fin_mult_noITC(i,r,t)$ofswind(i) = cost_cap_fin_mult_noITC(i,r,t) ; - -* Trim the cost_cap_fin_mult parameters to reduce file sizes -cost_cap_fin_mult(i,r,t)$[(not valinv_irt(i,r,t))$(not upgrade(i)) - $(not sum{(v,rscbin), allow_cap_up(i,v,r,rscbin,t) }) - $(not sum{(v,rscbin), allow_ener_up(i,v,r,rscbin,t) })] = 0 ; - -cost_cap_fin_mult_noITC(i,r,t)$[(not valinv_irt(i,r,t))$(not upgrade(i)) - $(not sum{(v,rscbin), allow_cap_up(i,v,r,rscbin,t) }) - $(not sum{(v,rscbin), allow_ener_up(i,v,r,rscbin,t) })] = 0 ; - -cost_cap_fin_mult_no_credits(i,r,t)$[(not valinv_irt(i,r,t))$(not upgrade(i)) - $(not sum{(v,rscbin), allow_cap_up(i,v,r,rscbin,t) }) - $(not sum{(v,rscbin), allow_ener_up(i,v,r,rscbin,t) })] = 0 ; - -* Round the cost_cap_fin_mult parameters, since they were re-calculated -cost_cap_fin_mult(i,r,t)$cost_cap_fin_mult(i,r,t) - = round(cost_cap_fin_mult(i,r,t),3) ; - -cost_cap_fin_mult_noITC(i,r,t)$cost_cap_fin_mult_noITC(i,r,t) - = round(cost_cap_fin_mult_noITC(i,r,t),3) ; - -cost_cap_fin_mult_no_credits(i,r,t)$cost_cap_fin_mult_no_credits(i,r,t) - = round(cost_cap_fin_mult_no_credits(i,r,t),3) ; - -rsc_fin_mult(i,r,t)$rsc_fin_mult(i,r,t) = round(rsc_fin_mult(i,r,t),3) ; - -* Set cost_cap_fin_mult_out equal to cost_cap_fin_mult before we alter cost_cap_fin_mult -* for state fossil retirement policies and/or a full-region zero-carbon policy. -cost_cap_fin_mult_out(i,r,t) = cost_cap_fin_mult(i,r,t) ; - -* Penalize new gas built within cost recovery period of 20 years for states that require -* fossil retirements if Sw_StateRPS=1 and/or within 20 years of a zero-carbon policy -cost_cap_fin_mult(i,r,t)$[gas(i)$valcap_irt(i,r,t)] = - cost_cap_fin_mult(i,r,t) - * max(sum{st$r_st(r,st), ng_crf_penalty_st(t,st) }$(not ccs(i))$Sw_StateRPS$(yeart(t)>=firstyear_RPS), - ng_crf_penalty_nat(i,t) ) ; - -* --- End calculations of cost_cap_fin_mult family of parameters --- * \ No newline at end of file diff --git a/d1_temporal_params.gms b/d1_temporal_params.gms deleted file mode 100644 index 444c7fc5..00000000 --- a/d1_temporal_params.gms +++ /dev/null @@ -1,910 +0,0 @@ -*============================================= -* -- Timeslices and seasons -- -*============================================= - -Sets -h_rep(allh) "representative timeslices" -/ -$offlisting -$include inputs_case%ds%%temporal_inputs%%ds%set_h.csv -$onlisting -/ - -$onempty -h_stress(allh) "stress timeslices for the current model year" -/ -$offlisting -$include inputs_case%ds%stress%stress_year%%ds%set_h.csv -$onlisting -/ -$offempty - - -szn_rep(allszn) "representative periods, or seasons if modeling full year" -/ -$offlisting -$include inputs_case%ds%%temporal_inputs%%ds%set_szn.csv -$onlisting -/ - -actualszn(allszn) "actual periods (each is described by a representative period)" -/ -$offlisting -$include inputs_case%ds%%temporal_inputs%%ds%set_actualszn.csv -$onlisting -/ - -$onempty -szn_stress(allszn) "stress periods for the current model year" -/ -$offlisting -$include inputs_case%ds%stress%stress_year%%ds%set_szn.csv -$onlisting -/ -$offempty -; - -* The h set contains h_rep and h_stress; the szn set containts szn_rep and szn_stress -h(allh) = no ; -h(allh)$[h_rep(allh)] = yes ; -h(allh)$[h_stress(allh)] = yes ; - -szn(allszn) = no ; -szn(allszn)$[szn_rep(allszn)] = yes ; -szn(allszn)$[szn_stress(allszn)] = yes ; - -Sets -$ONEMPTY -h_preh(allh, allh) "mapping set between one timeslice and all other timeslices earlier in that period" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%h_preh.csv -$include inputs_case%ds%stress%stress_year%%ds%h_preh.csv -$offdelim -$onlisting -/ -$OFFEMPTY - -h_szn(allh,allszn) "mapping of hour blocks to seasons" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%h_szn.csv -$include inputs_case%ds%stress%stress_year%%ds%h_szn.csv -$offdelim -$onlisting -/ - -h_szn_start(allszn,allh) "starting hour of each season" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%h_szn_start.csv -$include inputs_case%ds%stress%stress_year%%ds%h_szn_start.csv -$offdelim -$onlisting -/ - -h_szn_end(allszn,allh) "ending hour of each season" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%h_szn_end.csv -$include inputs_case%ds%stress%stress_year%%ds%h_szn_end.csv -$offdelim -$onlisting -/ - -* Inter-seasonal storage level (e.g. H2), which uses h_actualszn and nexth_actualszn, -* is only tracked over actual ("energy") periods, not stress periods -h_actualszn(allh,allszn) "mapping from rep timeslices to actual periods" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%h_actualszn.csv -$offdelim -$onlisting -/ -$ONEMPTY -szn_actualszn(allszn,allszn) "mapping from rep timeslices to actual periods" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%szn_actualszn.csv -$offdelim -$onlisting -/ -$OFFEMPTY -nexth_actualszn(allszn,allh,allszn,allh) "Mapping between one timeslice and the next for actual periods (szns)" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%nexth_actualszn.csv -$offdelim -$onlisting -/ - -nexth(allh,allh) "Mapping set between one timeslice (first) and the following (second)" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%nexth.csv -$include inputs_case%ds%stress%stress_year%%ds%nexth.csv -$offdelim -$onlisting -/ -$ONEMPTY -nextpartition(allszn,allszn) "Mapping between one partition (allszn) and the next" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%nextpartition.csv -$offdelim -$onlisting -/ -$OFFEMPTY -; - -* Record the stress periods for each model year -h_t(allh,t)$[tmodel(t)] = no ; -h_t(h,t)$[tmodel(t)] = yes ; - -szn_t(allszn,t)$[tmodel(t)] = no ; -szn_t(szn,t)$[tmodel(t)] = yes ; - -h_stress_t(allh,t)$[h_stress(allh)$tmodel(t)] = no ; -h_stress_t(h,t)$[h_stress(h)$tmodel(t)] = yes ; - -szn_stress_t(allszn,t)$[szn_stress(allszn)$tmodel(t)] = no ; -szn_stress_t(szn,t)$[szn_stress(szn)$tmodel(t)] = yes ; - -h_szn_t(allh,allszn,t)$[h_szn(allh,allszn)$tmodel(t)] = no ; -h_szn_t(h,szn,t)$[h_szn(h,szn)$tmodel(t)] = yes ; - -$offOrder -set starting_hour(allh) "starting hour without tz adjustments" - final_hour(allh) "final hour without tz adjustments" -; - -* find the minimum and maximum ordinal of modeled hours within each season -starting_hour(allh) = no ; -starting_hour(h)$[sum{szn,h_szn(h,szn)$(smin(hh$h_szn(hh,szn),ord(hh))=ord(h)) }] = yes ; - -final_hour(allh) = no ; -final_hour(h)$[sum{szn,h_szn(h,szn)$(smax(hh$h_szn(hh,szn),ord(hh))=ord(h)) }] = yes ; - -* note summing over szn to find the minimum/maximum ordered hour within that season -starting_hour_nowrap(allh) = no ; -starting_hour_nowrap(h)$[sum{szn, h_szn_start(szn,h) }$(not Sw_HourlyWrap)] = yes ; - -final_hour_nowrap(allh) = no ; -final_hour_nowrap(h)$[sum{szn, h_szn_end(szn,h) }$(not Sw_HourlyWrap)] = yes ; - -* Get the order of actual periods -nextszn(actualszn,actualsznn)$[(ord(actualsznn) = ord(actualszn) + 1)] = yes ; -nextszn(actualszn,actualsznn) - $[(ord(actualszn) = smax(actualsznnn, ord(actualsznnn))) - $(ord(actualsznn) = smin(actualsznnn, ord(actualsznnn)))] - = yes ; - -$onOrder - - -parameter hours(allh) "--hours-- number of hours in each time block" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%numhours.csv -$include inputs_case%ds%stress%stress_year%%ds%numhours.csv -$offdelim -$onlisting -/ ; - -parameter numdays(allszn) "--number of days-- number of days for each season" ; -numdays(allszn) = 0 ; -numdays(szn) = sum{h$h_szn(h,szn),hours(h) } / 24 ; -$ONEMPTY -parameter numpartitions(allszn) "--number of periods-- number of partitions for each season in timeseries" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%numpartitions.csv -$offdelim -$onlisting -/ ; -$OFFEMPTY -parameter numhours_nexth(allh,allhh) "--hours-- number of times hh follows h throughout year" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%numhours_nexth.csv -$offdelim -$onlisting -/ ; - -* Written by input_processing/hourly_writetimeseries.py -parameter frac_h_quarter_weights(allh,quarter) "--unitless-- fraction of timeslice associated with each quarter" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%frac_h_quarter_weights.csv -$include inputs_case%ds%stress%stress_year%%ds%frac_h_quarter_weights.csv -$offdelim -$onlisting -/ ; - -parameter frac_h_ccseason_weights(allh,ccseason) "--unitless-- fraction of timeslice associated with each ccseason" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%frac_h_ccseason_weights.csv -$include inputs_case%ds%stress%stress_year%%ds%frac_h_ccseason_weights.csv -$offdelim -$onlisting -/ ; - -szn_quarter_weights(allszn,quarter) = 0 ; -szn_quarter_weights(szn,quarter) = - sum{h$h_szn(h,szn), frac_h_quarter_weights(h,quarter) } - / sum{h$h_szn(h,szn), 1} ; - -szn_ccseason_weights(allszn,ccseason) = 0 ; -szn_ccseason_weights(szn,ccseason) = - sum{h$h_szn(h,szn), frac_h_ccseason_weights(h,ccseason) } - / sum{h$h_szn(h,szn), 1} ; - -hours_daily(allh) = 0 ; -hours_daily(h_rep) = %GSw_HourlyChunkLengthRep% ; -hours_daily(h_stress) = %GSw_HourlyChunkLengthStress% ; - - -*=============================================== -* -- Climate Adjustments to Transmission -- -*=============================================== - -trans_cap_delta(allh,t) = 0 ; -trans_cap_delta(h,t) = - climate_heuristics_finalyear('trans_summer_cap_delta') * climate_heuristics_yearfrac(t) - * sum{quarter$sameas(quarter,"summ"), frac_h_quarter_weights(h,quarter) }; - - -*============================================= -* -- Mexico and Canada -- -*============================================= - -$ifthene.Canada %GSw_Canada% == 1 -parameter can_imports_szn_frac(allszn) "--unitless-- [Sw_Canada=1] fraction of annual imports that occur in each season" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%can_imports_szn_frac.csv -$offdelim -$onlisting -/ ; - -parameter can_exports_h_frac(allh) "--unitless-- [Sw_Canada=1] fraction of annual exports by timeslice" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%can_exports_h_frac.csv -$offdelim -$onlisting -/ ; - -can_imports_szn(r,allszn,t) = 0 ; -can_imports_szn(r,szn,t) = can_imports(r,t) * can_imports_szn_frac(szn) ; -can_exports_h(r,allh,t) = 0 ; -can_exports_h(r,h,t)$[hours(h)] = can_exports(r,t) * can_exports_h_frac(h) / hours(h) ; - -$endif.Canada - -* zero Canada exports when Canada is not modeled -can_imports_szn(r,szn,t)$[Sw_Canada=0] = 0 ; -can_exports_h(r,h,t)$[Sw_Canada=0] = 0 ; - -$onempty -parameter canmexload(r,allh) "load for canadian and mexican regions" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%canmexload.csv -$offdelim -$onlisting -/ ; -$offempty - - -*============================================= -* -- Air quality policies -- -*============================================= - -h_weight_csapr(allh) = 0 ; -h_weight_csapr(h) = - sum{quarter, frac_h_quarter_weights(h,quarter) * quarter_weight_csapr(quarter) } ; - - -*================================================== -* -- Availability (forced and scheduled outages) -- -*================================================== - -parameter outage_forced_h(i,r,allh) "--fraction-- forced outage rate" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%outage_forced_h.csv -$include inputs_case%ds%stress%stress_year%%ds%outage_forced_h.csv -$offdelim -$onlisting -/ ; - -* Infer some forced outage rates from parent techs -outage_forced_h(i,r,h)$pvb(i) = outage_forced_h("battery_li",r,h) ; -outage_forced_h(i,r,h)$geo(i) = outage_forced_h("geothermal",r,h) ; - -outage_forced_h(i,r,h)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), outage_forced_h(ii,r,h) } ; - -* Upgrade plants assume the same forced outage rate as what they're upgraded to -outage_forced_h(i,r,h)$upgrade(i) = sum{ii$upgrade_to(i,ii), outage_forced_h(ii,r,h) } ; - -parameter outage_scheduled_h(i,allh) "--fraction-- scheduled outage rate" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%outage_scheduled_h.csv -$include inputs_case%ds%stress%stress_year%%ds%outage_scheduled_h.csv -$offdelim -$onlisting -/ ; - -* Infer some scheduled outage rates from parent techs -outage_scheduled_h(i,h)$pvb(i) = outage_scheduled_h("battery_li",h) ; -outage_scheduled_h(i,h)$geo(i) = outage_scheduled_h("geothermal",h) ; - -outage_scheduled_h(i,h)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), outage_scheduled_h(ii,h) } ; - -* Upgrade plants assume the same scheduled outage rate as what they're upgraded to -outage_scheduled_h(i,h)$upgrade(i) = sum{ii$upgrade_to(i,ii), outage_scheduled_h(ii,h) } ; - -* Calculate availability (includes forced and scheduled outage rates) -avail(i,r,allh) = 0 ; -avail(i,r,h)$valcap_ir(i,r) = 1 ; - -avail(i,r,h)$[valcap_ir(i,r)] = (1 - outage_forced_h(i,r,h)) * (1 - outage_scheduled_h(i,h)) ; - - -*============================================= -* -- DR Shed -- -*============================================= - -* Written by hourly_writetimeseries.py -$onempty -parameter dr_shed_out(i,r,allh) "--fraction-- fraction of capacity available for DR shed resources" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%dr_shed_out.csv -$include inputs_case%ds%stress%stress_year%%ds%dr_shed_out.csv -$offdelim -$onlisting -/ ; -$offempty - -* DR Shed resources are only available during stress periods -avail(i,r,h)$[dr_shed(i)$h_rep(h)] = 0 ; -avail(i,r,h)$[dr_shed(i)$h_stress(h)] = dr_shed_out(i,r,h) ; - -*upgrade plants assume the same availability of what they are upgraded to -avail(i,r,h)$[upgrade(i)$valcap_i(i)] = sum{ii$upgrade_to(i,ii), avail(ii,r,h) } ; - -* In eq_reserve_margin, thermal outages are captured through the PRM rather than through -* forced/scheduled outages. If GSw_PRM_StressOutages is not true, -* set the availability of thermal generator to 1 during stress periods. -avail(i,r,h) - $[h_stress(h)$valcap_ir(i,r)$(Sw_PRM_StressOutages=0) - $(not vre(i))$(not hydro(i))$(not storage(i))$(not consume(i)) - ] = 1 ; - -* Geothermal is currently the only tech where derate_geo_vintage(i,v) != 1. -* If other techs with a non-unity vintage-dependent derate are added, avail(i,r,h) may need to be -* multiplied by derate_geo_vintage(i,v) in additional locations throughout the model. -* Divide by (1 - outage rate) (i.e. avail) since geothermal_availability is defined -* as the total product of derate_geo_vintage(i,v) * avail(i,r,h). -derate_geo_vintage(i,initv)$[geo(i)$valcap_iv(i,initv)] = - geothermal_availability - / (sum{(r,h)$valcap_ir(i,r), avail(i,r,h) * hours(h) } - / sum{(r,h)$valcap_ir(i,r), hours(h) }) ; - -seas_cap_frac_delta(i,v,r,allszn,t) = 0 ; -seas_cap_frac_delta(i,v,r,szn,t)$valcap(i,v,r,t) = - sum{quarter, szn_quarter_weights(szn,quarter) * quarter_cap_frac_delta(i,v,r,quarter,t) } ; - - -*============================================= -* -- Hydrogen -- -*============================================= - -* assign hydrogen demand by region and timeslice -* we assumed demand is flat, i.e., timeslices w/ more hours -* have more demand in metric tons but the same rate in metric tons/hour -h2_exogenous_demand_regional(r,p,allh,t) = 0 ; -h2_exogenous_demand_regional(r,p,h,t)$[tmodel_new(t)$h2_share(r,t)] - = h2_share(r,t) * h2_exogenous_demand(p,t) / 8760 ; - - -*============================================= -* -- Capacity factor -- -*============================================= - -* Written by cfgather.py, overwritten by hourly_writetimeseries.py -parameter cf_in(i,r,allh) "--fraction-- capacity factors for renewable technologies" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%cf_vre.csv -$include inputs_case%ds%stress%stress_year%%ds%cf_vre.csv -$offdelim -$onlisting -/ ; - -cf_in(i,r,h)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), cf_in(ii,r,h) } ; - -*initial assignment of capacity factors -cf_rsc(i,v,r,allh,t) = 0 ; -cf_rsc(i,v,r,h,t)$[cf_in(i,r,h)$cf_tech(i)$valcap(i,v,r,t)] = cf_in(i,r,h) ; - -* Written by input_processing/hourly_writetimeseries.py -$onempty -parameter cf_hyd(i,allszn,r,allt) "--fraction-- hydro capacity factors by season and year" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%cf_hyd.csv -$include inputs_case%ds%stress%stress_year%%ds%cf_hyd.csv -$offdelim -$onlisting -/ ; -$offempty - -$ifthen.climatehydro %GSw_ClimateHydro% == 1 - -* Written by climateprep.py -table climate_hydro_seasonal(r,allszn,allt) "annual/seasonal nondispatchable hydropower availability" -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%climate_hydadjsea.csv -$offdelim -$onlisting -; - -* adjust cf_hyd based on annual/seasonal climate multipliers -* non-dispatchable hydro gets new seasonal profiles as well as annually-varying CF -* dispatchable hydro keeps the original seasonal profiles; only annual CF changes. Reflects the assumption -* that reservoirs will be utilized in the same seasonal pattern even if seasonal inflows change. -cf_hyd(i,szn,r,t)$[hydro_nd(i)$(yeart(t)>=Sw_ClimateStartYear)] = - sum{allt$att(allt,t), cf_hyd(i,szn,r,t) * climate_hydro_seasonal(r,szn,allt) } ; - -cf_hyd(i,szn,r,t)$[hydro_d(i)$(yeart(t)>=Sw_ClimateStartYear)] = - sum{allt$att(allt,t), cf_hyd(i,szn,r,t) * climate_hydro_annual(r,allt) } ; - -$endif.climatehydro - -*created by /input_processing/writecapdat.py -parameter cap_hyd_szn_adj(i,allszn,r) "--fraction-- seasonal max capacity adjustment for dispatchable hydro" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%cap_hyd_szn_adj.csv -$include inputs_case%ds%stress%stress_year%%ds%cap_hyd_szn_adj.csv -$offdelim -$onlisting -/ ; - -*Upgraded hydro parameters: -* By default, capacity factors for upgraded hydro techs use what we upgraded from. -cf_hyd(i,szn,r,t)$[upgrade(i)$(hydro(i) or psh(i))] = - sum{ii$upgrade_from(i,ii), cf_hyd(ii,szn,r,t) } ; - -* dispatchable hydro has a separate constraint for seasonal generation which uses m_cf_szn -cf_rsc(i,v,r,h,t)$[hydro(i)$valcap(i,v,r,t)] = sum{szn$h_szn(h,szn), cf_hyd(i,szn,r,t) } ; - -cf_rsc(i,v,r,h,t)$[rsc_i(i)$(sum{tt, capacity_exog(i,v,r,tt) })] = - cf_rsc(i,"init-1",r,h,t) ; - -* For cap_hyd_szn_adj, which only applies to dispatchable hydro or upgraded disp hydro with added pumping, we first try using the from-tech, but if that is -* not available we use to to-tech, and if not that either we just use 1. -cap_hyd_szn_adj(i,szn,r)$[upgrade(i)$(hydro_d(i) or psh(i))] = - sum{ii$upgrade_from(i,ii), cap_hyd_szn_adj(ii,szn,r) } ; -cap_hyd_szn_adj(i,szn,r)$[upgrade(i)$hydro_d(i)$(not cap_hyd_szn_adj(i,szn,r))] = - sum{ii$upgrade_to(i,ii), cap_hyd_szn_adj(ii,szn,r) } ; -cap_hyd_szn_adj(i,szn,r)$[upgrade(i)$hydro_d(i)$(not cap_hyd_szn_adj(i,szn,r))] = 1 ; - - -* do not apply "avail" for hybrid PV+battery because "avail" represents the battery availability -m_cf(i,v,r,allh,t) = 0 ; -m_cf(i,v,r,h,t)$[cf_tech(i)$valcap(i,v,r,t)$cf_rsc(i,v,r,h,t)$cf_adj_t(i,v,t)] = - cf_rsc(i,v,r,h,t) - * cf_adj_t(i,v,t) - * (avail(i,r,h)$[not pvb(i) and not hydro(i)] + 1$(pvb(i) or hydro(i)) ); - -* can remove capacity factors for new vintages that have not been introduced yet -m_cf(i,newv,r,h,t)$[not sum{tt$(yeart(tt) <= yeart(t)), ivt(i,newv,tt ) }$valcap(i,newv,r,t)$m_cf(i,newv,r,h,t)] = 0 ; - -* distpv capacity factor is divided by (1.0 - distloss) to provide a busbar equivalent capacity factor -m_cf(i,v,r,h,t)$[distpv(i)$valcap(i,v,r,t)] = m_cf(i,v,r,h,t) / (1.0 - distloss) ; - -* doing this before calculating m_cf_szn to make sure -* m_cf_szn does not get populated with very small values -m_cf(i,v,r,h,t)$[not valcap(i,v,r,t)] = 0 ; -m_cf(i,v,r,h,t)$[(m_cf(i,v,r,h,t)<0.01)$valcap(i,v,r,t)] = 0 ; -m_cf(i,v,r,h,t)$[cf_tech(i)$valcap(i,v,r,t)$m_cf(i,v,r,h,t)] = round(m_cf(i,v,r,h,t),3) ; - -* Remove capacity when there is no corresponding capacity factor -m_capacity_exog(i,v,r,t)$[initv(v)$cf_tech(i)$(not sum{h, m_cf(i,v,r,h,t) })] = 0 ; - -* Average CF by season -m_cf_szn(i,v,r,allszn,t) = 0 ; -m_cf_szn(i,v,r,szn,t)$[cf_tech(i)$valcap(i,v,r,t)$(hydro_d(i) or hyd_add_pump(i))] = - sum{h$h_szn(h,szn), hours(h) * m_cf(i,v,r,h,t) } - / sum{h$h_szn(h,szn), hours(h) } ; - -* adding upgrade techs for hydro -m_cf_szn(i,v,r,szn,t) - $[cf_tech(i)$upgrade(i)$valcap(i,v,r,t)$(hydro_d(i) or psh(i))] - = sum{ii$upgrade_from(i,ii), m_cf_szn(ii,v,r,szn,t) } ; - -m_cf_szn(i,v,r,szn,t) - $[cf_tech(i)$upgrade(i)$valcap(i,v,r,t)$hydro_d(i)$(not m_cf_szn(i,v,r,szn,t))] - = sum{ii$upgrade_to(i,ii), m_cf_szn(ii,v,r,szn,t) } ; - -m_cf_szn(i,v,r,szn,t) - $[cf_tech(i)$upgrade(i)$valcap(i,v,r,t)$hydro_d(i)$(not m_cf_szn(i,v,r,szn,t))] - = 1 ; - -* Calculate daytime hours (for PVB) based on hours with nonzero PV CF -dayhours(allh) = 0 ; -dayhours(h)$[sum{(i,v,r,t)$[pv(i)$valgen(i,v,r,t)], m_cf(i,v,r,h,t) }] = yes ; - - -*===================================================================================== -* -- Cooling Water Initialization, Seasonal Distribution, & Climate Adjustment -- -*===================================================================================== - -* Initialize water capacity based on water requirements of existing fleet in base year. -* We conservatively assume plants have enough water available to operate up to a -* 100% capacity factor, or to operate at full capacity at any time of the year. -if(%cur_year% = sum{t$tfirst(t), yeart(t) }, - wat_supply_init(wst,r) = sum{(i,v,h,t)$[h_rep(h)$valcap(i,v,r,t)$initv(v)$i_wst(i,wst)$tfirst(t)], - hours(h) - * (sum{w$i_w(i,w), m_capacity_exog(i,v,r,t) * water_rate(i,w)}) - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - } / 1E6 ; - - m_watsc_dat(wst,"cap",r,t)$tmodel_new(t) = wat_supply_new(wst,"cap",r) + wat_supply_init(wst,r) ; - -* --- Climate Impacts: Cooling Water Capacity --- -$ifthen.climatewater %GSw_ClimateWater% == 1 -* Update water supply curve with annually-varying water supply. Multiplier is applied to (wat_supply_new + wat_supply_init). -* NOTE: Only the capacity changes, not the cost - m_watsc_dat(wst,"cap",r,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) - ] $= sum{allt$att(allt,t), - m_watsc_dat(wst,"cap",r,t) * wat_supply_climate(wst,r,allt) } ; -* If wst is in wst_climate but does not have data in input file, assign its multiplier to the fsu multiplier - m_watsc_dat(wst,"cap",r,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) - $sum{allt$att(allt,t), (not wat_supply_climate(wst,r,allt)) } - ] $= sum{allt$att(allt,t), - m_watsc_dat(wst,"cap",r,t) * wat_supply_climate('fsu',r,allt) } ; -$endif.climatewater -) ; - -* Initialize seasonal distribution factors for new unappropriated water access -watsa(wst,r,allszn,t) = 0 ; -watsa(wst,r,szn,t)$[tmodel_new(t)$Sw_WaterMain] = - sum{quarter, - szn_quarter_weights(szn,quarter) * watsa_temp(wst,r,quarter) } ; - -* update seasonal distribution factors for water sources other than fresh surface unappropriated -* and also fsu with missing data -watsa(wst,r,szn,t)$[(not sum{sznn, watsa(wst,r,sznn,t)})$tmodel_new(t)$Sw_WaterMain] = - round(numdays(szn)/365 , 4) ; - -* --- Climate Impacts: Cooling Water Seasonal Distribution --- -$ifthen.climatewater %GSw_ClimateWater% == 1 -* Update seasonal distribution factors for fsu; other water types are unchanged -* declared over allt to allow for external data files that extend beyond end_year -* Written by climateprep.py -table watsa_climate(wst,r,allszn,allt) "time-varying fractional seasonal allocation of water" -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%climate_UnappWaterSeaAnnDistr.csv -$offdelim -$onlisting -; -* Use the sparse assignment $= to make sure we don't assign zero to wst's not included in watsa_climate -watsa(wst,r,szn,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) - $Sw_WaterMain - ] $= sum{allt$att(allt,t), watsa_climate(wst,r,szn,allt) }; -* If wst is in wst_climate but does not have data in input file, assign its multiplier to the fsu multiplier -watsa(wst,r,szn,t)$[wst_climate(wst)$tmodel_new(t)$(yeart(t)>=Sw_ClimateStartYear) - $sum{allt$att(allt,t), (not watsa_climate(wst,r,szn,allt)) } - $Sw_WaterMain - ] $= sum{allt$att(allt,t), watsa_climate('fsu',r,szn,allt) }; -$endif.climatewater - - -*============================================= -* -- Operating reserves and minloading -- -*============================================= - -$onempty -set opres_periods(allszn) "Periods within which the operating reserve constraint applies" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%opres_periods.csv -$offdelim -$onlisting -/ ; -$offempty - -opres_h(allh) = 0 ; -opres_h(h) = sum{szn$opres_periods(szn), h_szn(h,szn) } ; - - -set hour_szn_group(allh,allhh) "h and hh in the same season - used in minloading constraint" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%hour_szn_group.csv -$offdelim -$onlisting -/ ; - -*reducing problem size by removing h-hh combos that are the same -hour_szn_group(h,hh)$sameas(h,hh) = no ; - -hydmin(i,r,allszn) = 0 ; -hydmin(i,r,szn) = round(sum{quarter, szn_quarter_weights(szn,quarter) * hydmin_quarter(i,r,quarter) }, 3) ; - -minloadfrac(r,i,allh) = 0 ; -minloadfrac(r,i,h) = minloadfrac0(i) ; - -* adjust nuclear mingen to minloadfrac_nuclear_flex if running with flexible nuclear -minloadfrac(r,i,h)$[nuclear(i)$Sw_NukeFlex] = minloadfrac_nuclear_flex ; -* CSP and coal use user inputs -minloadfrac(r,i,h)$csp(i) = minloadfrac_csp ; -minloadfrac(r,i,h)$[coal(i)$(not minloadfrac(r,i,h))] = minloadfrac_coal ; -*set seasonal values for minloadfrac for hydro techs -minloadfrac(r,i,h)$[sum{szn$h_szn(h,szn), hydmin(i,r,szn ) }] = - sum{szn$h_szn(h,szn), hydmin(i,r,szn) } ; -*water tech assignment -minloadfrac(r,i,h)$[i_water_cooling(i)$Sw_WaterMain] = - sum{ii$ctt_i_ii(i,ii), minloadfrac(r,ii,h) } ; -*upgrade techs get their corresponding upgraded-to minloadfracs -minloadfrac(r,i,h)$upgrade(i) = sum{ii$upgrade_to(i,ii), minloadfrac(r,ii,h) } ; -*remove minloadfrac for non-viable generators -minloadfrac(r,i,h)$[not sum{(v,t), valcap(i,v,r,t) }] = 0 ; - -*reduced set of minloading constraints and mingen contributors -minloadfrac(r,i,h)$[(Sw_MinLoadTechs=0)] = 0 ; -minloadfrac(r,i,h)$[(Sw_MinLoadTechs=2)$(geo(i) or csp(i) or lfill(i))] = 0 ; -minloadfrac(r,i,h)$[(Sw_MinLoadTechs=3)$(not nuclear(i))$(not hydro(i))] = 0 ; -minloadfrac(r,i,h)$[(Sw_MinLoadTechs=4)$(not boiler(i))$(not hydro(i))] = 0 ; - - -*============================================= -* -- Electricity demand -- -*============================================= - -* Flexible demand -$onempty -parameter flex_frac_load(flex_type,r,allh,allt) -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%flex_frac_all.csv -$offdelim -$onlisting -/ ; - - -* EV adoptable managed charging -parameter evmc_baseline_load(r,allh,allt) "--fraction-- how much adopted shaped EV load is allowed to be shed in each timeslice h" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%evmc_baseline_load.csv -$offdelim -$onlisting -/ ; - -parameter evmc_shape_gen(i,r,allh) "--fraction-- how much adopted shaped EV load is allowed to be shed in each timeslice h" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%evmc_shape_generation.csv -$offdelim -$onlisting -/ ; - -parameter evmc_shape_load(i,r,allh) "--fraction-- how much adopted shaped EV load is added in each timeslice h" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%evmc_shape_load.csv -$offdelim -$onlisting -/ ; - -parameter evmc_storage_discharge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be discharged (deferred charging) in each timeslice h" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%evmc_storage_discharge.csv -$offdelim -$onlisting -/ ; - -parameter evmc_storage_charge_frac(i,r,allh,allt) "--fraction-- fraction of adopted EV storage discharge capacity that can be charged (add back deferred charging) in each timeslice h" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%evmc_storage_charge.csv -$offdelim -$onlisting -/ ; - -parameter evmc_storage_energy_hours(i,r,allh,allt) "--hours-- Allowable EV storage SOC (quantity deferred EV charge) [MWh] divided by nameplate EVMC discharge capacity [MW]" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%evmc_storage_energy.csv -$offdelim -$onlisting -/ ; -$offempty - -* Written by hourly_writetimeseries.py -parameter load_allyear(r,allh,allt) "--MW-- end-use load by region, timeslice, and year" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%load_allyear.csv -$include inputs_case%ds%stress%stress_year%%ds%load_allyear.csv -$offdelim -$onlisting -/ ; -* Dividing by (1-distloss) converts end-use load to busbar load -load_exog(r,allh,t) = 0 ; -load_exog(r,h,t) = load_allyear(r,h,t) / (1.0 - distloss) ; - -parameter prm_year(r) "--fraction-- planning reserve margin for the current solve year" -/ -$offlisting -$ondelim -$include inputs_case%ds%stress%stress_year%%ds%prm.csv -$offdelim -$onlisting -/ ; -prm(r,t)$tmodel(t) = prm_year(r) ; - -* Stress-period load is scaled up by PRM -load_exog(r,h,t)$h_stress(h) = load_exog(r,h,t) * (1 + prm(r,t)) ; - -* first define mexican growth load then replace canadian with -* province-specific growth factors -load_exog(r,h,t)$canmexload(r,h) = mex_growth_rate(t) * canmexload(r,h) ; - -load_exog(r,h,t)$sum{st$r_st(r,st),can_growth_rate(st,t) } = - canmexload(r,h) * sum{st$r_st(r,st),can_growth_rate(st,t) } ; - -* Flexible load doesn't yet work with hourly resolution -flex_h_corr1(flex_type,allh,allh) = no ; -flex_h_corr2(flex_type,allh,allh) = no ; - -* assign zero values to avoid unassigned parameter errors -flex_demand_frac(flex_type,r,allh,t) = 0 ; -flex_demand_frac(flex_type,r,h,t)$Sw_EFS_Flex = flex_frac_load(flex_type,r,h,t) ; - -*initial values are set here (after SwI_Load has been accounted for) -load_exog0(r,allh,t) = 0 ; -load_exog0(r,h,t) = load_exog(r,h,t) ; - - -load_exog_flex(flex_type,r,allh,t) = 0 ; -load_exog_flex(flex_type,r,h,t) = load_exog(r,h,t) * flex_demand_frac(flex_type,r,h,t) ; -load_exog_static(r,allh,t) = 0 ; -load_exog_static(r,h,t) = load_exog(r,h,t) - sum{flex_type, load_exog_flex(flex_type,r,h,t) } ; - - - -set maxload_szn(r,allh,t,allszn) "hour with highest load within each szn" ; -maxload_szn(r,allh,t,allszn) = 0 ; -maxload_szn(r,h,t,szn) - $[(smax(hh$[h_szn(hh,szn)], load_exog_static(r,hh,t)) - = load_exog_static(r,h,t)) - $h_szn(h,szn)$Sw_OpRes] = yes ; - -set h_ccseason_prm(allh,ccseason) "peak-load hour for the entire modeled system by ccseason" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%h_ccseason_prm.csv -$offdelim -$onlisting -/ ; - -peak_static_frac(r,ccseason,t) = 1 - sum{(flex_type,h)$h_ccseason_prm(h,ccseason), flex_demand_frac(flex_type,r,h,t) } ; - - - -* Written by hourly_writetimeseries.py -parameter peak_ccseason(r,ccseason,allt) "--MW-- end-use peak demand by region, season, year" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%peak_ccseason.csv -$offdelim -$onlisting -/ ; -*Dividing by (1-distloss) converts end-use load to busbar load -peakdem_static_ccseason(r,ccseason,t) = peak_ccseason(r,ccseason,t) * peak_static_frac(r,ccseason,t) / (1.0 - distloss) ; - - -$onempty -parameter peak_h(r,allh,allt) "--MW-- busbar peak demand by timeslice" -/ -$offlisting -$ondelim -$include inputs_case%ds%%temporal_inputs%%ds%peak_h.csv -$offdelim -$onlisting -/ ; -$offempty - -peakdem_static_h(r,allh,t) = 0 ; -peakdem_static_h(r,h,t) = peak_h(r,h,t) * (1 - sum{flex_type, flex_demand_frac(flex_type,r,h,t) }) / (1.0 - distloss) ; - - -*============================================= -* -- Fossil gas supply curve -- -*============================================= - -gasadder_cd(cendiv,t,allh) = 0 ; -gasadder_cd(cendiv,t,h) = (gasprice_ref(cendiv,t) - gasprice_nat(t))/2 ; - -*winter gas gets marked up -gasadder_cd(cendiv,t,h) = - gasadder_cd(cendiv,t,h) - + gasprice_ref_frac_adder * frac_h_quarter_weights(h,"wint") * gasprice_ref(cendiv,t) ; - -szn_adj_gas(allh) = 0 ; -szn_adj_gas(h) = 1 ; -szn_adj_gas(h)$frac_h_quarter_weights(h,"wint") = - szn_adj_gas(h) + frac_h_quarter_weights(h,"wint") * szn_adj_gas_winter ; - - -*============================================= -* -- Round parameters for GAMS -- -*============================================= - -avail(i,r,h)$avail(i,r,h) = round(avail(i,r,h),3) ; -can_imports_szn(r,szn,t)$can_imports_szn(r,szn,t) = round(can_imports_szn(r,szn,t),3) ; -can_exports_h(r,h,t)$can_exports_h(r,h,t) = round(can_exports_h(r,h,t),3) ; -h_weight_csapr(h)$h_weight_csapr(h) = round(h_weight_csapr(h),3) ; -load_exog(r,h,t)$load_exog(r,h,t) = round(load_exog(r,h,t),3) ; -load_exog_static(r,h,t)$load_exog_static(r,h,t) = round(load_exog_static(r,h,t),3) ; -minloadfrac(r,i,h)$minloadfrac(r,i,h) = round(minloadfrac(r,i,h),3) ; -szn_adj_gas(h)$szn_adj_gas(h) = round(szn_adj_gas(h), 3) ; -cap_hyd_szn_adj(i,szn,r)$cap_hyd_szn_adj(i,szn,r) = round(cap_hyd_szn_adj(i,szn,r),3) ; -peakdem_static_ccseason(r,ccseason,t)$peakdem_static_ccseason(r,ccseason,t) = round(peakdem_static_ccseason(r,ccseason,t),2) ; -seas_cap_frac_delta(i,v,r,szn,t)$seas_cap_frac_delta(i,v,r,szn,t) = round(seas_cap_frac_delta(i,v,r,szn,t),3) ; - - -* Write the inputs for debugging purposes -$ifthene.write %cur_year% == %startyear% -execute_unload 'inputs_case%ds%inputs.gdx' ; -$endif.write diff --git a/d2_post_solve_adjustments.gms b/d2_post_solve_adjustments.gms deleted file mode 100644 index 5b7bd8bb..00000000 --- a/d2_post_solve_adjustments.gms +++ /dev/null @@ -1,131 +0,0 @@ -*** Shrink sets based on technologies used (optional) -if(Sw_NewValCapShrink = 1, - -* remove newv dimensions for technologies that do not have capacity in this year -* and if it is not a vintage you can build in future years -* and if the plant has not been upgraded -* note since we're applying this only to new techs the upgrades portion -* needs to be present in combination with the ability to be built in future periods -* said differently, we want to make sure the vintage cannot be built in future periods, -* it hasn't been built yet, and it has no associated upgraded units -* here the second year index tracks which year has just solved - valcap_remove(i,v,r,t,"%cur_year%")$[newv(v)$valcap(i,v,r,t)$ivt(i,v,"%cur_year%") -* if there is no capacity.. - $(not CAP.l(i,v,r,"%cur_year%")) -* if you have not invested in it.. - $(not sum(tt$[(yeart(tt)<=%cur_year%)], INV.l(i,v,r,tt) )) -* if you cannot invest in the ivt combo in future years.. - $(not sum{tt$[tt.val>%cur_year%],ivt(i,v,tt)}) - $(not sum(tt$[valinv(i,v,r,tt)$(yeart(tt)>%cur_year%)],1)) -* if it has not been upgraded.. -* note the newv condition above allows for the capacity equations -* of motion to still function - this would/does not work for initv vintanges without additional work - $(not sum{(tt,ii)$[tsolved(tt)$upgrade_from(ii,i)$valcap(ii,v,r,tt)], - UPGRADES.l(ii,v,r,tt)}) - ] = yes ; - valcap(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; - valcap_h2ptc(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; - valgen(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; - valgen_h2ptc(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no; - valinv(i,v,r,t)$valcap_remove(i,v,r,t,"%cur_year%") = no ; - inv_cond(i,v,r,t,"%cur_year%")$valcap_remove(i,v,r,t,"%cur_year%") = no ; - valcap_irt(i,r,t) = sum{v, valcap(i,v,r,t) } ; - valcap_iv(i,v)$sum{(r,t)$tmodel_new(t), valcap(i,v,r,t) } = yes ; - valcap_i(i)$sum{v, valcap_iv(i,v) } = yes ; - valcap_ivr(i,v,r)$sum{t, valcap(i,v,r,t) } = yes ; - valgen_irt(i,r,t) = sum{v, valgen(i,v,r,t) } ; - valinv_irt(i,r,t) = sum{v, valinv(i,v,r,t) } ; - valinv_tg(st,tg,t)$sum{(i,r)$[tg_i(tg,i)$r_st(r,st)], valinv_irt(i,r,t) } = yes ; - -) ; - -*** Adjust CCS incentives for upgrades -if(Sw_Upgrades = 1, -* note sum over tt required here as we want to only remove the incentive -* from years beyond the current year if upgrades occurred in this solve year - -* extend the current-year incentive beyond current date to expiration date - only needed -* when needing to specify beyond current amounts - co2_captured_incentive(i,v,r,t)$[sum{tt$tmodel(tt),upgrades.l(i,v,r,tt) } - $(not sum{tt$tfix(tt),upgrades.l(i,v,r,tt)}) - $(year(t) < %cur_year% + co2_capture_incentive_length) - $(yeart(t) >= %cur_year%) - $valcap(i,v,r,t) ] = co2_captured_incentive(i,v,r,"%cur_year%") ; - -* remove co2 captured incentive after the length of time if upgrades occurred in this year - co2_captured_incentive(i,v,r,t)$[sum{tt$tmodel(tt),upgrades.l(i,v,r,tt) } - $(year(t) >= %cur_year% + co2_capture_incentive_length) - $valcap(i,v,r,t) ] = 0 ; - -* adjust fom of upgraded-from plant to updated cost for maintaining the CCS equipment - cost_fom(i,v,r,t)$[sum{(ii,tt)$[tmodel(tt)$upgrade_from(ii,i)],upgrades.l(ii,v,r,tt) } - $(year(t) >= %cur_year%) - $valcap(i,v,r,t) ] = - max(cost_fom(i,v,r,t), - sum{ii$upgrade_from(ii,i),cost_fom(ii,v,r,t) } - ) ; -) ; - - -*** Regional emissions for tax credit phaseout -* emit_r_tc is calculated the same as the EMIT variable in the model. We do not use -* EMIT.l here because the emissions are only modeled for those in the emit_modeled set. -emit_r_tc(r,t)$tmodel_new(t) = - -* Emissions from generation - sum{(i,v,h)$[valgen(i,v,r,t)$h_rep(h)], - hours(h) * emit_rate("process","CO2",i,v,r,t) - * (GEN.l(i,v,r,h,t) - + CCSFLEX_POW.l(i,v,r,h,t)$[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)]) - } - -* Plus emissions produced via production activities (SMR, SMR-CCS, DAC) -* The "production" of negative CO2 emissions via DAC is also included here - + sum{(p,i,v,h)$[valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], - hours(h) * prod_emit_rate("process","CO2",i,t) - * PRODUCE.l(p,i,v,r,h,t) - } - -*[minus] co2 reduce from flexible CCS capture -*capture = capture per energy used by the ccs system * CCS energy - -* Flexible CCS - bypass - - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_byp(i)$h_rep(h)], - ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POW.l(i,v,r,h,t) })$Sw_CCSFLEX_BYP - -* Flexible CCS - storage - - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_sto(i)$h_rep(h)], - ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POWREQ.l(i,v,r,h,t) })$Sw_CCSFLEX_STO -; - -emit_nat_tc(t)$tmodel_new(t) = sum{r, emit_r_tc(r,t) } ; - - -*** Recalculate regional CO2 emissions rate for use in state CO2 cap import accounting -* [metric kiloton] * [1000 metric ton / metric kiloton] / ([MW] * [hours]) = [metric ton/MWh] -$ifthen.stateco2 %GSw_StateCO2ImportLevel% == 'r' - co2_emit_rate_r(r,t)$tmodel(t) = ( - emit_r_tc(r,t) - / sum{(i,v,h)$[valgen(i,v,r,t)], hours(h) * GEN.l(i,v,r,h,t) } -* Avoid division-by-zero errors - )$sum{(i,v,h)$[valgen(i,v,r,t)], hours(h) * GEN.l(i,v,r,h,t) } ; -$else.stateco2 -* sum emissions and generation within the region - co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t)$tmodel(t) = ( - sum{rr$r_%GSw_StateCO2ImportLevel%(rr,%GSw_StateCO2ImportLevel%), - emit_r_tc(rr,t) } - / sum{(i,v,rr,h)$[valgen(i,v,rr,t) - $r_%GSw_StateCO2ImportLevel%(rr,%GSw_StateCO2ImportLevel%)], - hours(h) * GEN.l(i,v,rr,h,t) } -* Avoid division-by-zero errors - )$sum{(i,v,rr,h)$[valgen(i,v,rr,t) - $r_%GSw_StateCO2ImportLevel%(rr,%GSw_StateCO2ImportLevel%)], - hours(h) * GEN.l(i,v,rr,h,t) } - ; -* broadcast the regional emissions rate to each r in the region - co2_emit_rate_r(r,t)$tmodel(t) = - sum{%GSw_StateCO2ImportLevel% - $r_%GSw_StateCO2ImportLevel%(r,%GSw_StateCO2ImportLevel%), - co2_emit_rate_regional(%GSw_StateCO2ImportLevel%,t) } - ; -$endif.stateco2 diff --git a/d2_unfix_op.gms b/d2_unfix_op.gms deleted file mode 100644 index 8ef23ad9..00000000 --- a/d2_unfix_op.gms +++ /dev/null @@ -1,115 +0,0 @@ -* load -LOAD.lo(r,h,t_unfix) = 0 ; -LOAD.up(r,h,t_unfix) = +inf ; -FLEX.lo(flex_type,r,h,t_unfix)$Sw_EFS_flex = 0 ; -FLEX.up(flex_type,r,h,t_unfix)$Sw_EFS_flex = +inf ; -DROPPED.lo(r,h,t_unfix)$[(yeart(t_unfix)=h2_demand_start)] = 0 ; -CREDIT_H2PTC.up(i,v,r,h,t_unfix)$[valgen_h2ptc(i,v,r,t_unfix)$h_rep(h)$Sw_H2_PTC$h2_ptc_years(t_unfix)$(yeart(t_unfix)>=h2_demand_start)] = +inf ; - -* CO2 capture and storage -CO2_CAPTURED.lo(r,h,t_unfix)$Sw_CO2_Detail = 0 ; -CO2_CAPTURED.up(r,h,t_unfix)$Sw_CO2_Detail = +inf ; -CO2_STORED.lo(r,cs,h,t_unfix)$[Sw_CO2_Detail$r_cs(r,cs)] = 0 ; -CO2_STORED.up(r,cs,h,t_unfix)$[Sw_CO2_Detail$r_cs(r,cs)] = +inf ; -CO2_FLOW.lo(r,rr,h,t_unfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = 0 ; -CO2_FLOW.up(r,rr,h,t_unfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = +inf ; - -* Positive/negative variables -EMIT.lo(etype,e,r,t_unfix)$emit_modeled(e,r,t_unfix) = -inf ; -EMIT.up(etype,e,r,t_unfix)$emit_modeled(e,r,t_unfix) = +inf ; -STORAGE_INTERDAY_DISPATCH.lo(i,v,r,h,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = -inf ; -STORAGE_INTERDAY_DISPATCH.up(i,v,r,h,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = +inf ; -STORAGE_INTERDAY_LEVEL_MAX_DAY.lo(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = -inf ; -STORAGE_INTERDAY_LEVEL_MAX_DAY.up(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = +inf ; -STORAGE_INTERDAY_LEVEL_MIN_DAY.lo(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = -inf ; -STORAGE_INTERDAY_LEVEL_MIN_DAY.up(i,v,r,allszn,t_unfix)$[storage_interday(i)$valgen(i,v,r,t_unfix)] = +inf ; diff --git a/d2_varfix.gms b/d2_varfix.gms deleted file mode 100644 index 85db2442..00000000 --- a/d2_varfix.gms +++ /dev/null @@ -1,125 +0,0 @@ -* Round problematic variables -* Non-rounded parameters can sometimes cause numerical issues when summing over tfix in model equations -if(Sw_RemoveSmallNumbers = 1, - CAP.l(i,v,r,tfix)$[abs(CAP.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; - CAP_ENERGY.l(i,v,r,tfix)$[abs(CAP_ENERGY.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; - UPGRADES.l(i,v,r,tfix)$[abs(UPGRADES.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; - CAP_ABOVE_LIM.l(tg,r,tfix)$[abs(CAP_ABOVE_LIM.l(tg,r,tfix)) < rhs_tolerance] = 0 ; - INV.l(i,v,r,tfix)$[abs(INV.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; - INV_ENERGY.l(i,v,r,tfix)$[abs(INV_ENERGY.l(i,v,r,tfix)) < rhs_tolerance] = 0 ; - INV_RSC.l(i,v,r,rscbin,tfix)$[abs(INV_RSC.l(i,v,r,rscbin,tfix)) < rhs_tolerance] = 0 ; - INV_POI.l(r,tfix)$[abs(INV_POI.l(r,tfix)) < rhs_tolerance] = 0 ; - H2_STOR_INV.l(h2_stor,r,tfix)$[abs(H2_STOR_INV.l(h2_stor,r,tfix)) < rhs_tolerance] = 0 ; - H2_TRANSPORT_INV.l(r,rr,tfix) $[abs(H2_TRANSPORT_INV.l(r,rr,tfix) ) < rhs_tolerance] = 0 ; -); - -*load variable -LOAD.fx(r,h,tfix) = LOAD.l(r,h,tfix) ; -FLEX.fx(flex_type,r,h,tfix)$Sw_EFS_flex = FLEX.l(flex_type,r,h,tfix) ; -* PEAK_FLEX.fx(r,ccseason,tfix)$Sw_EFS_flex = PEAK_FLEX.l(r,ccseason,tfix) ; -DROPPED.fx(r,h,tfix)$[(yeart(tfix)=model_builds_start_yr) - $(sum{(tgg,rr), cap_limit(tgg,rr,tfix)}) - $sum{(i,newv)$tg_i(tg,i), valinv(i,newv,r,tfix)}] = CAP_ABOVE_LIM.l(tg,r,tfix) ; -CAP_SDBIN.fx(i,v,r,ccseason,sdbin,tfix)$[valcap(i,v,r,tfix)$(storage(i) or hyd_add_pump(i))$(not csp(i))$Sw_PRM_CapCredit] = CAP_SDBIN.l(i,v,r,ccseason,sdbin,tfix) ; -CAP_SDBIN_ENERGY.fx(i,v,r,ccseason,sdbin,tfix)$[valcap(i,v,r,tfix)$battery(i)$Sw_PRM_CapCredit] = CAP_SDBIN_ENERGY.l(i,v,r,ccseason,sdbin,tfix) ; -GROWTH_BIN.fx(gbin,i,st,tfix)$[sum{r$[r_st(r,st)], valinv_irt(i,r,tfix) }$stfeas(st)$Sw_GrowthPenalties$(yeart(tfix)<=Sw_GrowthPenLastYear)] = GROWTH_BIN.l(gbin,i,st,tfix) ; -INV.fx(i,v,r,tfix)$[valinv(i,v,r,tfix)] = INV.l(i,v,r,tfix) ; -INV_ENERGY.fx(i,v,r,tfix)$[valinv(i,v,r,tfix)$battery(i)] = INV_ENERGY.l(i,v,r,tfix) ; -INV_REFURB.fx(i,v,r,tfix)$[valinv(i,v,r,tfix)$refurbtech(i)] = INV_REFURB.l(i,v,r,tfix) ; -INV_RSC.fx(i,v,r,rscbin,tfix)$[valinv(i,v,r,tfix)$rsc_i(i)$m_rscfeas(r,i,rscbin)] = INV_RSC.l(i,v,r,rscbin,tfix) ; -CAP_RSC.fx(i,v,r,rscbin,tfix)$[valcap(i,v,r,tfix)$rsc_i(i)$m_rscfeas(r,i,rscbin)] = CAP_RSC.l(i,v,r,rscbin,tfix) ; -INV_CAP_UP.fx(i,v,r,rscbin,tfix)$[allow_cap_up(i,v,r,rscbin,tfix)] = INV_CAP_UP.l(i,v,r,rscbin,tfix) ; -INV_ENER_UP.fx(i,v,r,rscbin,tfix)$[allow_ener_up(i,v,r,rscbin,tfix)] = INV_ENER_UP.l(i,v,r,rscbin,tfix) ; -UPGRADES.fx(i,v,r,tfix)$[valcap(i,v,r,tfix)$upgrade(i)] = UPGRADES.l(i,v,r,tfix) ; -UPGRADES_RETIRE.fx(i,v,r,tfix)$[valcap(i,v,r,tfix)$upgrade(i)] = UPGRADES_RETIRE.l(i,v,r,tfix) ; -EXTRA_PRESCRIP.fx(pcat,r,tfix)$[force_pcat(pcat,tfix)$sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,tfix) }] = EXTRA_PRESCRIP.l(pcat,r,tfix) ; -EXTRA_PRESCRIP_ENERGY.fx(pcat,r,tfix)$[force_pcat(pcat,tfix)$sum{(i,newv)$[prescriptivelink(pcat,i)], valinv(i,newv,r,tfix) }] = EXTRA_PRESCRIP_ENERGY.l(pcat,r,tfix) ; - -* generation and storage variables -GEN.fx(i,v,r,h,tfix)$valgen(i,v,r,tfix) = GEN.l(i,v,r,h,tfix) ; -GEN_PLANT.fx(i,v,r,h,tfix)$[storage_hybrid(i)$(not csp(i))$valgen(i,v,r,tfix)$Sw_HybridPlant] = GEN_PLANT.l(i,v,r,h,tfix) ; -GEN_STORAGE.fx(i,v,r,h,tfix)$[storage_hybrid(i)$(not csp(i))$valgen(i,v,r,tfix)$Sw_HybridPlant] = GEN_STORAGE.l(i,v,r,h,tfix) ; -CURT.fx(r,h,tfix)$Sw_CurtMarket = CURT.l(r,h,tfix) ; -MINGEN.fx(r,szn,tfix)$Sw_Mingen = MINGEN.l(r,szn,tfix) ; -STORAGE_IN.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$(storage_standalone(i) or hyd_add_pump(i))] = STORAGE_IN.l(i,v,r,h,tfix) ; -STORAGE_IN_PLANT.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] = STORAGE_IN_PLANT.l(i,v,r,h,tfix) ; -STORAGE_IN_GRID.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] = STORAGE_IN_GRID.l(i,v,r,h,tfix) ; -STORAGE_LEVEL.fx(i,v,r,h,tfix)$[valgen(i,v,r,tfix)$storage(i)$(not storage_interday(i))] = STORAGE_LEVEL.l(i,v,r,h,tfix) ; -STORAGE_INTERDAY_LEVEL.fx(i,v,r,allszn,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL.l(i,v,r,allszn,tfix) ; -STORAGE_INTERDAY_DISPATCH.fx(i,v,r,allh,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_DISPATCH.l(i,v,r,allh,tfix) ; -STORAGE_INTERDAY_LEVEL_MAX_DAY.fx(i,v,r,allszn,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL_MAX_DAY.l(i,v,r,allszn,tfix) ; -STORAGE_INTERDAY_LEVEL_MIN_DAY.fx(i,v,r,allszn,tfix)$[valgen(i,v,r,tfix)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL_MIN_DAY.l(i,v,r,allszn,tfix) ; -AVAIL_SITE.fx(x,h,tfix)$[Sw_SpurScen$xfeas(x)] = AVAIL_SITE.l(x,h,tfix) ; -RAMPUP.fx(i,r,h,hh,tfix)$[Sw_StartCost$startcost(i)$numhours_nexth(h,hh)$valgen_irt(i,r,tfix)] = RAMPUP.l(i,r,h,hh,tfix) ; - -* flexible CCS variables -CCSFLEX_POW.fx(i,v,r,h,tfix)$[ccsflex(i)$valgen(i,v,r,tfix)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)] = CCSFLEX_POW.l(i,v,r,h,tfix) ; -CCSFLEX_POWREQ.fx(i,v,r,h,tfix)$[ccsflex_sto(i)$valgen(i,v,r,tfix)$Sw_CCSFLEX_STO] = CCSFLEX_POWREQ.l(i,v,r,h,tfix) ; -CCSFLEX_STO_STORAGE_LEVEL.fx(i,v,r,h,tfix)$[ccsflex_sto(i)$valgen(i,v,r,tfix)$Sw_CCSFLEX_STO] = CCSFLEX_STO_STORAGE_LEVEL.l(i,v,r,h,tfix) ; -CCSFLEX_STO_STORAGE_CAP.fx(i,v,r,tfix)$[ccsflex_sto(i)$valcap(i,v,r,tfix)$Sw_CCSFLEX_STO] = CCSFLEX_STO_STORAGE_CAP.l(i,v,r,tfix) ; - -* trade variables -FLOW.fx(r,rr,h,tfix,trtype)$routes(r,rr,trtype,tfix) = FLOW.l(r,rr,h,tfix,trtype) ; -OPRES_FLOW.fx(ortype,r,rr,h,tfix)$[Sw_OpRes$opres_model(ortype)$opres_routes(r,rr,tfix)$opres_h(h)] = OPRES_FLOW.l(ortype,r,rr,h,tfix) ; -PRMTRADE.fx(r,rr,trtype,ccseason,tfix)$[routes(r,rr,trtype,tfix)$routes_prm(r,rr)] = PRMTRADE.l(r,rr,trtype,ccseason,tfix) ; - -* operating reserve variables -OPRES.fx(ortype,i,v,r,h,tfix)$[Sw_OpRes$valgen(i,v,r,tfix)$reserve_frac(i,ortype)$opres_h(h)] = OPRES.l(ortype,i,v,r,h,tfix) ; - -* variable fuel amounts -GASUSED.fx(cendiv,gb,h,tfix)$[(Sw_GasCurve=0)$h_rep(h)] = GASUSED.l(cendiv,gb,h,tfix) ; -VGASBINQ_NATIONAL.fx(fuelbin,tfix)$[Sw_GasCurve=1] = VGASBINQ_NATIONAL.l(fuelbin,tfix) ; -VGASBINQ_REGIONAL.fx(fuelbin,cendiv,tfix)$[Sw_GasCurve=1] = VGASBINQ_REGIONAL.l(fuelbin,cendiv,tfix) ; -BIOUSED.fx(bioclass,r,tfix)$[sum{(i,v)$(bio(i) or cofire(i)), valgen(i,v,r,tfix) }] = BIOUSED.l(bioclass,r,tfix) ; - -* RECS variables -RECS.fx(RPSCat,i,st,ast,tfix)$[stfeas(st)$RecMap(i,RPSCat,st,ast,tfix)$(stfeas(ast) or sameas(ast,"voluntary"))$Sw_StateRPS] = RECS.l(RPSCat,i,st,ast,tfix) ; -ACP_Purchases.fx(RPSCat,st,tfix)$[(stfeas(st) or sameas(st,"voluntary"))$Sw_StateRPS] = ACP_Purchases.l(RPSCat,st,tfix) ; -EMIT.fx(etype,e,r,tfix)$emit_modeled(e,r,tfix) = EMIT.l(etype,e,r,tfix) ; - -* transmission variables -CAPTRAN_ENERGY.fx(r,rr,trtype,tfix)$routes(r,rr,trtype,tfix) = CAPTRAN_ENERGY.l(r,rr,trtype,tfix) ; -CAPTRAN_PRM.fx(r,rr,trtype,tfix)$[routes(r,rr,trtype,tfix)$routes_prm(r,rr)] = CAPTRAN_PRM.l(r,rr,trtype,tfix) ; -CAPTRAN_GRP.fx(transgrp,transgrpp,tfix)$trancap_init_transgroup(transgrp,transgrpp,"AC") = CAPTRAN_GRP.l(transgrp,transgrpp,tfix) ; -INVTRAN.fx(r,rr,trtype,tfix)$routes_inv(r,rr,trtype,tfix) = INVTRAN.l(r,rr,trtype,tfix) ; -INVTRAN_AC.fx(r,rr,tscbin,tfix)$routes_inv(r,rr,"AC",tfix) = INVTRAN_AC.l(r,rr,tscbin,tfix) ; -INV_CONVERTER.fx(r,tfix)$Sw_VSC = INV_CONVERTER.l(r,tfix) ; -CAP_CONVERTER.fx(r,tfix)$Sw_VSC = CAP_CONVERTER.l(r,tfix) ; -CONVERSION.fx(r,h,intype,outtype,tfix)$Sw_VSC = CONVERSION.l(r,h,intype,outtype,tfix) ; -CONVERSION_PRM.fx(r,ccseason,intype,outtype,tfix)$Sw_VSC = CONVERSION_PRM.l(r,ccseason,intype,outtype,tfix) ; -CAP_SPUR.fx(x,tfix)$[Sw_SpurScen$xfeas(x)] = CAP_SPUR.l(x,tfix) ; -INV_SPUR.fx(x,tfix)$[Sw_SpurScen$xfeas(x)] = INV_SPUR.l(x,tfix) ; -INV_POI.fx(r,tfix)$Sw_TransIntraCost = INV_POI.l(r,tfix) ; -TRAN_CAPEX_BINS.fx(r,rr,tscbin,tfix)$[routes_inv(r,rr,"AC",tfix)$tsc_binwidth(r,rr,tscbin)] = TRAN_CAPEX_BINS.l(r,rr,tscbin,tfix) ; - -* water climate variables -WATCAP.fx(i,v,r,tfix)$[valcap(i,v,r,tfix)$Sw_WaterMain$Sw_WaterCapacity] = WATCAP.l(i,v,r,tfix) ; -WAT.fx(i,v,w,r,h,tfix)$[i_water(i)$valgen(i,v,r,tfix)$Sw_WaterMain] = WAT.l(i,v,w,r,h,tfix) ; -WATER_CAPACITY_LIMIT_SLACK.fx(wst,r,tfix)$[Sw_WaterMain$Sw_WaterCapacity] = WATER_CAPACITY_LIMIT_SLACK.l(wst,r,tfix) ; - -*H2 and DAC production variables -PRODUCE.fx(p,i,v,r,h,tfix)$[consume(i)$i_p(i,p)$valcap(i,v,r,tfix)$h_rep(h)$Sw_Prod] = PRODUCE.l(p,i,v,r,h,tfix) ; -H2_FLOW.fx(r,rr,h,tfix)$[h2_routes(r,rr)$(Sw_H2 = 2)] = H2_FLOW.l(r,rr,h,tfix) ; -H2_TRANSPORT_INV.fx(r,rr,tfix)$[h2_routes(r,rr)$(Sw_H2 = 2)] = H2_TRANSPORT_INV.l(r,rr,tfix) ; -H2_STOR_INV.fx(h2_stor,r,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_INV.l(h2_stor,r,tfix) ; -H2_STOR_CAP.fx(h2_stor,r,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_CAP.l(h2_stor,r,tfix) ; -H2_STOR_IN.fx(h2_stor,r,h,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_IN.l(h2_stor,r,h,tfix) ; -H2_STOR_OUT.fx(h2_stor,r,h,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)] = H2_STOR_OUT.l(h2_stor,r,h,tfix) ; -H2_STOR_LEVEL.fx(h2_stor,r,actualszn,h,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)$(Sw_H2_StorTimestep=2)] = H2_STOR_LEVEL.l(h2_stor,r,actualszn,h,tfix) ; -H2_STOR_LEVEL_SZN.fx(h2_stor,r,actualszn,tfix)$[(h2_stor_r(h2_stor,r))$(Sw_H2=2)$(Sw_H2_StorTimestep=1)] = H2_STOR_LEVEL_SZN.l(h2_stor,r,actualszn,tfix) ; - -*CO2-related variables -CO2_CAPTURED.fx(r,h,tfix)$Sw_CO2_Detail = CO2_CAPTURED.l(r,h,tfix) ; -CO2_STORED.fx(r,cs,h,tfix)$[Sw_CO2_Detail$r_cs(r,cs)] = CO2_STORED.l(r,cs,h,tfix) ; -CO2_FLOW.fx(r,rr,h,tfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = CO2_FLOW.l(r,rr,h,tfix) ; -CO2_TRANSPORT_INV.fx(r,rr,tfix)$[Sw_CO2_Detail$co2_routes(r,rr)] = CO2_TRANSPORT_INV.l(r,rr,tfix) ; -CO2_SPURLINE_INV.fx(r,cs,tfix)$[Sw_CO2_Detail$r_cs(r,cs)] = CO2_SPURLINE_INV.l(r,cs,tfix) ; diff --git a/d3_data_dump.gms b/d3_data_dump.gms deleted file mode 100644 index 29c97036..00000000 --- a/d3_data_dump.gms +++ /dev/null @@ -1,424 +0,0 @@ -$ontext -This file creates a gdx file with all of the data necessary for the Augur module to solve. This includes: - - Generator capacities - - Exogenous retirments (sequential solves only) - - Wind capacity by build year (because wind CFs change by build year) - - heat rates, fuel costs, and vom costs - - capacity factors (hydro, wind) - - availability rates (1 - outage rates) - - transmission capacities and loss rates - - technology sets -$offtext - -$if not set start_year $setglobal start_year %startyear% - -*=============================== -* Set and parameter definitions -*=============================== - -set rfeas(r) "list of feasible r regions - for use in Augur only" - trange(t) "range from first year to current year" - tcur(t) "current year" - tnext(t) "next year" - valcap_i_filt(i) "subset of valcap" - valcap_ir_filt(i,r) "subset of valcap" - valcap_iv_filt(i,v) "subset of valcap" - routes_filt(r,rr,trtype) "set of transmission connections" -; - -parameter -avail_filt(i,v,r,allszn) "--fraction-- fraction of capacity available for generation by season" -can_exports_h_filt(r,allh) "--MW-- Canada exports by region and timeslice filtered for the previous solve year" -can_imports_cap(i,v,r) "--MW-- Canadian import max capacity" -can_imports_szn_filt(r,allszn) "--MWh-- Canada imports by region and season filtered for the previous solve year" -cap_converter_filt(r) "--MW-- VSC AC/DC converter capacity" -cap_exist_i(i) "--MW-- technologies with existing capacity in the current solve year" -cap_exist_ir(i,r) "--MW-- technology-region combinations with existing capacity in the current solve year" -cap_exist_iv(i,v) "--MW-- technology-vintage combinations with existing capacity in the current solve year" -cap_exist(i,v,r) "--MW-- capacity that exists in the current solve year" -cap_exog_filt(i,v,r) "--MW-- exogenous capacity" -cap_hyd_szn_adj_filt(i,allszn,r) "--fraction-- seasonal hydro capacity adjustment filtered for the previous solve year" -cap_init(i,v,r) "--MW-- initial capacity" -cap_ivrt(i,v,r,t) "--MW-- generation power capacity" -cap_energy_ivrt(i,v,r,t) "--MWh-- generation energy capacity" -cap_pvb(i,v,r) "--MW-- Hybrid PV+battery capacity (PV)" -cap_trans_energy(r,rr,trtype) "--MW-- transmission capacity for energy trading" -cap_trans_prm(r,rr,trtype) "--MW-- transmission capacity for PRM trading" -cf_adj_t_filt(i,v,t) "--fraction-- capacity factor adjustment for wind" -cost_cap_filt(i,t) "--2004$/MW-- technology capital costs" -cost_cap_fin_mult_filt(i,r,t) "--unitless-- capital cost financial multipliers" -cost_vom_filt(i,v,r) "--$/MWh-- VO&M costs filtered for the previous solve year and existing capacity" -ctt_i_ii_filt(i,ii) "--set-- set linking watercooling techs i to numeraire techs ii filtered for existing watercooling techs" -ctt_i_ii_psh(i,ii) "--set-- set linking PSH techs with water i to numeraire techs ii filtered for valid capacity techs" -emissions_price(e,r) "--2004$/metric ton-- combined emissions taxes and marginal prices for emissions caps" -emit_rate_filt(e,i,v,r) "--metric tons/MWh-- emission rate for the previous solve year" -energy_price(r,allh) "--2004$/MWh-- energy price from the previous solve year" -flex_load_opt(r,allh) "--MW-- model results for optimizing flexible load" -flex_load(r,allh) "--MW-- total exogenously defined flexible load" -fuel_price_filt(i,r) "--$/mmBTU-- fuel prices filtered for the previous solve year and existing capacity" -gen_h_stress_filt(i,r,allh,t) "--MW-- generation by stress timeslice with charge and production load as negative generation" -heat_rate_filt(i,v,r) "--MMBtu/MWh-- heat rate" -h2_usage_regional(r,allh,t) "--metric tons-- H2 usage by region" -inv_cond_filt(i,v,t) "--set-- vintage-year mapping for investments by technology" -inv_ivrt(i,v,r,t) "--MW-- investments in power generation capacity" -inv_energy_ivrt(i,v,r,t) "--MWh-- investments in energy generation capacity" -m_cf_filt(i,v,r,allh) "--fraction-- capacity factor used in the model" -m_cf_szn_filt(i,v,r,allszn) "--fraction-- modelled capacity factors filtered for hydro resources to set seasonal energy constraints" -minloadfrac_filt(r,i,allszn) "--fraction-- modelled mingen fraction filtered for hydro resources to set mingen constraints" -prod_filt(i,v,r,allh) "--MW-- power consumed for PRODUCE.l" -ra_cap_loadsite(r,t) "--MW-- capacity of flexibly sited load" -repbioprice_filt(r) "--2004$/MWh-- marginal price for biofuel in region where biofuel was used" -repgasprice_filt(r) "--$/mmBTU-- NG prices in ReEDS filtered for the previous solve year" -repgasprice_r(r,t) "--$/mmBTU-- NG prices in ReEDS, switch-dependent, at the BA level" -repgasprice(cendiv,t) "--$/mmBTU-- NG prices in ReEDS, the calculation of which depends on what switch is used" -repgasquant(cendiv,t) "--mmBTU-- NG fuel usage in ReEDS - used to determine NG price" -ret_ivrt(i,v,r,t) "--MW-- retirements of generation capacity" -ret(i,v,r) "--MW-- retirements of generation capacity" -rsc_dat_filt(i,r,sc_cat,rscbin) "--$/MW-- capital costs filtered for pumped-hydro so arbitrage value doesn't exceed capital costs" -storage_eff_filt(i) "--fraction-- storage efficiency filtered for the next solve year" -upgrade_to_filt(i,ii) "--set-- set linking upgrade techs to the tech the upgraded from filtered for existing upgrades" -; - -rfeas(r) = yes ; - -trange(t) = no ; -loop(t$[(yeart(t)>%start_year%)$(yeart(t)<=%next_year%)], -trange(t) = yes ; -) ; -trange("%next_year%") = no ; -trange("%cur_year%") = yes ; - -tcur(t) = no ; -tcur("%cur_year%") = yes ; - -tnext(t) = no ; -tnext("%next_year%") = yes ; - -*populate reduced-form sets -valcap_iv_filt(i,v) = sum{(r,t)$tcur(t), valcap(i,v,r,t)} ; -valcap_i_filt(i) = sum{v, valcap_iv_filt(i,v)} ; -valcap_ir_filt(i,r) = sum{t$tcur(t), valcap_irt(i,r,t)} ; - -*======================================= -* Removing banned technologies from sets -*======================================= - -csp_sm(i) = csp_sm(i)$(not ban(i)) ; -geo(i) = geo(i)$(not ban(i)) ; -hydro_d(i) = hydro_d(i)$(not ban(i)) ; -hydro_nd(i) = hydro_nd(i)$(not ban(i)) ; -nuclear(i) = nuclear(i)$(not ban(i)) ; -storage_duration(i) = storage_duration(i)$(not ban(i)) ; -storage_eff(i,t) = storage_eff(i,t)$(not ban(i)) ; -storage_standalone(i) = storage_standalone(i)$(not ban(i)) ; - -*============================== -* Get ReEDS generation capacity -*============================== - -cap_exist(i,v,r)$valcap_ivr(i,v,r) = sum{t$tcur(t), CAP.l(i,v,r,t) } ; -cap_exist_ir(i,r)$valcap_ir_filt(i,r) = sum{v, cap_exist(i,v,r) } ; -cap_exist_iv(i,v)$valcap_iv_filt(i,v) = sum{r, cap_exist(i,v,r) } ; -cap_exist_i(i)$valcap_i_filt(i) = sum{(r,v), cap_exist(i,v,r) } ; - -cap_ivrt(i,v,r,t)$([not (upv(i) or wind(i))]$valcap(i,v,r,t)$trange(t)) = CAP.l(i,v,r,t) ; -cap_energy_ivrt(i,v,r,t)$[valcap(i,v,r,t)$trange(t)$battery(i)] = CAP_ENERGY.l(i,v,r,t) ; -cap_ivrt(i,v,r,t)$([upv(i) or wind(i)]$valcap(i,v,r,t)) = - m_capacity_exog(i,v,r,t)$trange(t) - + sum{tt$[inv_cond(i,v,r,t,tt)$trange(tt)], - INV.l(i,v,r,tt) + INV_REFURB.l(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]} ; -cap_init(i,v,r)$([not distpv(i)]$valcap_ivr(i,v,r)) = sum{t$tcur(t), cap_ivrt(i,v,r,t)$initv(v) } ; -cap_init(i,v,r)$(distpv(i)$valcap_ivr(i,v,r)) = sum{t$tfirst(t), cap_ivrt(i,v,r,t) } ; -inv_ivrt(i,v,r,t)$[valcap(i,v,r,t)$trange(t)] = [INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)]$valinv(i,v,r,t) + UPGRADES.l(i,v,r,t)$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades] ; -inv_energy_ivrt(i,v,r,t)$[valcap(i,v,r,t)$trange(t)$battery(i)] = INV_ENERGY.l(i,v,r,t); -inv_ivrt("distpv",v,r,t)$([trange(t)$(not tfirst(t))]$valcap("distpv",v,r,t)) = cap_ivrt("distpv",v,r,t) - sum{tt$tprev(t,tt), cap_ivrt("distpv",v,r,tt) } ; -inv_ivrt("distpv","init-1",r,"%next_year%") = inv_distpv(r,"%next_year%") ; - -ret_ivrt(i,v,r,t)$([trange(t)$(not tfirst(t))$newv(v)]$valcap(i,v,r,t)) = sum{tt$tprev(t,tt), cap_ivrt(i,v,r,tt)} - cap_ivrt(i,v,r,t) + inv_ivrt(i,v,r,t) ; -ret_ivrt(i,v,r,t)$([abs(ret_ivrt(i,v,r,t) < 1e-6)]$valcap(i,v,r,t)) = 0 ; - -ret(i,v,r)$valcap_ivr(i,v,r) = sum{t, ret_ivrt(i,v,r,t) } ; - -cap_exog_filt(i,v,r)$([not canada(i)]$valcap_ivr(i,v,r)) = sum{t$tnext(t), m_capacity_exog(i,v,r,t) } ; - -gen_h_stress_filt(i,r,allh,t)$[tcur(t)$valgen_irt(i,r,t)$h_stress_t(allh,t)] = - sum{v$valgen(i,v,r,t), GEN.l(i,v,r,allh,t)} -; -*============================ -* Fuel prices -*============================ - -fuel_price_filt(i,r)$cap_exist_ir(i,r) = sum{t$tcur(t), fuel_price(i,r,t) } ; - -* populate the fuel price for H2-CT/CC techs as the marginal off the -* hydrogen demand constraint (in $/[metric tons/hour]) divided by hours and -* times h2_combustion_intensity (metric tons / mmbtu) to get $ / mmbtu -- note there should -* always be a positive value here since if an H2-CT/CC is built it consumes hydrogen -* the equation from which we extract the marginal depends on whether -* we have the national (Sw_H2 = 1) or regional (Sw_H2 = 2) constraint -h2_usage_regional(r,h,t)$tcur(t) = - hours(h) * ( - h2_exogenous_demand_regional(r,'h2',h,t) - + sum{(i,v)$[valgen(i,v,r,t)$h2_combustion(i)], - GEN.l(i,v,r,h,t) * h2_combustion_intensity * heat_rate(i,v,r,t)} - ) -; - -fuel_price_filt(i,r)$[Sw_H2$h2_combustion(i)$(sum{t$tcur(t),yeart(t) } >= h2_demand_start)$cap_exist_ir(i,r)] = - sum{t$tcur(t), - (1 / cost_scale) * (1 / pvf_onm(t)) * h2_combustion_intensity * ( - eq_h2_demand.m('h2',t)$[Sw_H2=1] -* regional demand is now by hour, so calculate annual price as the weighted average of demand across hours - + (sum{h, eq_h2_demand_regional.m(r,h,t) / hours(h) * h2_usage_regional(r,h,t) } - / sum{h, h2_usage_regional(r,h,t) } - )$[(Sw_H2=2)$(sum{h, h2_usage_regional(r,h,t) })] - ) - } -; - -* for regions that consumed biomass, use the cost of the last supply curve bin consumed -repbioprice_filt(r)$[sum{(t, bioclass), bioused.l(bioclass,r,t) }] = - sum{t$tcur(t), smax{bioclass$[bioused.l(bioclass,r,t)], - sum{usda_region$r_usda(r, usda_region), biosupply(usda_region,bioclass,"price")} } + bio_transport_cost } ; - -* for regions with no biomass, assign biomass price as the cost of the cheapest available supply curve bin for that region -* also safeguard against outlying values (for some reason smax sometimes returns -INF for regions w/o biomass consumption) -repbioprice_filt(r)$[(repbioprice_filt(r) <= 0)] = rep_bio_price_unused(r) ; - -repgasquant(cendiv,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 3)$tcur(t)] = - sum{(gb,h), GASUSED.l(cendiv,gb,h,t) * hours(h) } ; - -repgasquant(cendiv,t)$[(Sw_GasCurve = 1 or Sw_GasCurve = 2)$tcur(t)] = - sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], - hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) - } ; - -repgasprice(cendiv,t)$[(Sw_GasCurve = 0)$tcur(t)] = - smax{gb$[sum{h, GASUSED.l(cendiv,gb,h,t) }], gasprice(cendiv,gb,t) } ; - -repgasprice(cendiv,t)$[(Sw_GasCurve = 2)$tcur(t)$repgasquant(cendiv,t)] = - sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], - hours(h)*heat_rate(i,v,r,t)*fuel_price(i,r,t)*GEN.l(i,v,r,h,t) - } / (repgasquant(cendiv,t)) ; - -repgasprice_r(r,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 2)$tcur(t)] = sum{cendiv$r_cendiv(r,cendiv), repgasprice(cendiv,t) } ; - -repgasprice_r(r,t)$[(Sw_GasCurve = 1)$tcur(t)] = - ( sum{(h,cendiv), - gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * - hours(h) } / sum{h, hours(h) } - - + smax((fuelbin,cendiv)$[VGASBINQ_REGIONAL.l(fuelbin,cendiv,t)$r_cendiv(r,cendiv)], gasbinp_regional(fuelbin,cendiv,t) ) - - + smax(fuelbin$VGASBINQ_NATIONAL.l(fuelbin,t), gasbinp_national(fuelbin,t) ) - ) ; - -* catch any infinite values, assign to reference gas price -repgasprice_r(r,t)$[(repgasprice_r(r,t) = -inf or repgasprice_r(r,t) = inf)$tcur(t)] = - smax{cendiv$r_cendiv(r,cendiv), gasprice_ref(cendiv,t) } ; - -repgasprice_filt(r) = sum{t$tcur(t), repgasprice_r(r,t) } ; - -*============================ -* Filter necessary input data -*============================ - -avail_filt(i,v,r,szn)$[cap_exist_iv(i,v)$(not vre(i))] = - smax{h$h_szn(h,szn), avail(i,r,h) * derate_geo_vintage(i,v) } ; - -can_exports_h_filt(r,h) = sum{t$tcur(t), can_exports_h(r,h,t)} ; - -can_imports_cap(i,v,r)$canada(i) = sum{t$tcur(t), m_capacity_exog(i,v,r,t) } ; - -can_imports_szn_filt(r,szn) = sum{t$tcur(t), can_imports_szn(r,szn,t)} ; - -*can_exports_h_filt(r,h)$[Sw_Canada = 2] = 0 ; -*can_imports_cap(i,v,r)$[Sw_Canada = 2] = 0 ; -*can_imports_szn_filt(r,szn)$[Sw_Canada = 2] = 0 ; - -cap_hyd_szn_adj_filt(i,szn,r)$[cap_exist_ir(i,r)$hydro_d(i)] = cap_hyd_szn_adj(i,szn,r) ; - -cost_cap_filt(i,t)$[storage_standalone(i)] = cost_cap(i,t)$tnext(t) ; - -cost_cap_fin_mult_filt(i,r,t)$([storage_standalone(i)]) = cost_cap_fin_mult(i,r,t)$tnext(t) ; - -cost_vom_filt(i,v,r)$cap_exist(i,v,r) = sum{t$tcur(t), cost_vom(i,v,r,t) } ; - -cf_adj_t_filt(i,v,t)$[cap_exist_iv(i,v)$trange(t)] = cf_adj_t(i,v,t) ; -cf_adj_t_filt(i,v,"%next_year%") = cf_adj_t(i,v,"%next_year%")$(vre(i) or pvb(i)) ; - -ctt_i_ii_filt(i,ii) = ctt_i_ii(i,ii)$cap_exist_i(i) ; - -ctt_i_ii_psh(i,ii) = ctt_i_ii(i,ii)$[valcap_i_filt(i)$psh(i)] ; - -emit_rate_filt(e,i,v,r)$cap_exist(i,v,r) = sum{(t,etype)$tcur(t), emit_rate(etype,e,i,v,r,t) } ; - -heat_rate_filt(i,v,r)$cap_exist(i,v,r) = sum{t$tcur(t), heat_rate(i,v,r,t) } ; - -inv_cond_filt(i,v,t)$[(vre(i) or pvb(i))$tnext(t)] = sum{(tt,r), inv_cond(i,v,r,tt,t) } ; - -m_cf_filt(i,v,r,h)$[(vre(i) or pvb(i))$cap_exist(i,v,r)] = sum{t$tnext(t), m_cf(i,v,r,h,t) } ; - -m_cf_szn_filt(i,v,r,szn)$[hydro(i)$cap_exist(i,v,r)] = sum{t$tcur(t), m_cf_szn(i,v,r,szn,t) } ; - -minloadfrac_filt(r,i,szn)$[hydro(i)$cap_exist_ir(i,r)$szn_rep(szn)] = - sum{h$h_szn(h,szn), minloadfrac(r,i,h) * hours(h) } / sum{h$h_szn(h,szn), hours(h) } ; - -rsc_dat_filt(i,r,"cost",rscbin)$[storage_standalone(i)$cap_exist_ir(i,r)] = rsc_dat(i,r,"cost",rscbin) ; - - -storage_eff_filt(i)$storage(i) = sum{t$tnext(t), storage_eff(i,t) } ; - -upgrade_to_filt(i,ii) = upgrade_to(i,ii)$cap_exist_i(i) ; - -*============================ -* Get ReEDS transmission data -*============================ - -cap_trans_energy(r,rr,trtype) = sum{t$tcur(t), CAPTRAN_ENERGY.l(r,rr,trtype,t) } ; -cap_trans_prm(r,rr,trtype) = sum{t$tcur(t), CAPTRAN_PRM.l(r,rr,trtype,t) } ; - -cap_converter_filt(r) = sum{t$tcur(t), CAP_CONVERTER.l(r,t) } ; - -* In Augur, trtype="AC" includes everything except for VSC -routes_filt(r,rr,trtype) = sum{t$tcur(t), routes(r,rr,trtype,t) } ; - -*============================ -* Flexible load data -*============================ - -flex_load(r,h) = sum{(flex_type,t)$tcur(t), load_exog_flex(flex_type,r,h,t) } ; - -flex_load_opt(r,h) = sum{(flex_type,t)$tcur(t), FLEX.l(flex_type,r,h,t) } ; - -ra_cap_loadsite(r,t)$[Sw_LoadSiteCF$val_loadsite(r)] = CAP_LOADSITE.l(r,t) ; - -*============================ -* Extra consumption data -*============================ - -prod_filt(i,v,r,h)$[sum{t$tcur(t), valcap(i,v,r,t)}$consume(i)$hours(h)] = - sum{(p,t)$[i_p(i,p)$tcur(t)], PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) } ; - -*============================ -* Get ReEDS emissions prices [$/metric ton] -*============================ -* NOT included: eq_emit_rate_limit (disabled by default), eq_CSAPR_Budget, eq_CSAPR_Assurance -emissions_price(e,r) = - (1 / cost_scale) - * sum{t$tcur(t), - (1 / pvf_onm(t)) * eq_annual_cap.m(e,t) - + emit_tax(e,r,t) - } ; - -* Add marginal prices from CO2-specific constraints -emissions_price("CO2",r) = - emissions_price("CO2",r) - + (1 / cost_scale) - * sum{t$tcur(t), - (1 / pvf_onm(t)) * [ - eq_RGGI_cap.m(t)$RGGI_R(r) - + sum{st$r_st(r,st), eq_state_cap.m(st,t) } - ] - } ; - -*=================================== -* Get ReEDS energy prices ($/MWh) -*=================================== - -energy_price(r,h)$hours(h) = - sum{t$tcur(t), - (1 / cost_scale) * (1 / pvf_onm(t)) * eq_supply_demand_balance.m(r,h,t) / hours(h) } ; - -*======================================= -* Unload all relevant data to a gdx file -*======================================= - -execute_unload 'ReEDS_Augur%ds%augur_data%ds%reeds_data_%cur_year%.gdx' - avail_filt - bcr - bir_pvb_config - can_exports_h_filt - can_imports_cap - can_imports_szn_filt - cap_converter_filt - cap_exog_filt - cap_hyd_szn_adj_filt - cap_init - cap_ivrt - cap_energy_ivrt - cap_trans_energy - cap_trans_prm - cf_adj_t_filt - converter_efficiency_vsc - cost_cap_filt - cost_cap_fin_mult_filt - cost_vom_filt - csp_sm - ctt_i_ii_filt - ctt_i_ii_psh - degrade_annual - emissions_price - emit_rate_filt - energy_price - flex_load - flex_load_opt - fuel_price_filt - fuel2tech - gen_h_stress_filt - geo - h_szn - heat_rate_filt - hierarchy - hydro_d - hydro_nd - hours - hydmin - i - ilr - ilr_pvb_config - i_subsets - inv_cond_filt - inv_ivrt - inv_energy_ivrt - ivt_num - m_cf_filt - m_cf_szn_filt - maxage - minloadfrac_filt - notvsc - nuclear - prm - prod_filt - pvf_onm - r - rfeas - r_cendiv - ra_cap_loadsite - repbioprice_filt - repgasprice_filt - ret - ret_ivrt - routes_filt - rsc_dat_filt - sdbin - storage_duration - storage_eff - storage_eff_filt - storage_standalone - Sw_VSC - szn - tfirst - tmodel_new - tranloss - trtype - upgrade_to_filt - v - vom_hyd -; - - -*** dump data for tax credit phaseout calculations -execute_unload "outputs%ds%tc_phaseout_data%ds%emit_for_tc_phaseout_calc_%cur_year%.gdx" - emit_nat_tc, emit_r_tc -; \ No newline at end of file diff --git a/d_solve_iterate.py b/d_solve_iterate.py deleted file mode 100644 index d22675fe..00000000 --- a/d_solve_iterate.py +++ /dev/null @@ -1,179 +0,0 @@ -#%% Imports -import os -import site -import argparse -import pandas as pd -import subprocess -from glob import glob -import reeds -import Augur - - -#%% Main function -def run_reeds(casepath, t, onlygams=False, iteration=0): - """ - """ - # #%% Arguments for testing - # casepath = os.path.expanduser('~/github/ReEDS-2.0/runs/v20230512_prasM0_ERCOT') - # t = 2020 - # onlygams = 0 - # iteration = 0 - # os.chdir(casepath) - - #%% Inferred inputs - site.addsitedir(casepath) - import runbatch - - #%% Get the run settings - sw = reeds.io.get_switches(casepath) - years = pd.read_csv( - os.path.join(casepath,'inputs_case','modeledyears.csv') - ).columns.astype(int).values - tprev = {**{years[0]:years[0]}, **dict(zip(years[1:], years))} - tnext = {**dict(zip(years, years[1:])), **{years[-1]:years[-1]}} - - #%%### Run GAMS LP - if not onlyaugur: - #%% Get the command to run GAMS for this solve year - batch_case = os.path.basename(casepath) - stress_year = f"{t}i{iteration}" - ### Get the restartfile (last iteration from previous year) - if t == min(years): - restartfile = batch_case - else: - restartfile = sorted( - glob(os.path.join(casepath,'g00files',f"{batch_case}_{tprev[t]}i*")) - )[-1] - - cmd_gams = runbatch.solvestring_sequential( - batch_case=batch_case, - caseSwitches=sw, - cur_year=t, - next_year=tnext[t], - prev_year=tprev[t], - stress_year=stress_year, - restartfile=restartfile, - hpc=int(sw['hpc']), - iteration=iteration, - ) - print(cmd_gams) - - ### Run GAMS LP - result = subprocess.run(cmd_gams, shell=True) - if result.returncode: - raise Exception(f'd_solveoneyear.gms failed with return code {result.returncode}') - - #%% Add solve time to run metadata - try: - cmd_log = ( - f"python {os.path.join(casepath, 'reeds', 'log.py')}" - f" --year={t}\n" - ) - subprocess.run(cmd_log, shell=True) - except Exception as err: - print(err) - - #%% Check to see if the restart file exists - savefile = f"{batch_case}_{t}i{iteration}" - if not os.path.isfile(os.path.join("g00files", savefile+".g00")): - raise Exception(f"Missing {savefile}.g00") - - - #%%### Run Augur - if (not onlygams) and (tnext[t] > int(sw.GSw_SkipAugurYear)): - Augur.main(t=t, tnext=tnext[t], casedir=casepath, iteration=iteration) - - -#%% Driver function -def main(casepath, t, overwrite=False): - """ - """ - ### Get the run settings - sw = reeds.io.get_switches(casepath) - for iteration in range(int(sw.GSw_PRM_StressIterateMax)): - #%% If not overwriting, skip iterations that have already finished - if ( - (not overwrite) - ## Check if GAMS finished - and os.path.isfile( - os.path.join( - sw.casedir, 'g00files', - f"{os.path.basename(sw.casedir)}_{t}i{iteration}.g00")) - ## Check if the output of hourly_writetimeseries.py for this year/iteration - ## exists, indicating stress period calcluations finished (or that we're not - ## using stress periods) - and os.path.isfile( - os.path.join( - sw.casedir, 'inputs_case', f'stress{t}i{iteration+1}', 'cf_vre.csv')) - ## Check if Augur finished - and os.path.isfile( - os.path.join( - sw.casedir, 'ReEDS_Augur', 'augur_data', f'ReEDS_Augur_{t}.gdx')) - ): - print(f'Already ran {t}i{iteration} so continuing to next iteration') - continue - - #%% Run ReEDS and Augur - run_reeds(casepath, t, iteration=iteration) - - #%% Stop here if there's no stress period data for the next iteration - ### (either because we're not iterating or because the threshold was met) - if not os.path.isfile( - os.path.join( - sw.casedir, 'inputs_case', f'stress{t}i{iteration+1}', 'set_h.csv') - ): - print('No new stress periods to add, so moving to next solve year') - break - ### Otherwise continue iterating - else: - print(f'NEUE threshold was not met, so performing iteration {iteration+1}') - - ### Delete old restart files if desired - years = pd.read_csv( - os.path.join(casepath,'inputs_case','modeledyears.csv') - ).columns.astype(int).values - tprev = {**{years[0]:years[0]}, **dict(zip(years[1:], years))} - - if ((not int(sw['keep_g00_files'])) and (not int(sw['debug']))) and (min(years) < t): - g00files = glob(os.path.join(casepath, 'g00files', f'*{tprev[t]}i*.g00')) - for i in g00files: - os.remove(i) - - -#%% Procedure -if __name__ == '__main__': - #%% Argument inputs - import argparse - parser = argparse.ArgumentParser(description='Sequential ReEDS') - parser.add_argument('casepath', type=str, - help='path to ReEDS run folder') - parser.add_argument('t', type=int, - help='year to run') - parser.add_argument('--iteration', '-i', type=int, default=0, - help='iteration counter for this run') - parser.add_argument('--onlygams', '-g', action='store_true', - help='Only run GAMS (skip Augur)') - parser.add_argument('--onlyaugur', '-a', action='store_true', - help='Only run Augur (skip GAMS)') - parser.add_argument('--overwrite', '-o', action='store_true', - help='Overwrite iterations that have already finished') - - args = parser.parse_args() - casepath = args.casepath - t = args.t - iteration = args.iteration - onlygams = args.onlygams - onlyaugur = args.onlyaugur - overwrite = args.overwrite - - #%% Switch to run folder - os.chdir(casepath) - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(casepath,'gamslog.txt'), - ) - - #%% Run it - main(casepath=casepath, t=t, overwrite=overwrite) diff --git a/d_solveallyears.gms b/d_solveallyears.gms deleted file mode 100644 index 204d1bc5..00000000 --- a/d_solveallyears.gms +++ /dev/null @@ -1,165 +0,0 @@ -* global needed for this file: -* case : name of case you're running -* niter : current iteration - -$setglobal ds %ds% - -$ifthen.unix %system.filesys% == UNIX -$setglobal ds / -$endif.unix - -$log 'Running intertemporal solve for...' -$log ' case == %case%' -$log ' iteration == %niter%' - -*remove any load years -tload(t) = no ; - -$if not set niter $setglobal niter 0 -$eval previter %niter%-1 - -*if this isn't the first iteration -$ifthene.notfirstiter %niter%>0 - -$if not set load_ref_dem $setglobal load_ref_dem 0 - -$ifthene.loadref %load_ref_dem% == 0 -* need to load psupply0 and load_exog0... -* should also set load_exog to load_exog0 - - -$endif.loadref - -*============================ -* --- CC and Curtailment --- -*============================ - -*indicate we're loading data -tload(t)$tmodel(t) = yes ; - -$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_merged_%niter%.gdx -$loaddcr cc_old_load2 = cc_old -$loaddcr cc_mar_load2 = cc_mar -$loaddcr cc_evmc_load2 = cc_evmc -$loaddcr sdbin_size_load2 = sdbin_size -$gdxin - -cc_old_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_old_load2(loadset,i,r,ccreg,szn,t) } } ; -cc_mar_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_mar_load2(loadset,i,r,ccreg,szn,t) } } ; - -cc_evmc_load(i,r,szn,t) = sum{loadset, cc_evmc_load2(loadset,i,r,szn,t) } ; - -sdbin_size_load(ccreg,szn,sdbin,t) = sum{loadset, sdbin_size_load2(loadset,ccreg,szn,sdbin,t) } ; - -*=============================== -* --- Begin Capacity Credit --- -*=============================== - -*Clear params before calculation -cc_int(i,v,r,szn,t) = 0 ; -cc_totmarg(i,r,szn,t) = 0 ; -cc_excess(i,r,szn,t) = 0 ; -cc_scale(i,r,szn,t) = 0 ; -sdbin_size(ccreg,szn,sdbin,t) = 0 ; - -*Storage duration bin sizes by year -sdbin_size(ccreg,szn,sdbin,t)$tload(t) = sdbin_size_load(ccreg,szn,sdbin,t) ; - -*Sw_Int_CC=0 means use average capacity credit for each tech, and don't differentiate vintages -*If there is no existing capacity to calculate average, use marginal capacity credit instead. -if(Sw_Int_CC=0, - cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)$sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) }] = - cc_old_load(i,r,szn,t) / sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) } ; - cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)$(cc_old_load(i,r,szn,t)=0)] = m_cc_mar(i,r,szn,t) ; -) ; - -*For the remaining options we initially use marginal values for cc_int, differentiated by vintage based on seasonal capacity factors. -if(Sw_Int_CC=1 or Sw_Int_CC=2, - cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)$sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) }] = - m_cc_mar(i,r,szn,t) * m_cf_szn(i,v,r,szn,t) / sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) } ; - cc_totmarg(i,r,szn,t)$[tload(t)$vre(i)] = sum{v$valcap(i,v,r,t), cc_int(i,v,r,szn,t) * CAP.l(i,v,r,t) } ; -) ; - -*Sw_Int_CC=1 means use average capacity credit for each tech, but differentiate based on vintage. -*Start with marginal capacity credit with seasonal vintage-based capacity factor adjustment, -*and scale with cc_old_load to result in the correct total capacity credit. -if(Sw_Int_CC=1, - cc_scale(i,r,szn,t)$[tload(t)$vre(i)] = 1 ; - cc_scale(i,r,szn,t)$[tload(t)$vre(i)$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) / cc_totmarg(i,r,szn,t) ; - cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)] = cc_int(i,v,r,szn,t) * cc_scale(i,r,szn,t) ; -) ; - -*Sw_Int_CC=2 means use marginal capacity credit, adjusted by seasonal capacity factors by vintage -if(Sw_Int_CC=2, - cc_excess(i,r,szn,t)$[tload(t)$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) - cc_totmarg(i,r,szn,t) ; -) ; - - -*no longer want m_cc_mar since it should not enter the planning reserve margin constraint -m_cc_mar(i,r,szn,t) = 0 ; - -cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) > 1] = 1 ; -cc_int(i,v,r,szn,t)$[tload(t)$csp_storage(i)$valcap(i,v,r,t)] = 1 ; - -*======================================= -* --- Begin Averaging of CC/Curt --- -*======================================= - -$ifthene.afterseconditer %niter%>1 - -*when set to 1 - it will take the average over all previous iterations -if(Sw_AVG_iter=1, - cc_int(i,v,r,szn,t)$[tload(t)$vre(i)$valcap(i,v,r,t)] = - (cc_int(i,v,r,szn,t) + cc_iter(i,v,r,szn,t,"%previter%")) / 2 ; - ) ; - -$endif.afterseconditer - -*Remove very small numbers to make it easier for the solver -cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) < 0.001] = 0 ; - -cc_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) ; - -execute_unload 'ReEDS_Augur%ds%augur_data%ds%curtout_%case%_%niter%.gdx' cc_int ; - -*following line will load in the level values if the switch is enabled -*note that this is still within the conditional that we are now past the first iteration -*and thus a loadpoint is enabled -if(Sw_Loadpoint = 1, -execute_loadpoint 'gdxfiles%ds%%case%_load.gdx' ; -ReEDSmodel.optfile = 8 ; -) ; - -$endif.notfirstiter - - -* rounding of all cc and curt parameters -* used in the intertemporal case - -cc_int(i,v,r,szn,t) = round(cc_int(i,v,r,szn,t), 4) ; -cc_totmarg(i,r,szn,t) = round(cc_totmarg(i,r,szn,t), 4) ; -cc_excess(i,r,szn,t) = round(cc_excess(i,r,szn,t), 4) ; -cc_scale(i,r,szn,t) = round(cc_scale(i,r,szn,t), 4) ; - - -*============================== -* --- Solve Supply Side --- -*============================== - -solve ReEDSmodel using lp minimizing z ; - -if(Sw_Loadpoint = 1, -execute_unload 'gdxfiles%ds%%case%_load.gdx' ; -) ; - -*============================ -* --- Iteration Tracking --- -*============================ - -cap_iter(i,v,r,t,"%niter%")$valcap(i,v,r,t) = CAP.l(i,v,r,t) ; -cap_energy_iter(i,v,r,t,"%niter%")$valcap(i,v,r,t) = CAP_ENERGY.l(i,v,r,t) ; -gen_iter(i,v,r,t,"%niter%")$valcap(i,v,r,t) = sum{h, GEN.l(i,v,r,h,t) * hours(h) } ; -gen_iter(i,v,r,t,"%niter%")$[vre(i)$valcap(i,v,r,t)] = sum{h, m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; -cap_firm_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) * CAP.l(i,v,r,t) ; -cap_firm_iter(i,v,r,szn,t,"%niter%")$storage(i) = sum{sdbin, CAP_SDBIN.l(i,v,r,szn,sdbin,t) * cc_storage(i,sdbin) } ; -cap_energy_firm_iter(i,v,r,szn,t,"%niter%")$storage(i) = sum{sdbin, CAP_SDBIN_ENERGY.l(i,v,r,szn,sdbin,t) } ; \ No newline at end of file diff --git a/d_solveoneyear.gms b/d_solveoneyear.gms deleted file mode 100644 index b3484a80..00000000 --- a/d_solveoneyear.gms +++ /dev/null @@ -1,290 +0,0 @@ -* Includes these scripts, in order: -* - d1_temporal_params.gms -* - d1_financials.gms -* - * solves the model * -* - d2_post_solve_adjustments.gms -* - d2_varfix.gms -* - d3_data_dump.gms - -$setglobal ds \ - -$ifthen.unix %system.filesys% == UNIX -$setglobal ds / -$endif.unix - -* globals needed for this file: -* case : name of case you're running -* cur_year : current year - -*remove any load years -tload(t) = no ; - -* --- reset tmodel --- -tmodel(t) = no ; -tmodel("%cur_year%") = yes ; - -$log 'Solving sequential case for...' -$log ' Case: %case%' -$log ' Year: %cur_year%' - - -*** Define the h- and szn-dependent parameters -$onMultiR -$include d1_temporal_params.gms -$offMulti - - -* need to have values initialized before making adjustments -* thus cannot perform these adjustments until 2010 has solved -$ifthene.post_startyear %cur_year%>%startyear% -* Here we calculate the RHS value of eq_rsc_INVlim because floating point -* differences can cause small number issues that either make the model -* infeasible or result in very tiny number (order 1e-16) in the matrix -rhs_eq_rsc_INVlim(r,i,rscbin,t)$[tmodel(t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i)] = - -*capacity indicated by the resource supply curve (with undiscovered geo available -*at the "discovered" amount and hydro upgrade availability adjusted over time) - m_rsc_dat(r,i,rscbin,"cap") * ( - 1$[not geo_hydro(i)] + geo_discovery(i,r,t)$geo_hydro(i)) - + hyd_add_upg_cap(r,i,rscbin,t)$(Sw_HydroCapEnerUpgradeType=1) -*minus the cumulative invested capacity in that region/class/bin... -*Note that yeart(tt) is stricly < here, while it is <= in eq_rsc_INVlim. That is because -*values where yeart(tt)==yeart(t) are variables rather than parameters because they are not -*values from prior solve years. - - sum{(ii,v,tt)$[valinv(ii,v,r,tt)$(yeart(tt) < yeart(t))$rsc_agg(i,ii)], - INV_RSC.l(ii,v,r,rscbin,tt) * resourcescaler(ii) } -*minus exogenous (pre-start-year) capacity, using its level in the first year (tfirst) - - sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], - capacity_exog_rsc(ii,v,r,rscbin,tt) } -; - - -flag_eq_rsc_INVlim(r,i,rscbin,t)$tmodel(t) = no ; - -* Identify instances when the RHS values are within rhs_tolerance of zero -flag_eq_rsc_INVlim(r,i,rscbin,t)$[tmodel(t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i) - $(rhs_eq_rsc_INVlim(r,i,rscbin,t) > -rhs_tolerance) - $(rhs_eq_rsc_INVlim(r,i,rscbin,t) < rhs_tolerance)] = yes ; - -* When RHS is 0 (or close enough), the eq_rsc_INVlim equation says that all relevant INV_RSC are 0. -* Therefore we can set the INV_RSC variable to zero anywhere the flag_eq_rsc_INVlim is true -loop(i$rsc_i(i), - INV_RSC.fx(ii,v,r,rscbin,t)$[tmodel(t)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i) - $(flag_eq_rsc_INVlim(r,i,rscbin,t))$(valinv(ii,v,r,t)$rsc_agg(i,ii))] = 0 ; -) ; - -* set m_capacity_exog to the maximum of either its original amount -* or the amount of upgraded capacity that has occurred in the past "Sw_UpgradeLifespan" years -* to avoid forcing recently upgraded capacity into retirement -if(Sw_Upgrades = 1, - - m_capacity_exog(i,v,r,t)$[valcap(i,v,r,t)$sameas(t,"%cur_year%") - $(sum{(ii,tt)$[(tt.val <= t.val)$(t.val - tt.val <= Sw_UpgradeLifespan) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], UPGRADES.l(ii,v,r,tt) } ) ] = -* [maximum of] initial capacity recorded in d_solveprep - max( m_capacity_exog0(i,v,r,t), -* -or- capacity of upgrades that have occurred from this i v r t combination - sum{(ii,tt)$[(tt.val <= t.val)$(t.val - tt.val <= Sw_UpgradeLifespan) - $valcap(ii,v,r,tt)$upgrade_from(ii,i)], - UPGRADES.l(ii,v,r,tt) } - ) ; - -) ; - -* if the relative growth constraint is turned on, then calculate the growth -* limits for each growth bin -if(Sw_GrowthPenalties > 0, - -* Calculate the maximum deployment that could have been achieved in the last modeled -* year. For example, if tmodel is 2023 and the prior two solve years were 2020 and -* 2015, then we are calculating the maximum deployment that could have occured in -* 2020 at the growth rate specified in gbin1. This requires looking back to tprev -* and the solve year before tprev, hence the need for the yeart(ttt). -* The denominator is simply a discount term, and the multiplication is an associated -* compounding term. - last_year_max_growth(st,tg,t)$tmodel(t) = - sum{(i,v,r,tt)$[valinv(i,v,r,tt)$r_st(r,st)$tg_i(tg,i)$tprev(t,tt)], - INV.l(i,v,r,tt) } - / sum{allt$[(allt.val>sum{tt$tprev(t,tt), sum{ttt$tprev(tt,ttt), yeart(ttt) } }) - $(allt.val<=sum{tt$tprev(t,tt), yeart(tt) })], - (growth_bin_size_mult("gbin1") ** (allt.val - sum{tt$tprev(t,tt), sum{ttt$tprev(tt,ttt), yeart(ttt) } } - 1)) } - * (growth_bin_size_mult("gbin1") ** ((sum{tt$tprev(t,tt), yeart(tt) - sum{ttt$tprev(tt,ttt), yeart(ttt) } }) - 1)) ; - -* Now calculate the growth bin size for the current solve year, assuming that the -* maximum growth allowed in gbin1 happens each year over the current solve period. - growth_bin_limit("gbin1",st,tg,t)$tmodel(t) = - sum{allt$[(allt.val>sum{tt$tprev(t,tt), yeart(tt) }) - $(allt.val<=yeart(t))], - last_year_max_growth(st,tg,t) * growth_bin_size_mult("gbin1") ** (allt.val - sum{tt$tprev(t,tt), yeart(tt) }) } - / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; - -* Do not allow growth_bin_limit to decline over time (i.e., if a higher growth -* rate was achieved in the past, allow the model to start from that higher level) - growth_bin_limit("gbin1",st,tg,t)$tmodel(t) = smax{tt, growth_bin_limit("gbin1",st,tg,tt) } ; - -* If the calculated gbin1 value is less than the minimum bin size, then set it to the minimum bin size - growth_bin_limit("gbin1",st,tg,t)$[tmodel(t)$(growth_bin_limit("gbin1",st,tg,t) < gbin_min(tg))$stfeas(st)] = gbin_min(tg) ; - -* Now set the size of the remaining bins - growth_bin_limit(gbin,st,tg,t)$[tmodel(t)$(not sameas(gbin,"gbin1"))] = - growth_bin_limit("gbin1",st,tg,t) * (growth_bin_size_mult(gbin) - growth_bin_size_mult("gbin1")) ; - - growth_bin_limit(gbin,st,tg,t)$growth_bin_limit(gbin,st,tg,t) = round(growth_bin_limit(gbin,st,tg,t),0) ; - -) ; - -$endif.post_startyear - -* Load capacity credit results -$ifthene.tcheck %cur_year%>%GSw_SkipAugurYear% - -*indicate we're loading data -tload("%cur_year%") = yes ; - -*file written by ReEDS_Augur.py -* loaddcr = domain check (dc) + overwrite values storage previously (r) -$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_%prev_year%.gdx -$loaddcr cc_old_load = cc_old -$loaddcr cc_mar_load = cc_mar -$loaddcr cc_evmc_load = cc_evmc -$loaddcr sdbin_size_load = sdbin_size -$gdxin - -*Note: these values are rounded before they are written to the gdx file, so no need to round them here - -* assign old and marginal capacity credit parameters to those -* corresponding to each balancing areas cc region -cc_old(i,r,ccseason,t)$[tload(t)$(vre(i) or csp(i) or pvb(i))] = - sum{ccreg$r_ccreg(r,ccreg), cc_old_load(i,r,ccreg,ccseason,t) } ; - -m_cc_mar(i,r,ccseason,t)$[tload(t)$(vre(i) or csp(i) or pvb(i))] = - sum{ccreg$r_ccreg(r,ccreg), cc_mar_load(i,r,ccreg,ccseason,t) } ; - -sdbin_size(ccreg,ccseason,sdbin,t)$tload(t) = sdbin_size_load(ccreg,ccseason,sdbin,t) ; - -* --- Assign hybrid PV+battery capacity credit --- -* Limit the capacity credit of hybrid PV such that the total capacity credit from the -* PV and the battery do not exceed the inverter limit. -* Example: * PV = 130 MWdc, Battery = 65 MW, Inverter = 100 MW (PVdc/Battery=0.5; PVdc/INVac=1.3) -* Assuming the capacity credit of the Battery is 65 MW, then capacity credit of the PV -* is limited to 35 MW or 0.269 (35MW/130MW) on a relative basis. -* Max capacity credit PV [MWac/MWdc] = (Inverter - Battery capacity credit) / PV_dc -* = (PV_dc / ILR - PV_dc * BCR) / PV_dc -* = 1/ILR - BCR -* marginal capacity credit -m_cc_mar(i,r,ccseason,t)$[tload(t)$pvb(i)] = min{ m_cc_mar(i,r,ccseason,t), 1 / ilr(i) - bcr(i) } ; - -* old capacity credit -* (1) convert cc_old from MW to a fractional basis -* (2) adjust the fractional value to be less than 1/ILR - BCR -* (3) multiply by CAP to convert back to MW -cc_old(i,r,ccseason,t)$[tload(t)$pvb(i)$sum{(v,tt)$tprev(t,tt), CAP.l(i,v,r,tt)}] = - min{ cc_old(i,r,ccseason,t) / sum{(v,tt)$tprev(t,tt), CAP.l(i,v,r,tt)}, 1 / ilr(i) - bcr(i) } - * sum{(v,tt)$tprev(t,tt), CAP.l(i,v,r,tt)}; - -$endif.tcheck - - -*** Calculate financial multipliers -* These are calculated here because the ITC phaseout can influence these parameters, -* and the timing of the phaseout is not known beforehand. -$include d1_financials.gms - - -$ifthene %cur_year%==%startyear% -*initialize CAP.l for 2010 because it has not been defined yet -CAP.l(i,v,r,"%startyear%")$[m_capacity_exog(i,v,r,"%startyear%")] = m_capacity_exog(i,v,r,"%startyear%") ; -$endif - -$ifthene %cur_year%==%startyear% -*initialize CAP_ENERGY.l for 2010 because it has not been defined yet -CAP_ENERGY.l(i,v,r,"%startyear%")$[m_capacity_exog_energy(i,v,r,"%startyear%")] = m_capacity_exog_energy(i,v,r,"%startyear%") ; -$endif - -* Now that cost_cap_fin_mult is done, calculate cost_growth, which is -* the minimum cost of that technology within a state -if(Sw_GrowthPenalties > 0, -*rsc_fin_mult holds the multipliers for sccapcosttech, so don't include them here - cost_growth(i,st,t)$[tmodel(t)$sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)$(not sccapcosttech(i))] = - smin{r$[valinv_irt(i,r,t)$r_st(r,st)$cost_cap_fin_mult(i,r,t)], - cost_cap_fin_mult(i,r,t) * cost_cap(i,t) } ; - -*rsc_fin_mult holds the capital costs for sccapcosttech - cost_growth(i,st,t)$[tmodel(t)$sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)$sccapcosttech(i)] = - smin{(r,rscbin)$[valinv_irt(i,r,t)$r_st(r,st)$rsc_fin_mult(i,r,t)], - rsc_fin_mult(i,r,t) * m_rsc_dat(r,i,rscbin,"cost") } ; - - cost_growth(i,st,t)$cost_growth(i,st,t) = round(cost_growth(i,st,t),3) ; -) ; - -* Write the inputs for debugging and error checks: -* Always write data for the first solve year (currently always 2010). -* Overwrites the versions written by d_solveprep.gms and d1_temporal_params.gms. -$ifthene.write %cur_year%=%startyear% -execute_unload 'inputs_case%ds%inputs.gdx' ; -$endif.write - -* If using debug mode, write the inputs for every solve year -$ifthene.debug %debug%>0 -execute_unload 'alldata_%stress_year%.gdx' ; -$endif.debug - - -* --- diagnoses gdx dump settings --- -$ifthene.diagnose %diagnose%=1 -$ifthene.diagnose_2 %diagnose_year%<=%cur_year% -$include inputs_case%ds%diagnose.gms -$endif.diagnose_2 -$endif.diagnose - - -* ------------------------------ -* Solve the Model -* ------------------------------ -$ifthen.valstr %GSw_ValStr% == 1 -OPTION lp = convert ; -ReEDSmodel.optfile = 1 ; -$echo dumpgdx ReEDSmodel_jacobian.gdx > convert.opt -solve ReEDSmodel minimizing z using lp ; -OPTION lp = %solver% ; -ReEDSmodel.optfile = %GSw_gopt% ; -OPTION savepoint = 1 ; -$endif.valstr - -solve ReEDSmodel minimizing z using lp ; -tsolved(t)$tmodel(t) = yes ; - -* record objective function values right after solve -z_rep(t)$tmodel(t) = Z.l ; -z_rep_inv(t)$tmodel(t) = Z_inv.l(t) ; -z_rep_op(t)$tmodel(t) = Z_op.l(t) ; - - -* --------------------------------- -* Modeling to Generate Alternatives -* --------------------------------- -$ifthene.mga %GSw_MGA_CostDelta%>0 -$ifthene.mga1 %cur_year%>=%GSw_StartMarkets% -*## Activate MGA mode -Sw_MGA = 1 ; -solve ReEDSmodel %GSw_MGA_Direction%imizing MGA_OBJ using lp ; -*## Deactivate MGA mode -Sw_MGA = 0 ; -$endif.mga1 -$endif.mga - - -*** Adjust some parameters based on the solution for this solve year -$include d2_post_solve_adjustments.gms - -*** Fix decision variables to their optimized levels for this solve year -tfix("%cur_year%") = yes ; -$include d2_varfix.gms - -*** Dump data used in calculations between solve years -$include d3_data_dump.gms - -*** Abort if the solver returns an error -if (ReEDSmodel.modelStat > 1, - abort "Model did not solve to optimality", - ReEDSmodel.modelStat) ; diff --git a/d_solvepcm.gms b/d_solvepcm.gms deleted file mode 100644 index 711b58c7..00000000 --- a/d_solvepcm.gms +++ /dev/null @@ -1,25 +0,0 @@ -*** Reset years -tmodel(t) = no ; -tmodel("%cur_year%") = yes ; -set t_unfix(t) "year to unfix variables when rerunning a single solve year" ; -t_unfix("%cur_year%") = yes ; - -*** Activate PCM mode -Sw_PCM = 1 ; -Sw_MinCF = 0 ; - -*** Unfix the operational variables -$include d2_unfix_op.gms - -*** Define the h- and szn-dependent parameters -$onMultiR -$include d1_temporal_params.gms -$offMulti - -*** Solve it -solve ReEDSmodel minimizing Z using lp ; - -*** Abort if the solver returns an error -if (ReEDSmodel.modelStat > 1, - abort "Model did not solve to optimality", - ReEDSmodel.modelStat) ; diff --git a/d_solveprep.gms b/d_solveprep.gms deleted file mode 100644 index dbaa6df9..00000000 --- a/d_solveprep.gms +++ /dev/null @@ -1,254 +0,0 @@ -$setglobal ds \ - -$ifthen.unix %system.filesys% == UNIX -$setglobal ds / -$endif.unix - -Model ReEDSmodel /all/ ; - -*================================= -* -- MODEL AND SOLVER OPTIONS -- -*================================= - -OPTION lp = %solver% ; -ReEDSmodel.optfile = %GSw_gopt% ; -*treat fixed variables as parameters -ReEDSmodel.holdfixed = 1 ; - -$ifthen %solver%==CBC -* adjust the GAMS infeasibility tolerance to handle empty rows when using CBC -ReEDSmodel.tolinfeas = 1e-15 ; -$endif - - -$if not set loadgdx $setglobal loadgdx 0 - -$ifthen.gdxin %loadgdx% == 1 -execute_loadpoint "gdxfiles%ds%%gdxfin%.gdx" ; -Option BRatio = 0.0 ; -$endif.gdxin - -*================================================ -* --- Parameters only used when loading data --- -*================================================ - -set tload(t) "years in which data is loaded" ; -tload(t) = no ; - -parameter - cc_old_load(i,r,ccreg,ccseason,t) "--MW-- cc_old loading in from the cc_out gdx file" - sdbin_size_load(ccreg,ccseason,sdbin,t) "--MW-- bin_size power loading in from the cc_out gdx file" - cc_mar_load(i,r,ccreg,ccseason,t) "--fraction-- cc_mar loading in from the cc_out gdx file" - cc_evmc_load(i,r,ccseason,t) "--fraction-- cc_evmc loading in from the cc_out gdx file" -; - - -*============================ -* --- Round parameters --- -*============================ -* As a general rule, costs or prices should be rounded to two decimal places -* and all other parameter should be rounded to no more than 3 decimal places -* Some exceptions might exist due to number scaling (e.g., emission rates) -acp_price(st,t)$acp_price(st,t) = round(acp_price(st,t),2) ; -avail_retire_exog_rsc(i,v,r,t)$valcap(i,v,r,t) = round(avail_retire_exog_rsc(i,v,r,t),3) ; -batterymandate(st,t)$batterymandate(st,t) = round(batterymandate(st,t),2) ; -bcr(i)$bcr(i) = round(bcr(i),4) ; -biosupply(usda_region,bioclass,"price") = round(biosupply(usda_region,bioclass,"price"),2) ; -biosupply(usda_region,bioclass,"cap") = round(biosupply(usda_region,bioclass,"cap"),3) ; -cc_storage(i,sdbin)$cc_storage(i,sdbin) = round(cc_storage(i,sdbin),3) ; -cendiv_weights(r,cendiv)$cendiv_weights(r,cendiv) = round(cendiv_weights(r,cendiv), 3) ; -cost_cap(i,t)$cost_cap(i,t) = round(cost_cap(i,t),2) ; -cost_cap_energy(i,t)$cost_cap_energy(i,t) = round(cost_cap_energy(i,t),2) ; -cost_co2_pipeline_fom(r,rr,t) =round(cost_co2_pipeline_fom(r,rr,t),2) ; -cost_co2_pipeline_cap(r,rr,t) =round(cost_co2_pipeline_cap(r,rr,t),2) ; -cost_co2_spurline_fom(r,cs,t) = round(cost_co2_spurline_fom(r,cs,t),2) ; -cost_co2_spurline_cap(r,cs,t) = round(cost_co2_spurline_cap(r,cs,t),2) ; -cost_co2_stor_bec(cs,t) = round(cost_co2_stor_bec(cs,t),2) ; -cost_fom(i,v,r,t)$cost_fom(i,v,r,t) = round(cost_fom(i,v,r,t),2) ; -cost_fom_energy(i,v,r,t)$cost_fom_energy(i,v,r,t) = round(cost_fom_energy(i,v,r,t),2) ; -cost_h2_storage_cap(h2_stor,t) = round(cost_h2_storage_cap(h2_stor,t), 2) ; -cost_h2_transport_cap(r,rr,t)$cost_h2_transport_cap(r,rr,t) = round(cost_h2_transport_cap(r,rr,t),2) ; -cost_h2_transport_fom(r,rr,t)$cost_h2_transport_fom(r,rr,t) = round(cost_h2_transport_fom(r,rr,t),2) ; -cost_opres(i,ortype,t)$cost_opres(i,ortype,t) = round(cost_opres(i,ortype,t),2) ; -cost_prod(i,v,r,t)$cost_prod(i,v,r,t) = round(cost_prod(i,v,r,t), 2) ; -cost_upgrade(i,v,r,t)$cost_upgrade(i,v,r,t) = round(cost_upgrade(i,v,r,t),2) ; -cost_vom(i,v,r,t)$cost_vom(i,v,r,t) = round(cost_vom(i,v,r,t),2) ; -cost_vom_pvb_b(i,v,r,t)$cost_vom_pvb_b(i,v,r,t) = round(cost_vom_pvb_b(i,v,r,t),2) ; -cost_vom_pvb_p(i,v,r,t)$cost_vom_pvb_p(i,v,r,t) = round(cost_vom_pvb_p(i,v,r,t),2) ; -degrade(i,tt,t)$degrade(i,tt,t) = round(degrade(i,tt,t),3) ; -derate_geo_vintage(i,v)$derate_geo_vintage(i,v) = round(derate_geo_vintage(i,v),3) ; -distance(r,rr,trtype)$distance(r,rr,trtype) = round(distance(r,rr,trtype),3) ; -* non-CO2 emission/capture rates get small, here making sure accounting stays correct -emit_rate(etype,e,i,v,r,t)$valgen(i,v,r,t) = round(emit_rate(etype,e,i,v,r,t),10) ; -capture_rate(e,i,v,r,t)$valgen(i,v,r,t) = round(capture_rate(e,i,v,r,t),6) ; -fuel_price(i,r,t)$fuel_price(i,r,t) = round(fuel_price(i,r,t),2) ; -gasmultterm(cendiv,t)$gasmultterm(cendiv,t) = round(gasmultterm(cendiv,t),3) ; -heat_rate(i,v,r,t)$heat_rate(i,v,r,t) = round(heat_rate(i,v,r,t),2) ; -m_capacity_exog(i,v,r,t)$[valcap(i,v,r,t)$(not sameas(i,"smr"))] = round(m_capacity_exog(i,v,r,t),3) ; -m_capacity_exog_energy(i,v,r,t)$[valcap(i,v,r,t)] = round(m_capacity_exog_energy(i,v,r,t),3) ; -m_rsc_dat(r,i,rscbin,"cap")$m_rsc_dat(r,i,rscbin,"cap") = round(m_rsc_dat(r,i,rscbin,"cap"),3) ; -m_rsc_dat(r,i,rscbin,"cost")$m_rsc_dat(r,i,rscbin,"cost") = round(m_rsc_dat(r,i,rscbin,"cost"),2) ; -m_rsc_dat(r,i,rscbin,"cost_trans")$m_rsc_dat(r,i,rscbin,"cost_trans") = round(m_rsc_dat(r,i,rscbin,"cost_trans"),2) ; -prm(r,t)$prm(r,t) = round(prm(r,t),3) ; -prod_conversion_rate(i,v,r,t)$prod_conversion_rate(i,v,r,t) = round(prod_conversion_rate(i,v,r,t),6) ; -ptc_value_scaled(i,v,t)$ptc_value_scaled(i,v,t) = round(ptc_value_scaled(i,v,t),2) ; -recperc(rpscat,st,t)$recperc(rpscat,st,t) = round(recperc(rpscat,st,t),3) ; -rggi_cap(t)$rggi_cap(t) = round(rggi_cap(t),0) ; -state_cap(st,t)$state_cap(st,t) = round(state_cap(st,t),0) ; -storage_eff_pvb_g(i,t)$storage_eff_pvb_g(i,t) = round(storage_eff_pvb_g(i,t),3) ; -storage_eff_pvb_p(i,t)$storage_eff_pvb_p(i,t) = round(storage_eff_pvb_p(i,t),3) ; -tranloss(r,rr,trtype)$tranloss(r,rr,trtype) = round(tranloss(r,rr,trtype),3) ; -tsc_binwidth(r,rr,tscbin)$tsc_binwidth(r,rr,tscbin) = round(tsc_binwidth(r,rr,tscbin),2) ; -tsc_forward(r,rr,tscbin)$tsc_forward(r,rr,tscbin) = round(tsc_forward(r,rr,tscbin),2) ; -tsc_reverse(r,rr,tscbin)$tsc_reverse(r,rr,tscbin) = round(tsc_reverse(r,rr,tscbin),2) ; -transmission_cost_nonac(r,rr,trtype)$transmission_cost_nonac(r,rr,trtype) = round(transmission_cost_nonac(r,rr,trtype),2) ; -transmission_line_fom(r,rr,trtype)$transmission_line_fom(r,rr,trtype) = round(transmission_line_fom(r,rr,trtype),3) ; -trans_cost_cap_fin_mult(t) = round(trans_cost_cap_fin_mult(t),3) ; -trans_cost_cap_fin_mult_noITC(t) = round(trans_cost_cap_fin_mult_noITC(t),3) ; -upgrade_derate(i,v,r,t)$upgrade_derate(i,v,r,t) = round(upgrade_derate(i,v,r,t),3) ; -winter_cap_frac_delta(i,v,r)$winter_cap_frac_delta(i,v,r) = round(winter_cap_frac_delta(i,v,r),3) ; - - -*================================================ -* --- SEQUENTIAL SETUP --- -*================================================ -$ifthen.seq %timetype%=="seq" - -* -- upgrade capacity tracking -- -m_capacity_exog0(i,v,r,t) = m_capacity_exog(i,v,r,t) ; - -* remove cc_int as it is only used in the intertemporal setting -cc_int(i,v,r,ccseason,t) = 0 ; - -*for the sequential solve, what matters is the relative ratio of the pvf for capital and the pvf for onm -*therefore, we set the pvf capital to one, and then pvf_onm to the relative 20 year present value by using the crf -pvf_capital(t) = 1 ; -pvf_onm(t)$tmodel_new(t) = round(1 / crf(t),6) ; - -$endif.seq - - -*================================================ -* --- INTERTEMPORAL AND WINDOW SETUP --- -*================================================ - -$ifthen.intwin ((%timetype%=="int") or (%timetype%=="win")) - -set - loadset "set used for loading in merged gdx files" / ReEDS_Augur_%startyear%*ReEDS_Augur_%endyear% / -; - -parameter - cc_evmc_load2(loadset,i,r,ccseason,t) "--fraction-- cc_evmc loading in from the cc_out gdx file" - cc_iter(i,v,r,ccseason,t,cciter) "--fraction-- Actual capacity value in iteration cciter" - cc_mar_load2(loadset,i,r,ccseason,t) "--fraction-- cc_mar loading in from the cc_out gdx file" - cc_old_load2(loadset,i,r,ccseason,t) "--MW-- cc_old loading in from the cc_out gdx file" - cc_scale(i,r,ccseason,t) "--unitless-- scaling of marginal capacity value levels in intertemporal runs to equal total capacity value" - cc_totmarg(i,r,ccseason,t) "--MW-- original estimate of total capacity value for intertemporal, based on marginals" - sdbin_size_load2(loadset,ccreg,ccseason,sdbin,t) "--MW-- bin_size power loading in from the cc_out gdx file" -; - -cc_scale(i,r,ccseason,t) = 0 ; -cc_totmarg(i,r,ccseason,t) = 0 ; - -$endif.intwin - - -*================================================ -* --- INTERTEMPORAL SETUP --- -*================================================ - -$ifthen.int %timetype%=="int" - -* Iteration tracking -set cciter "placeholder for iteration number for tracking CC" /0*20/ ; -parameter cap_iter(i,v,r,t,cciter) "--MW-- Power apacity by iteration" - cap_energy_iter(i,v,r,t,cciter) "--MWh-- Energy capacity by iteration" - gen_iter(i,v,r,t,cciter) "--MWh-- Annual uncurtailed generation by iteration" - cap_firm_iter(i,v,r,ccseason,t,cciter) "--MW-- VRE Firm capacity by iteration" - cap_energy_firm_iter(i,v,r,ccseason,t,cciter) "--MWh-- VRE Firm energy capacity by iteration" -; -cap_iter(i,v,r,t,cciter) = 0 ; -cap_energy_iter(i,v,r,t,cciter) = 0 ; -gen_iter(i,v,r,t,cciter) = 0 ; - - -*Assign csp3 and csp4 to use the same initial values as csp2_1 -cc_int(i,v,r,ccseason,t)$[csp3(i) or csp4(i)] = cc_int('csp2_1',v,r,ccseason,t) ; - -tmodel(t) = no ; -tmodel(t)$[tmodel_new(t)$(yeart(t)<=%endyear%)] = yes ; - - -*Cap the maximum CC in the first solve iteration -cc_int(i,v,r,ccseason,t)$[rsc_i(i)$(cc_int(i,v,r,ccseason,t)>0.4)$wind(i)] = 0.4 ; -cc_int(i,v,r,ccseason,t)$[rsc_i(i)$(cc_int(i,v,r,ccseason,t)>0.6)$pv(i)] = 0.6 ; - - -*set objective function to millions of dollars -cost_scale = 1 ; - -*marginal capacity value not used in intertemporal case -m_cc_mar(i,r,ccseason,t) = 0 ; -*static capacity value for existing capacity not used in intertemporal case -cc_old(i,r,ccseason,t) = 0 ; - -*sets needed for the demand side -*following sets are needed for linear interpolation of price -*that determine the year before the non-solved year and the year after -set t_before, t_after ; -alias(t,ttt) ; -t_before(t,tt)$[tprev(t,tt)$(ord(tt) = smax{ttt, ord(ttt)$tprev(t,ttt) })] = yes ; -t_after(t,tt)$tprev(tt,t) = yes ; - -* intentionally not declaring all indices to make these flexibile -* rep only used when running the demand side -parameter rep "reporting for all sectors/timeslices/regions" - repannual(t,*,*) "national and annual reporting" -; - -$endif.int - - -*======================= -* --- WINDOW SETUP --- -*======================= - -$ifthen.win %timetype%=="win" - -parameter pvf_capital0(t) "original pvf_capital used for calculating pvf_capital in window solve", - pvf_onm0(t) "original pvf_onm used for calculating pvf_capital in window solve"; - -pvf_capital0(t) = pvf_capital(t) ; -pvf_onm0(t) = pvf_onm(t) ; - -cost_scale = 1e-3 ; - -*Assign csp3 and csp4 to use the same initial values as csp2_1 -cc_int(i,v,r,ccseason,t)$[csp3(i) or csp4(i)] = cc_int('csp2_1',v,r,ccseason,t) ; - -*marginal capacity value not used in intertemporal case -m_cc_mar(i,r,ccseason,t) = 0 ; -*static capacity value for existing capacity not used in intertemporal case -cc_old(i,r,ccseason,t) = 0 ; - -tmodel(t) = no ; - -set windows /1*40/ ; -set blocks /start,stop/ ; - - -table solvewindows(windows,blocks) -$ondelim -$include inputs_case%ds%windows.csv -$offdelim -; - -$endif.win - - -*====================== -* --- Unload all inputs --- -*====================== - -execute_unload 'inputs_case%ds%inputs.gdx' ; diff --git a/d_solvewindow.gms b/d_solvewindow.gms deleted file mode 100644 index 3fa679bf..00000000 --- a/d_solvewindow.gms +++ /dev/null @@ -1,154 +0,0 @@ -* global needed for this file: -* case : name of case you're running -* niter : current iteration - -$setglobal ds \ - -$ifthen.unix %system.filesys% == UNIX -$setglobal ds / -$endif.unix - -$log 'Running window solve for...' -$log ' case == %case%' -$log ' iteration == %niter%' -$log ' window == %window%' - -*remove any load years -tload(t) = no ; - -$if not set niter $setglobal niter 0 -$eval previter %niter%-1 - -tmodel(t) = no ; -*enable years that fall within the window range -tmodel(t)$[tmodel_new(t)$(yeart(t)>=solvewindows("%window%","start")) - $(yeart(t)<=solvewindows("%window%","stop"))] = yes ; - - -*reset tlast to the final modeled period for this window -*then re-compute the financial multiplier for pv -tlast(t) = no ; -tlast(t)$[ord(t)=smax(tt$tmodel(tt),ord(tt))] = yes ; - -pvf_capital(t)$tmodel(t) = pvf_capital0(t) ; -pvf_onm(t) = pvf_onm0(t) ; -pvf_onm(t)$tlast(t) = round(pvf_capital0(t) / crf(t), 6) ; - -*if this isn't the first iteration -$ifthene.notfirstiter %niter%>0 - -*============================ -* --- CC and Curtailment --- -*============================ - -*indicate we're loading data -tload(t)$tmodel(t) = yes ; - -$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_merged_%niter%.gdx -$loaddcr loadset = merged_set_1 -$loaddcr cc_old_load2 = cc_old -$loaddcr cc_mar_load2 = cc_mar -$loaddcr sdbin_size_load2 = sdbin_size -$gdxin - -*collapse the set that came from merging the gdx files - -cc_old_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_old_load2(loadset,i,r,ccreg,szn,t) } } ; -cc_mar_load(i,r,szn,t) = sum{loadset, sum{ccreg$r_ccreg(r,ccreg), cc_mar_load2(loadset,i,r,ccreg,szn,t) } } ; - -sdbin_size_load(ccreg,szn,sdbin,t) = sum{loadset, sdbin_size_load2(loadset,ccreg,szn,sdbin,t) } ; - -*=============================== -* --- Begin Capacity Credit --- -*=============================== - -*Clear params before calculation -cc_int(i,v,r,szn,t) = 0 ; -cc_totmarg(i,r,szn,t) = 0 ; -cc_excess(i,r,szn,t) = 0 ; -cc_scale(i,r,szn,t) = 0 ; -sdbin_size(ccreg,szn,sdbin,t)$tload(t) = 0 ; - -*Storage duration bin sizes by year -sdbin_size(ccreg,szn,sdbin,t)$tload(t) = sdbin_size_load(ccreg,szn,sdbin,t) ; - -*Sw_Int_CC=0 means use average capacity credit for each tech, and don't differentiate vintages -*If there is no existing capacity to calculate average, use marginal capacity credit instead. -if(Sw_Int_CC=0, - cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)$sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) }] = - cc_old_load(i,r,szn,t) / sum{(vv)$(valcap(i,vv,r,t)), CAP.l(i,vv,r,t) } ; - cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)$(cc_old_load(i,r,szn,t)=0)] = m_cc_mar(i,r,szn,t) ; -) ; - -*For the remaining options we initially use marginal values for cc_int, differentiated by vintage based on seasonal capacity factors. -if(Sw_Int_CC=1 or Sw_Int_CC=2, - cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)$sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) }] = - m_cc_mar(i,r,szn,t) * m_cf_szn(i,v,r,szn,t) / sum{vv$ivt(i,vv,t), m_cf_szn(i,vv,r,szn,t) } ; - cc_totmarg(i,r,szn,t)$[tload(t)$(vre(i) or storage(i))] = sum{v$valcap(i,v,r,t), cc_int(i,v,r,szn,t) * CAP.l(i,v,r,t) } ; -) ; - -*Sw_Int_CC=1 means use average capacity credit for each tech, but differentiate based on vintage. -*Start with marginal capacity credit with seasonal vintage-based capacity factor adjustment, -*and scale with cc_old_load to result in the correct total capacity credit. -if(Sw_Int_CC=1, - cc_scale(i,r,szn,t)$[tload(t)$(vre(i) or storage(i))] = 1 ; - cc_scale(i,r,szn,t)$[tload(t)$(vre(i) or storage(i))$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) / cc_totmarg(i,r,szn,t) ; - cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)] = cc_int(i,v,r,szn,t) * cc_scale(i,r,szn,t) ; -) ; - -*Sw_Int_CC=2 means use marginal capacity credit, adjusted by seasonal capacity factors by vintage -if(Sw_Int_CC=2, - cc_excess(i,r,szn,t)$[tload(t)$cc_totmarg(i,r,szn,t)] = cc_old_load(i,r,szn,t) - cc_totmarg(i,r,szn,t) ; -) ; - - -*no longer want m_cc_mar since it should not enter the planning reserve margin constraint -m_cc_mar(i,r,szn,t) = 0 ; - -cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) > 1] = 1 ; -cc_int(i,v,r,szn,t)$[tload(t)$csp_storage(i)$valcap(i,v,r,t)] = 1 ; - -*======================================= -* --- Begin Averaging of CC/Curt --- -*======================================= - -$ifthene.afterseconditer %niter%>1 - -*when set to 1 - it will take the average over all previous iterations -if(Sw_AVG_iter=1, - cc_int(i,v,r,szn,t)$[tload(t)$(vre(i) or storage(i))$valcap(i,v,r,t)] = round((cc_int(i,v,r,szn,t) + cc_iter(i,v,r,szn,t,"%previter%")) / 2 ,4) ; - ) ; - -$endif.afterseconditer - -*Remove very small numbers to make it easier for the solver -cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) < 0.001] = 0 ; - -cc_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) ; - -execute_unload 'ReEDS_Augur%ds%augur_data%ds%curtout_%case%_%niter%.gdx' cc_int ; - -*following line will load in the level values if the switch is enabled -*note that this is still within the conditional that we are now past the first iteration -*and thus a loadpoint is enabled -if(Sw_Loadpoint = 1, -execute_loadpoint 'gdxfiles%ds%%case%_load.gdx' ; -%case%.optfile = 8 ; -) ; - -$endif.notfirstiter - -*============================== -* --- Solve Supply Side --- -*============================== - -solve ReEDSmodel using lp minimizing z ; - -*add years to tfix(t) if this is the last iteration -$ifthene.lastiter %niter%=%maxiter% - -$eval nextwindow %window% + 1 -tfix(t)$(tmodel(t)$(yeart(t)0)$(flow_through(r,h,t)>0)$tmodel_new(t)] = - flow_ba2ba(r,rr,h,t) / flow_through(r,h,t) ; - -Parameter A_downstream(r,rr,allh,t) "downstream power distribution matrix" ; -* see equation (10) in Bialek (1996) -A_downstream(r,rr,h,t)$tmodel_new(t) = 0 ; -A_downstream(r,r,h,t)$tmodel_new(t) = 1 ; -A_downstream(r,rr,h,t)$[(flow_ba2ba(r,rr,h,t)>0)$(flow_through(rr,h,t)>0)$tmodel_new(t)] = -flow_ba2ba(r,rr,h,t) / flow_through(rr,h,t) ; - -* --- calculate the inverse of the upstream and downstream power matricies --- - -parameter - Ainv_upstream(r,rr,allh,t) "inverse of A_upstream" - Ainv_downstream(r,rr,allh,t) "inverse of A_downstream" - a(r,rr) temp matrix for A - ainv(r,rr) temp matrix for A-inverse -; - -Ainv_upstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; -Ainv_downstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; -a(r,rr) = 0 ; -ainv(r,rr) = 0 ; - -Loop((h,t)$[tmodel_new(t)$Sw_calc_powfrac], -a(rr,r) = A_upstream(rr,r,h,t) ; -execute_unload 'outputs%ds%gdxforinverse_%case%.gdx' r, a ; -execute 'invert outputs%ds%gdxforinverse_%case%.gdx r a outputs%ds%gdxfrominverse_%case%.gdx ainv >> outputs%ds%invert1_%case%.log' ; -execute_load 'outputs%ds%gdxfrominverse_%case%.gdx', ainv ; -Ainv_upstream(rr,r,h,t) = ainv(rr,r) ; -) ; - -Loop((h,t)$[tmodel_new(t)$Sw_calc_powfrac], -a(r,rr) = A_downstream(r,rr,h,t) ; -execute_unload 'outputs%ds%gdxforinverse_%case%.gdx' r, a ; -execute 'invert outputs%ds%gdxforinverse_%case%.gdx r a outputs%ds%gdxfrominverse_%case%.gdx ainv >> outputs%ds%invert2_%case%.log' ; -execute_load 'outputs%ds%gdxfrominverse_%case%.gdx', ainv ; -Ainv_downstream(r,rr,h,t) = ainv(r,rr) ; -) ; - -* --- remove gdx files that were created to do the inverse calculation --- -execute 'rm outputs%ds%gdxforinverse_%case%.gdx' ; -execute 'rm outputs%ds%gdxfrominverse_%case%.gdx' ; -execute 'rm outputs%ds%invert1_%case%.log' ; -execute 'rm outputs%ds%invert2_%case%.log' ; - -* --- calculate upsteram and downstream power fractions --- - -parameter - powerfrac_upstream(rr,r,allh,t) "--unitless-- power fraction upstream : fraction of power at BA rr that was generated at BA r during time-slice h" - powerfrac_downstream(r,rr,allh,t) "--unitless-- power fraction downstream: fraction of power generated at BA r that serves load at BA rr during time-slice h" -; - -powerfrac_upstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; -powerfrac_downstream(r,rr,h,t)$sum{trtype,routes(rr,r,trtype,t) } = 0 ; - -* see equation (6) in Bialek (1996) -if(Sw_calc_powfrac > 0, -powerfrac_upstream(rr,r,h,t)$[(flow_through(rr,h,t)>0)$tmodel_new(t)] = 1 / flow_through(rr,h,t) * Ainv_upstream(rr,r,h,t) * totgen(r,h,t) ; -powerfrac_upstream(rr,r,h,t)$[(powerfrac_upstream(rr,r,h,t)<1e-3)$tmodel_new(t)] = 0 ; - -* see equation (12) in Bialek (1996) -powerfrac_downstream(r,rr,h,t)$[(flow_through(r,h,t)>0)$tmodel_new(t)] = 1 / flow_through(r,h,t) * Ainv_downstream(r,rr,h,t) * totload(rr,h,t) ; -powerfrac_downstream(r,rr,h,t)$[(powerfrac_downstream(r,rr,h,t)<1e-3)$tmodel_new(t)] = 0 ; -) ; - -* --- write the outputs --- -execute_unload "outputs%ds%rep_powerfrac_%fname%.gdx" powerfrac_downstream, powerfrac_upstream ; diff --git a/e_report.gms b/e_report.gms deleted file mode 100644 index ebd14d10..00000000 --- a/e_report.gms +++ /dev/null @@ -1,2091 +0,0 @@ -$setglobal ds \ - -$ifthen.unix %system.filesys% == UNIX -$setglobal ds / -$endif.unix - -$if not set case $setglobal case ref - - -sets -sys_costs / - inv_co2_network_pipe - inv_co2_network_spur - inv_converter_costs - inv_dac - inv_h2_pipeline - inv_h2_production - inv_h2_storage - inv_investment_capacity_costs - inv_investment_refurbishment_capacity - inv_investment_spurline_costs_rsc_technologies - inv_investment_water_access - inv_itc_payments_negative - inv_itc_payments_negative_refurbishments - inv_spurline_investment - inv_transmission_interzone_ac_investment - inv_transmission_interzone_dc_investment - inv_transmission_intrazone_investment - op_acp_compliance_costs - op_co2_incentive_negative - op_co2_network_fom_pipe - op_co2_network_fom_spur - op_co2_storage - op_co2_transport_storage - op_consume_fom - op_consume_vom - op_emissions_taxes - op_fom_costs - op_fuelcosts_objfn - op_h2combustion_fuel_costs - op_h2_fuel_costs - op_h2_revenue_exog - op_h2_transport - op_h2_transport_intrareg - op_h2_storage - op_h2_vom - op_h2_ptc_payments_negative - op_operating_reserve_costs - op_ptc_payments_negative - op_rect_fuel_costs - op_spurline_fom - op_startcost - op_transmission_fom - op_transmission_intrazone_fom - op_vom_costs -/, - -sys_costs_inv(sys_costs) / - inv_co2_network_pipe - inv_co2_network_spur - inv_converter_costs - inv_dac - inv_h2_pipeline - inv_h2_production - inv_h2_storage - inv_investment_capacity_costs - inv_investment_refurbishment_capacity - inv_investment_spurline_costs_rsc_technologies - inv_investment_water_access - inv_itc_payments_negative - inv_itc_payments_negative_refurbishments - inv_spurline_investment - inv_transmission_interzone_ac_investment - inv_transmission_interzone_dc_investment - inv_transmission_intrazone_investment -/, - -sys_costs_op(sys_costs) / - op_acp_compliance_costs - op_co2_incentive_negative - op_co2_network_fom_pipe - op_co2_network_fom_spur - op_co2_storage - op_co2_transport_storage - op_consume_fom - op_consume_vom - op_emissions_taxes - op_fom_costs - op_fuelcosts_objfn - op_h2combustion_fuel_costs - op_h2_fuel_costs - op_h2_revenue_exog - op_h2_transport - op_h2_transport_intrareg - op_h2_storage - op_h2_ptc_payments_negative - op_operating_reserve_costs - op_ptc_payments_negative - op_spurline_fom - op_startcost - op_transmission_fom - op_transmission_intrazone_fom - op_vom_costs -/, - -rev_cat "categories for renvenue streams" /load, res_marg, oper_res, rps, charge /, - -lcoe_cat "categories for LCOE calculation" /capcost, upgradecost, rsccost, fomcost, vomcost, gen / - -loadtype "categories for types of load" / end_use, dist_loss, trans_loss, stor_charge, h2_prod, h2_network, dac / - -h2_demand_type / "electricity", "cross-sector"/ - -; - -* Parameter definitions in the following file are read from e_report_params.csv -* and parsed in copy_files.py. -* All output parameters should be defined in e_report_params.csv. -$include e_report_params.gms - -* Restrict operational outputs to representative timeslices and seasons -h(h)$[not h_rep(h)] = no ; -szn(szn)$[not szn_rep(szn)] = no ; - -*================================================= -* -- CAPACITY ABOVE INTERCONNECTION QUEUE LIMIT -- -*================================================= - -cap_above_limit(tg,r,t)$tmodel_new(t) = CAP_ABOVE_LIM.l(tg,r,t) ; - -*===================== -* -- CO2 Reporting -- -*===================== - -CO2_CAPTURED_out(r,h,t)$tmodel_new(t) = CO2_CAPTURED.l(r,h,t) ; -CO2_CAPTURED_out_ann(r,t)$tmodel_new(t) = sum(h,hours(h) * CO2_CAPTURED.l(r,h,t) ); -CO2_STORED_out(r,cs,h,t)$[tmodel_new(t)$csfeas(cs)] = CO2_STORED.l(r,cs,h,t) ; -CO2_STORED_out_ann(r,cs,t)$[tmodel_new(t)$csfeas(cs)] = sum(h,hours(h) * CO2_STORED.l(r,cs,h,t) ); -CO2_TRANSPORT_INV_out(r,rr,t)$tmodel_new(t) = CO2_TRANSPORT_INV.l(r,rr,t) ; -CO2_SPURLINE_INV_out(r,cs,t)$[tmodel_new(t)$csfeas(cs)] = CO2_SPURLINE_INV.l(r,cs,t) ; - -CO2_FLOW_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) + CO2_FLOW.l(rr,r,h,t) ; -CO2_FLOW_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * (CO2_FLOW.l(r,rr,h,t) + CO2_FLOW.l(rr,r,h,t)) } ; - -CO2_FLOW_pos_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) ; -CO2_FLOW_pos_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * CO2_FLOW.l(r,rr,h,t) } ; - -CO2_FLOW_neg_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = -1 * CO2_FLOW.l(rr,r,h,t) ; -CO2_FLOW_neg_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = -1 * sum{h, hours(h) * CO2_FLOW.l(rr,r,h,t) } ; - -CO2_FLOW_net_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) - CO2_FLOW.l(rr,r,h,t) ; -CO2_FLOW_net_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * (CO2_FLOW.l(r,rr,h,t) - CO2_FLOW.l(rr,r,h,t)) } ; - -*========================= -* LCOE -*========================= - -avg_avail(i,v,r) = sum{h, hours(h) * avail(i,r,h) * derate_geo_vintage(i,v) } / 8760 ; -avg_cf(i,v,r,t)$[CAP.l(i,v,r,t)$(not rsc_i(i))] = - sum{h, GEN.l(i,v,r,h,t) * hours(h) } - / sum{h, - CAP.l(i,v,r,t) - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - * hours(h) } -; - -*non-rsc technologies do not face the grid supply curve -*and thus can be assigned to an individual bin - -*LCOE calculation is appropriate for sequential solve mode only where annual energy production is the same in every year. -*In inter-temporal modes this isn't the case and energy production should be discounted appropriately. - -lcoe(i,v,r,t,"bin1")$[(not rsc_i(i))$valcap_init(i,v,r,t)$ivt(i,v,t)$avg_avail(i,v,r)] = -* cost of capacity divided by generation - ((crf(t) * cost_cap_fin_mult(i,r,t) * cost_cap(i,t)$newv(v) - + cost_fom(i,v,r,t) - ) / (avg_avail(i,v,r) * 8760)) -*plus VOM costs - + cost_vom(i,v,r,t) -* plus fuel costs - assuming constant fuel prices here (model prices might be different) - + heat_rate(i,v,r,t) * fuel_price(i,r,t) -; - -gen_rsc(i,v,r,t)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)] = - sum{h, m_cf(i,v,r,h,t) * hours(h) } ; - -lcoe(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$gen_rsc(i,v,r,t)] = -* cost of capacity divided by generation - (crf(t) - * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) -* Spur-line costs embedded in supply curve for techs without explicitly-modeled spurlines - + m_rsc_dat(r,i,rscbin,"cost")$[newv(v)$(not spur_techs(i))] -* Spur-line costs assuming 1:1 ratio between gen cap and spur cap (i.e. no overbuilding) - + sum{x$[xfeas(x)$x_r(x,r)$spur_techs(i)], spurline_cost(x) * Sw_SpurCostMult} - ) - + cost_fom(i,v,r,t) - ) / gen_rsc(i,v,r,t) -*plus VOM costs - + cost_vom(i,v,r,t) -; - -lcoe_cf_act(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$rsc_i(i)] = lcoe(i,v,r,t,rscbin) ; -lcoe_cf_act(i,v,r,t,"bin1")$[(not rsc_i(i))$valcap_init(i,v,r,t)$ivt(i,v,t)$avg_cf(i,v,r,t)] = -* cost of capacity divided by generation - ((crf(t) * cost_cap_fin_mult(i,r,t) * cost_cap(i,t)$newv(v) - + cost_fom(i,v,r,t) - ) / (avg_cf(i,v,r,t) * 8760) - ) -*plus VOM costs - + cost_vom(i,v,r,t) -*plus fuel costs - assuming constant fuel prices here (model prices might be different) - + heat_rate(i,v,r,t) * fuel_price(i,r,t) -; - -lcoe_nopol(i,v,r,t,rscbin)$valcap_init(i,v,r,t) = lcoe(i,v,r,t,rscbin) ; -lcoe_nopol(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$gen_rsc(i,v,r,t)] = -* cost of capacity divided by generation - (crf(t) - * (cost_cap_fin_mult_noITC(i,r,t) * cost_cap(i,t) -* Spur-line costs embedded in supply curve for techs without explicitly-modeled spurlines - + m_rsc_dat(r,i,rscbin,"cost")$newv(v)$(not spur_techs(i))) -* Spur-line costs assuming 1:1 ratio between gen cap and spur cap (i.e. no overbuilding) - + sum{x$[xfeas(x)$x_r(x,r)$spur_techs(i)], spurline_cost(x) * Sw_SpurCostMult} - + cost_fom(i,v,r,t) - ) / gen_rsc(i,v,r,t) -*plus VOM costs - + cost_vom(i,v,r,t) -; - -lcoe_fullpol(i,v,r,t,rscbin)$valcap_init(i,v,r,t) = lcoe(i,v,r,t,rscbin) ; -lcoe_fullpol(i,v,r,t,rscbin)$[valcap_init(i,v,r,t)$ivt(i,v,t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$gen_rsc(i,v,r,t)] = -* cost of capacity divided by generation - (crf(t) - * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) -* Spur-line costs embedded in supply curve for techs without explicitly-modeled spurlines - + m_rsc_dat(r,i,rscbin,"cost")$newv(v)$(not spur_techs(i))) -* Spur-line costs assuming 1:1 ratio between gen cap and spur cap (i.e. no overbuilding) - + sum{x$[xfeas(x)$x_r(x,r)$spur_techs(i)], spurline_cost(x) * Sw_SpurCostMult} - + cost_fom(i,v,r,t)) - / gen_rsc(i,v,r,t) -*plus VOM costs - + cost_vom(i,v,r,t) -; - -lcoe_built(i,r,t)$[ [sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], GEN.l(i,v,r,h,t) * hours(h) }] or - [sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], GEN.l(i,v,r,h,t) * hours(h) }] ] = - (crf(t) * ( - sum{v$valinv(i,v,r,t), - INV.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) ) } - + sum{v$[valinv(i,v,r,t)$battery(i)], - INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap_energy(i,t) ) } - + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], - UPGRADES.l(i,v,r,t) * (cost_upgrade(i,v,r,t) * cost_cap_fin_mult(i,r,t) ) } - + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)], - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) } - ) - + sum{v$valinv(i,v,r,t), cost_fom(i,v,r,t) * INV.l(i,v,r,t) } - + sum{v$[valinv(i,v,r,t)$battery(i)], cost_fom_energy(i,v,r,t) * INV_ENERGY.l(i,v,r,t) } - + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], cost_fom(i,v,r,t) * UPGRADES.l(i,v,r,t) } - + sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], (cost_vom(i,v,r,t)+ heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h) } - + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], (cost_vom(i,v,r,t)+ heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h) } - ) / (sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], GEN.l(i,v,r,h,t) * hours(h) } - + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], GEN.l(i,v,r,h,t) * hours(h) }) -; - -lcoe_built_nat(i,t)$[sum{(v,r)$valinv(i,v,r,t), INV.l(i,v,r,t) }] = - sum{r, lcoe_built(i,r,t) * sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) } } - / sum{(v,r)$valinv(i,v,r,t), INV.l(i,v,r,t) } ; - -lcoe_pieces("capcost",i,r,t)$tmodel_new(t) = - sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap(i,t) ) } - + sum{v$[valinv(i,v,r,t)$battery(i)], INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult(i,r,t) * cost_cap_energy(i,t) ) } ; - -lcoe_pieces("upgradecost",i,r,t)$tmodel_new(t) = - sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], - cost_upgrade(i,v,r,t) * cost_cap_fin_mult(i,r,t) * UPGRADES.l(i,v,r,t) } - + sum{(v,rscbin)$allow_cap_up(i,v,r,rscbin,t), - cost_cap_fin_mult(i,r,t) * INV_CAP_UP.l(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } - + sum{(v,rscbin)$allow_ener_up(i,v,r,rscbin,t), - cost_cap_fin_mult(i,r,t) * INV_ENER_UP.l(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } ; - -lcoe_pieces("rsccost",i,r,t)$tmodel_new(t) = - sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)], - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) } ; - -lcoe_pieces("fomcost",i,r,t)$tmodel_new(t) = - sum{v$valinv(i,v,r,t), cost_fom(i,v,r,t) * INV.l(i,v,r,t) } - + sum{v$[valinv(i,v,r,t)$battery(i)], cost_fom_energy(i,v,r,t) * INV_ENERGY.l(i,v,r,t) } - + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], cost_fom(i,v,r,t) * UPGRADES.l(i,v,r,t) } ; - -lcoe_pieces("vomcost",i,r,t)$tmodel_new(t) = - sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], - (cost_vom(i,v,r,t) + heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h) } - + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], - (cost_vom(i,v,r,t) + heat_rate(i,v,r,t) * fuel_price(i,r,t)) * GEN.l(i,v,r,h,t) * hours(h)} ; - -lcoe_pieces("gen",i,r,t)$tmodel_new(t) = - sum{(v,h)$[valinv(i,v,r,t)$INV.l(i,v,r,t)], GEN.l(i,v,r,h,t) * hours(h) } - + sum{(v,h)$[UPGRADES.l(i,v,r,t)$Sw_Upgrades], GEN.l(i,v,r,h,t) * hours(h) } ; - -lcoe_pieces_nat(lcoe_cat,i,t)$tmodel_new(t) = sum{r, lcoe_pieces(lcoe_cat,i,r,t) } ; - -*======================================== -* REQUIREMENT PRICES AND QUANTITIES -*======================================== - -objfn_raw = z.l ; - -load_frac_rt(r,t)$sum{(rr,h), LOAD.l(rr,h,t) } = sum{h, hours(h) * LOAD.l(r,h,t) }/ sum{(rr,h), hours(h) * LOAD.l(rr,h,t) } ; - -*Load and operating reserve prices are $/MWh, and reserve margin price is $/MW/rep-day for -* capacity credit formulation and $/MW/stress-timeslice for stress period formulation. -reqt_price('load','na',r,h,t)$tmodel_new(t) = - (1 / cost_scale) * (1 / pvf_onm(t)) * eq_supply_demand_balance.m(r,h,t) / hours(h) ; - -reqt_price('oper_res',ortype,r,h,t)$tmodel_new(t) = - (1 / cost_scale) * (1 / pvf_onm(t)) * eq_OpRes_requirement.m(ortype,r,h,t) / hours(h) ; - -reqt_price('state_rps',RPSCat,r,'ann',t)$tmodel_new(t) = - (1 / cost_scale) * (1 / pvf_onm(t)) * sum{st$r_st(r,st), eq_REC_Requirement.m(RPSCat,st,t) } ; - -reqt_price('nat_gen','na',r,'ann',t)$tmodel_new(t) = - (1 / cost_scale) * (1 / pvf_onm(t)) * eq_national_gen.m(t) ; - -reqt_price('annual_cap',e,r,'ann',t)$tmodel_new(t) = - (1 / cost_scale) * (1 / pvf_onm(t)) * eq_annual_cap.m(e,t) ; - -* Capacity credit formulation ($/MW/rep-day) -reqt_price('res_marg','na',r,ccseason,t)$[Sw_PRM_CapCredit$tmodel_new(t)] = - eq_reserve_margin.m(r,ccseason,t) * (1 / cost_scale) * (1 / pvf_onm(t)) ; -* Stress period formulation ($/MW/stress-timeslice) -reqt_price('res_marg','na',r,allh,t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)$h_stress_t(allh,t)] = - eq_supply_demand_balance.m(r,allh,t) * (1 / cost_scale) * (1 / pvf_onm(t)) ; - -reqt_price('res_marg_ann','na',r,'ann',t)$tmodel_new(t) = -* Capacity credit formulation ($/MW-yr) - sum{ccseason, reqt_price('res_marg','na',r,ccseason,t) }$Sw_PRM_CapCredit -* Stress period formulation ($/MW-yr) - + sum{allh$h_stress_t(allh,t), reqt_price('res_marg','na',r,allh,t) }$(Sw_PRM_CapCredit=0) -; -*The marginal on the total load constraint, eq_loadcon is converted to $/MW-yr. -*We can't convert to $/MWh because stress periods have no hours. -reqt_price('eq_loadcon','na',r,allh,t)$[tmodel_new(t)$h_t(allh,t)] = - (1 / cost_scale) * (1 / pvf_onm(t)) * eq_loadcon.m(r,allh,t) ; - - -*Load and operating reserve quantities are MWh, and reserve margin quantity is MW -* Demand from production activities (H2 and DAC) doesn't count toward electricity demand -reqt_quant('load','na',r,h,t)$tmodel_new(t) = - hours(h) * ( - LOAD.l(r,h,t) - - sum{(p,i,v)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)$Sw_Prod], - PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) } - ) ; - -* Capacity credit formulation -reqt_quant('res_marg','na',r,ccseason,t)$[Sw_PRM_CapCredit$tmodel_new(t)] = - (peakdem_static_ccseason(r,ccseason,t) -* + PEAK_FLEX.l(r,ccseason,t) - ) * (1 + prm(r,t)) ; -* Stress period formulation -reqt_quant('res_marg','na',r,allh,t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)$h_stress_t(allh,t)] = - LOAD.l(r,allh,t) ; - -* Annual res_marg quantity is defined as the max requirement level in the year. -reqt_quant('res_marg_ann','na',r,'ann',t)$tmodel_new(t) = -* Capacity credit formulation - smax{ccseason, reqt_quant('res_marg','na',r,ccseason,t) }$Sw_PRM_CapCredit -* Stress period formulation - + smax{allh$h_stress_t(allh,t), reqt_quant('res_marg','na',r,allh,t) }$(Sw_PRM_CapCredit=0) -; - -reqt_quant('oper_res',ortype,r,h,t)$tmodel_new(t) = - hours(h) * ( - orperc(ortype,"or_load") * LOAD.l(r,h,t) - + orperc(ortype,"or_wind") * sum{(i,v)$[wind(i)$valgen(i,v,r,t)], - GEN.l(i,v,r,h,t) } - + orperc(ortype,"or_pv") * sum{(i,v)$[pv(i)$valcap(i,v,r,t)], - CAP.l(i,v,r,t) }$dayhours(h) - ) ; -reqt_quant('state_rps',RPSCat,r,'ann',t)$tmodel_new(t) = - sum{(st,h)$r_st_rps(r,st), RecPerc(RPSCat,st,t) * hours(h) *( - ( (LOAD.l(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - - sum{v$valgen("distpv",v,r,t), GEN.l("distpv",v,r,h,t) }) * (1.0 - distloss) - )$(RecStyle(st,RPSCat)=0) - - + ( LOAD.l(r,h,t) - can_exports_h(r,h,t)$[Sw_Canada=1] - - sum{v$valgen("distpv",v,r,t), GEN.l("distpv",v,r,h,t) } - )$(RecStyle(st,RPSCat)=1) - - + ( sum{(i,v)$[valgen(i,v,r,t)$(not storage_standalone(i))], GEN.l(i,v,r,h,t) - - (distloss * GEN.l(i,v,r,h,t))$(distpv(i)) - - (STORAGE_IN_GRID.l(i,v,r,h,t) * storage_eff_pvb_g(i,t))$[storage_hybrid(i)$(not csp(i))$Sw_HybridPlant] } - - can_exports_h(r,h,t)$[(Sw_Canada=1)$sameas(RPSCat,"CES")] - )$(RecStyle(st,RPSCat)=2) - )} ; - -reqt_quant('nat_gen','na',r,'ann',t)$tmodel_new(t) = - national_gen_frac(t) * ( -* if Sw_GenMandate = 1, then apply the fraction to the bus bar load - ( - sum{h, LOAD.l(r,h,t) * hours(h) } - + sum{(rr,h,trtype)$routes(rr,r,trtype,t), (tranloss(rr,r,trtype) * FLOW.l(rr,r,h,t,trtype) * hours(h)) } - )$[Sw_GenMandate = 1] - -* if Sw_GenMandate = 2, then apply the fraction to the end use load - + (sum{h, - hours(h) * - ( (LOAD.l(r,h,t) - can_exports_h(r,h,t)) * (1.0 - distloss) - sum{v$valgen("distpv",v,r,t), GEN.l("distpv",v,r,h,t) }) - })$[Sw_GenMandate = 2] - ) ; -reqt_quant('annual_cap',e,r,'ann',t)$tmodel_new(t) = emit_cap(e,t) * load_frac_rt(r,t) ; - -*We keep quantity of eq_loadcon in MW -reqt_quant('eq_loadcon','na',r,allh,t)$[tmodel_new(t)$h_t(allh,t)] = LOAD.l(r,allh,t) ; - -*System-wide quantities: -reqt_quant_sys('load','na',h,t)$tmodel_new(t) = sum{r, reqt_quant('load','na',r,h,t)} ; -reqt_quant_sys('oper_res',ortype,h,t)$tmodel_new(t) = sum{r, reqt_quant('oper_res',ortype,r,h,t)} ; -reqt_quant_sys('state_rps',RPSCat,'ann',t)$tmodel_new(t) = sum{r, reqt_quant('state_rps',RPSCat,r,'ann',t)} ; -reqt_quant_sys('nat_gen','na','ann',t)$tmodel_new(t) = sum{r, reqt_quant('nat_gen','na',r,'ann',t)} ; -reqt_quant_sys('annual_cap',e,'ann',t)$tmodel_new(t) = sum{r, reqt_quant('annual_cap',e,r,'ann',t)} ; -reqt_quant_sys('res_marg','na',ccseason,t)$[Sw_PRM_CapCredit$tmodel_new(t)] = - sum{r, reqt_quant('res_marg','na',r,ccseason,t)} ; -reqt_quant_sys('res_marg','na',allh,t)$[(Sw_PRM_CapCredit=0)$h_stress_t(allh,t)$tmodel_new(t)] = - sum{r, reqt_quant('res_marg','na',r,allh,t)} ; -reqt_quant_sys('res_marg_ann','na','ann',t)$tmodel_new(t) = sum{r, reqt_quant('res_marg_ann','na',r,'ann',t)} ; - -*System-wide average prices: -reqt_price_sys('load','na',h,t)$reqt_quant_sys('load','na',h,t) = - sum{r, reqt_price('load','na',r,h,t) * reqt_quant('load','na',r,h,t)}/ - reqt_quant_sys('load','na',h,t) ; - -reqt_price_sys('oper_res',ortype,h,t)$reqt_quant_sys('oper_res',ortype,h,t) = - sum{r, reqt_price('oper_res',ortype,r,h,t) * reqt_quant('oper_res',ortype,r,h,t)}/ - reqt_quant_sys('oper_res',ortype,h,t) ; - -reqt_price_sys('state_rps',RPSCat,'ann',t)$reqt_quant_sys('state_rps',RPSCat,'ann',t) = - sum{r, reqt_price('state_rps',RPSCat,r,'ann',t) * reqt_quant('state_rps',RPSCat,r,'ann',t)}/ - reqt_quant_sys('state_rps',RPSCat,'ann',t) ; - -reqt_price_sys('nat_gen','na','ann',t)$reqt_quant_sys('nat_gen','na','ann',t) = - sum{r, reqt_price('nat_gen','na',r,'ann',t) * reqt_quant('nat_gen','na',r,'ann',t)}/ - reqt_quant_sys('nat_gen','na','ann',t) ; - -reqt_price_sys('annual_cap',e,'ann',t)$reqt_quant_sys('annual_cap',e,'ann',t) = - sum{r, reqt_price('annual_cap',e,r,'ann',t) * reqt_quant('annual_cap',e,r,'ann',t)}/ - reqt_quant_sys('annual_cap',e,'ann',t) ; - -reqt_price_sys('res_marg','na',ccseason,t)$[Sw_PRM_CapCredit$reqt_quant_sys('res_marg','na',ccseason,t)] = - sum{r, reqt_price('res_marg','na',r,ccseason,t) * reqt_quant('res_marg','na',r,ccseason,t)}/ - reqt_quant_sys('res_marg','na',ccseason,t) ; -reqt_price_sys('res_marg','na',allh,t)$[(Sw_PRM_CapCredit=0)$h_stress_t(allh,t)$reqt_quant_sys('res_marg','na',allh,t)] = - sum{r, reqt_price('res_marg','na',r,allh,t) * reqt_quant('res_marg','na',r,allh,t)}/ - reqt_quant_sys('res_marg','na',allh,t) ; - -reqt_price_sys('res_marg_ann','na','ann',t)$reqt_quant_sys('res_marg_ann','na','ann',t) = - sum{r, reqt_price('res_marg_ann','na',r,'ann',t) * reqt_quant('res_marg_ann','na',r,'ann',t)}/ - reqt_quant_sys('res_marg_ann','na','ann',t) ; - -load_rt(r,t)$tmodel_new(t) = sum{h, hours(h) * load_exog(r,h,t) } ; - -load_stress(r,allh,t)$[tmodel_new(t)$h_stress_t(allh,t)] = LOAD.l(r,allh,t) ; - -co2_price(t)$tmodel_new(t) = (1 / cost_scale) * (1 / pvf_onm(t)) * eq_annual_cap.m("CO2",t) ; - -rggi_price(t)$tmodel_new(t) = (1 / cost_scale) * (1 / pvf_onm(t)) * eq_RGGI_cap.m(t) ; -rggi_quant(t)$tmodel_new(t) = RGGI_cap(t) ; - -state_cap_and_trade_price(st,t)$tmodel_new(t) = - (1 / cost_scale) * (1 / pvf_onm(t)) - * eq_state_cap.m(st,t) ; - -state_cap_and_trade_quant(st,t)$tmodel_new(t) = - state_cap(st,t) ; - -tran_hurdle_cost_ann(r,rr,trtype,t)$[tmodel_new(t)$routes(r,rr,trtype,t)$cost_hurdle(r,rr,t)] = - sum{h, hours(h) * cost_hurdle(r,rr,t) * FLOW.l(r,rr,h,t,trtype) } ; - -*======================================== -* RPS, CES, AND TAX CREDIT OUTPUTS -*======================================== - -rec_outputs(RPSCat,i,st,ast,t)$[stfeas(st)$(stfeas(ast) or sameas(ast,"voluntary"))$tmodel_new(t)] = RECS.l(RPSCat,i,st,ast,t) ; -acp_purchases_out(rpscat,st,t) = ACP_PURCHASES.l(RPSCat,st,t) ; -ptc_out(i,v,t)$[tmodel_new(t)$ptc_value_scaled(i,v,t)] = ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t) ; - -*======================================== -* FUEL PRICES AND QUANTITIES -*======================================== - -* The marginal biomass fuel price is derived from the linear program constraint marginals -* Case 1: the resource of a biomass class is NOT exhausted, i.e., BIOUSED.l(bioclass) < biosupply(bioclass) -* Marginal Biomass Price = eq_bioused.m -* Case 2: the resource of one or more biomass classes ARE exhausted, i.e., BIOUSED.l(bioclass) = biosupply(bioclass) -* Marginal Biomass Price = maximum difference between eq_bioused.m and eq_biousedlimit.m(bioclass) across all biomass classes in a region - -repbioprice(r,t)$tmodel_new(t) = max{0, smax{bioclass$BIOUSED.l(bioclass,r,t), eq_bioused.m(r,t) - - sum{usda_region$r_usda(r,usda_region), eq_biousedlimit.m(bioclass,usda_region,t) } } } / pvf_onm(t) ; - -* quantity of biomass used (convert from mmBTU to dry tons using biomass energy content) -bioused_out(bioclass,r,t)$tmodel_new(t) = BIOUSED.l(bioclass,r,t) / bio_energy_content ; -bioused_usda(bioclass,usda_region,t)$tmodel_new(t) = sum{r$r_usda(r,usda_region), bioused_out(bioclass,r,t) } ; - -* 1e9 converts from MMBtu to Quads -repgasquant(cendiv,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 3)$tmodel_new(t)] = - sum{(gb,h), GASUSED.l(cendiv,gb,h,t) * hours(h) } * gas_scale/ 1e9 ; - -repgasquant(cendiv,t)$[(Sw_GasCurve = 1 or Sw_GasCurve = 2)$tmodel_new(t)] = - ( sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], - hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t)} - + sum{(v,r,h)$[valcap("dac_gas",v,r,t)$r_cendiv(r,cendiv)], - hours(h) * dac_gas_cons_rate("dac_gas",v,t) * PRODUCE.l("DAC","dac_gas",v,r,h,t) }$Sw_DAC_Gas - + sum{(p,i,v,r,h)$[r_cendiv(r,cendiv)$valcap(i,v,r,t)$smr(i)], - hours(h) * smr_methane_rate * PRODUCE.l(p,i,v,r,h,t) }$Sw_H2 - ) / 1e9 ; - -repgasquant_irt(i,r,t)$tmodel_new(t) = - ( sum{(v,h)$[valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], - hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } - + sum{(v,h)$[valcap("dac_gas",v,r,t)], - hours(h) * dac_gas_cons_rate("dac_gas",v,t) * PRODUCE.l("DAC","dac_gas",v,r,h,t) }$Sw_DAC_Gas - + sum{(p,v,h)$[valcap(i,v,r,t)$smr(i)], - hours(h) * smr_methane_rate * PRODUCE.l(p,i,v,r,h,t) }$Sw_H2 - ) / 1e9 ; - -repgasquant_nat(t)$tmodel_new(t) = sum{cendiv, repgasquant(cendiv,t) } ; - -*for reported gasprice (not that used to compute system costs) -*scale back to $ / mmbtu -repgasprice(cendiv,t)$[(Sw_GasCurve = 0)$tmodel_new(t)$repgasquant(cendiv,t)] = - smax{gb$[sum{h, GASUSED.l(cendiv,gb,h,t) }], gasprice(cendiv,gb,t) } / gas_scale ; - -repgasprice(cendiv,t)$[(Sw_GasCurve = 2)$tmodel_new(t)$repgasquant(cendiv,t)] = - sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t)], - hours(h)*heat_rate(i,v,r,t)*fuel_price(i,r,t)*GEN.l(i,v,r,h,t) - } / (repgasquant(cendiv,t) * 1e9) ; - -repgasprice_r(r,t)$[(Sw_GasCurve = 0 or Sw_GasCurve = 2)$tmodel_new(t)] = sum{cendiv$r_cendiv(r,cendiv), repgasprice(cendiv,t) } ; - -repgasprice_r(r,t)$[(Sw_GasCurve = 1)$tmodel_new(t)] = - ( sum{(h,cendiv), - gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * - hours(h) } / sum{h, hours(h) } - - + smax((fuelbin,cendiv)$[VGASBINQ_REGIONAL.l(fuelbin,cendiv,t)$r_cendiv(r,cendiv)], gasbinp_regional(fuelbin,cendiv,t) ) - - + smax(fuelbin$VGASBINQ_NATIONAL.l(fuelbin,t), gasbinp_national(fuelbin,t) ) - ) ; - -repgasprice(cendiv,t)$[(Sw_GasCurve = 1)$tmodel_new(t)$repgasquant(cendiv,t)] = - sum{(i,r)$r_cendiv(r,cendiv), repgasprice_r(r,t) * repgasquant_irt(i,r,t) } / repgasquant(cendiv,t) ; - -repgasprice_nat(t)$[tmodel_new(t)$sum{cendiv, repgasquant(cendiv,t) }] = - sum{cendiv, repgasprice(cendiv,t) * repgasquant(cendiv,t) } - / sum{cendiv, repgasquant(cendiv,t) } ; - -*======================================== -* NATURAL GAS FUEL COSTS -*======================================== - -gasshare_ba(r,cendiv,t)$[r_cendiv(r,cendiv)$tmodel_new(t)$repgasquant(cendiv,t)] = - sum{i$[valgen_irt(i,r,t)$gas(i)],repgasquant_irt(i,r,t) / repgasquant(cendiv,t) } ; - -gasshare_techba(i,r,cendiv,t)$[r_cendiv(r,cendiv)$tmodel_new(t)$repgasquant(cendiv,t)$gas(i)] = - repgasquant_irt(i,r,t) / repgasquant(cendiv,t) ; - -gasshare_cendiv(cendiv,t)$[sum{cendiv2,repgasquant(cendiv2,t)}] = repgasquant(cendiv,t) / sum{cendiv2,repgasquant(cendiv2,t)} ; - -gascost_cendiv(cendiv,t)$tmodel_new(t) = -*cost of natural gas for Sw_GasCurve = 2 (static natural gas prices) - + sum{(i,v,r,h)$[r_cendiv(r,cendiv)$valgen(i,v,r,t)$gas(i)$heat_rate(i,v,r,t) - $[not bio(i)]$[not cofire(i)]$[Sw_GasCurve = 2]], - hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN.l(i,v,r,h,t) } - -*cost of natural gas for Sw_GasCurve = 0 (census division supply curves natural gas prices) - + sum{gb, sum{h,hours(h) * GASUSED.l(cendiv,gb,h,t) } * gasprice(cendiv,gb,t) - }$[Sw_GasCurve = 0] - -*cost of natural gas for Sw_GasCurve = 3 (national supply curve for natural gas prices with census division multipliers) - + sum{(h,gb), hours(h) * GASUSED.l(cendiv,gb,h,t) - * gasadder_cd(cendiv,t,h) + gasprice_nat_bin(gb,t) - }$[Sw_GasCurve = 3] -*cost of natural gas for Sw_GasCurve = 1 (national and census division supply curves for natural gas prices) -*first - anticipated costs of gas consumption given last year's amount - + (sum{(i,v,r,h)$[valgen(i,v,r,t)$gas(i)], - gasmultterm(cendiv,t) * szn_adj_gas(h) * cendiv_weights(r,cendiv) * - hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } -*second - adjustments based on changes from last year's consumption at the regional and national level - + sum{(fuelbin), - gasbinp_regional(fuelbin,cendiv,t) * VGASBINQ_REGIONAL.l(fuelbin,cendiv,t) } - - + sum{(fuelbin), - gasbinp_national(fuelbin,t) * VGASBINQ_NATIONAL.l(fuelbin,t) } * gasshare_cendiv(cendiv,t) - - )$[Sw_GasCurve = 1]; - -*======================================== -* BIOFUEL COSTS -*======================================== - -bioshare_techba(i,r,t)$[(cofire(i) or bio(i))$tmodel_new(t)] = -* biofuel-based generation of tech i in the BA (biopower + cofire) - (( sum{(v,h)$[valgen(i,v,r,t)$bio(i)], hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } - + sum{(v,h)$[cofire(i)$valgen(i,v,r,t)], bio_cofire_perc * hours(h) * heat_rate(i,v,r,t) * GEN.l(i,v,r,h,t) } - ) / -* biofuel-based generation of all techs in the BA (biopower + cofire) - ( sum{(ii,v,h)$[valgen(ii,v,r,t)$bio(ii)], hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } - + sum{(ii,v,h)$[cofire(ii)$valgen(ii,v,r,t)], bio_cofire_perc * hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } - ) - )$[ sum{(ii,v,h)$[valgen(ii,v,r,t)$bio(ii)], hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } - + sum{(ii,v,h)$[cofire(ii)$valgen(ii,v,r,t)], bio_cofire_perc * hours(h) * heat_rate(ii,v,r,t) * GEN.l(ii,v,r,h,t) } - ] -; - -*========================= -* GENERATION -*========================= - -* Calculate generation and include charging, pumping, and production as negative values -gen_h(i,r,h,t)$[tmodel_new(t)$valgen_irt(i,r,t)] = - sum{v$valgen(i,v,r,t), GEN.l(i,v,r,h,t) -* less storage charging - - STORAGE_IN.l(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)]} -* less load from hydrogen production - - sum{(v,p)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)], PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t)}$Sw_Prod -; -* A small amount of upv capacity is actually csp-ns, so convert it back now. -* UPV capacity is already in MWac at this point (matching csp-ns), -* so don't need to account for ILR. -gen_h("csp-ns",r,h,t)$[cap_cspns(r,t)$tmodel_new(t)] - = cap_cspns(r,t) * m_cf("upv_6","new1",r,h,t) ; -* We have to take csp-ns generation from somewhere, so take it from upv_6 (which all the -* csp-ns-containing regions have) -gen_h("upv_6",r,h,t)$[cap_cspns(r,t)$tmodel_new(t)] - = gen_h("upv_6",r,h,t) - gen_h("csp-ns",r,h,t) ; -* Make sure it doesn't go negative, just in case -gen_h("upv_6",r,h,t)$[cap_cspns(r,t)$tmodel_new(t)$(gen_h("upv_6",r,h,t) < 0)] = 0 ; -gen_h_nat(i,h,t)$tmodel_new(t) = sum{r, gen_h(i,r,h,t) } ; - -* Do it again for stress periods -gen_h_stress(i,r,allh,t)$[tmodel_new(t)$valgen_irt(i,r,t)$h_stress_t(allh,t)] = - sum{v$valgen(i,v,r,t), GEN.l(i,v,r,allh,t) -* less storage charging - - STORAGE_IN.l(i,v,r,allh,t)$[storage_standalone(i) or hyd_add_pump(i)] } -* less load from hydrogen production - - sum{(v,p)$[consume(i)$valcap(i,v,r,t)$i_p(i,p)], - PRODUCE.l(p,i,v,r,allh,t) / prod_conversion_rate(i,v,r,t)}$Sw_Prod -; -gen_h_stress_nat(i,allh,t)$[tmodel_new(t)$h_stress_t(allh,t)] = sum{r, gen_h_stress(i,r,allh,t) } ; - -gen_ann(i,r,t)$tmodel_new(t) = sum{h, gen_h(i,r,h,t) * hours(h) } ; -gen_ann_nat(i,t)$tmodel_new(t) = sum{r, gen_ann(i,r,t) } ; - -* Report generation without the charging and production included as above -gen_ivrt(i,v,r,t)$valgen(i,v,r,t) = sum{h, GEN.l(i,v,r,h,t) * hours(h) } ; -gen_ivrt_uncurt(i,v,r,t)$[(vre(i) or storage_hybrid(i)$(not csp(i)))$valgen(i,v,r,t)] = - sum{h, m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; - -* Report generation that will be used as a denominator in outputs, where VRE uses uncurtailed gen and storage uses GEN -gen_uncurtailed(i,r,t)$[valgen_irt(i,r,t)$(not vre(i))] = sum{v, gen_ivrt(i,v,r,t) } ; -gen_uncurtailed(i,r,t)$[valgen_irt(i,r,t)$vre(i)] = sum{v, gen_ivrt_uncurt(i,v,r,t) } ; -gen_uncurtailed_nat(i,t)$tmodel_new(t) = sum{r, gen_uncurtailed(i,r,t) } ; - -* Storage outputs -stor_inout(i,v,r,t,"in")$[valgen(i,v,r,t)$storage(i)$[not storage_hybrid(i)$(not csp(i))]] = sum{h, STORAGE_IN.l(i,v,r,h,t) * hours(h) } ; -stor_inout(i,v,r,t,"out")$[valgen(i,v,r,t)$storage(i)] = gen_ivrt(i,v,r,t) ; -stor_in(i,v,r,h,t)$[storage(i)$valgen(i,v,r,t)$(not storage_hybrid(i)$(not csp(i)))] = STORAGE_IN.l(i,v,r,h,t) ; -stor_out(i,v,r,h,t)$[storage(i)$valgen(i,v,r,t)] = GEN.l(i,v,r,h,t) ; -stor_level(i,v,r,h,t)$[valgen(i,v,r,t)$storage(i)] = STORAGE_LEVEL.l(i,v,r,h,t) ; -stor_interday_level(i,v,r,allszn,t)$[valgen(i,v,r,t)$storage_interday(i)] = STORAGE_INTERDAY_LEVEL.l(i,v,r,allszn,t) ; -stor_interday_dispatch(i,v,r,h,t)$[valgen(i,v,r,t)$storage_interday(i)] = STORAGE_INTERDAY_DISPATCH.l(i,v,r,h,t) ; - -*===================================================================== -* WATER ACCOUNTING, CAPACITY, NEW CAPACITY, AND RETIRED CAPACITY -*===================================================================== -water_withdrawal_ivrt(i,v,r,t)$valgen(i,v,r,t) = sum{h$valgen(i,v,r,t), WAT.l(i,v,"with",r,h,t) } ; -water_consumption_ivrt(i,v,r,t)$valgen(i,v,r,t) = sum{h$valgen(i,v,r,t), WAT.l(i,v,"cons",r,h,t) } ; - -watcap_ivrt(i,v,r,t)$valcap(i,v,r,t) = WATCAP.l(i,v,r,t) ; -watcap_out(i,r,t)$valcap_irt(i,r,t) = sum{v$valcap(i,v,r,t), WATCAP.l(i,v,r,t) } ; -watcap_new_out(i,r,t)$[valcap_irt(i,r,t)$i_water_cooling(i)] = - sum{h$h_rep(h), - hours(h) - * sum{w$[i_w(i,w)], - water_rate(i,w) } - * ( sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)} - + sum{v$valcap(i,v,r,t), - (1-upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))}$[upgrade(i)$Sw_Upgrades] ) - * (1 + sum{(v,szn), h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - } / 1E6 - + sum{v$[psh(i)$valinv(i,v,r,t)], WATCAP.l(i,v,r,t)} ; - -watcap_new_ivrt(i,v,r,t)$[valcap(i,v,r,t)$i_water_cooling(i)] = - sum{h$h_rep(h), - hours(h) - * sum{w$[i_w(i,w)], - water_rate(i,w) } - * ( [INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)]$valinv(i,v,r,t) - + [(1-upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))]$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades] ) - * (1 + sum{szn, h_szn(h,szn) * seas_cap_frac_delta(i,v,r,szn,t)}) - } / 1E6 - + WATCAP.l(i,v,r,t)$psh(i) ; - -watcap_new_ann_out(i,v,r,t)$watcap_new_ivrt(i,v,r,t) = watcap_new_ivrt(i,v,r,t) / (yeart(t) - sum(tt$tprev(t,tt), yeart(tt))) ; - -* --- Water Capacity Retirements ---* -watret_out(i,r,t)$[(not tfirst(t))] = sum{tt$tprev(t,tt), watcap_out(i,r,tt)} - watcap_out(i,r,t) + watcap_new_out(i,r,t) ; -watret_out(i,r,t)$[abs(watret_out(i,r,t)) < 1e-6] = 0 ; - -watret_ivrt(i,v,r,t)$[(not tfirst(t))] = sum{tt$tprev(t,tt), watcap_ivrt(i,v,r,tt)} - watcap_ivrt(i,v,r,t) + watcap_new_ivrt(i,v,r,t) ; -watret_ivrt(i,v,r,t)$[(abs(watret_ivrt(i,v,r,t)) < 1e-6)] = 0 ; - -watret_ann_out(i,v,r,t)$watret_ivrt(i,v,r,t) = watret_ivrt(i,v,r,t) / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; - -*========================= -* Operating Reserves -*========================= - -opres_supply_h(ortype,i,r,h,t)$[tmodel_new(t)$reserve_frac(i,ortype)] = - sum{v, OPRES.l(ortype,i,v,r,h,t) } ; - - -opres_supply(ortype,i,r,t)$[tmodel_new(t)$reserve_frac(i,ortype)] = - sum{h, hours(h) * opRes_supply_h(ortype,i,r,h,t) } ; - -* total opres trade -opres_trade(ortype,r,rr,t)$[opres_routes(r,rr,t)$tmodel_new(t)] = - sum{h, hours(h) * OPRES_FLOW.l(ortype,r,rr,h,t) } ; - -*========================= -* LOSSES AND CURTAILMENT -*========================= - -gen_new_uncurt(i,r,h,t)$[(vre(i) or storage_hybrid(i)$(not csp(i)))$valcap_irt(i,r,t)] = - sum{v$valinv(i,v,r,t), (INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)) * m_cf(i,v,r,h,t) * hours(h) } -; - -* Formulation follows eq_curt_gen_balance(r,h,t); since it uses =g= there may be extra curtailment -* beyond CURT.l(r,h,t) so we recalculate as (availability - generation - operating reserves) -curt_h(r,h,t)$tmodel_new(t) = - sum{(i,v)$[valcap(i,v,r,t)$(vre(i) or storage_hybrid(i)$(not csp(i)))], - m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) } - - sum{(i,v)$[valgen(i,v,r,t)$vre(i)], GEN.l(i,v,r,h,t) } - - sum{(i,v)$[valgen(i,v,r,t)$storage_hybrid(i)$(not csp(i))], GEN_PLANT.l(i,v,r,h,t) }$Sw_HybridPlant - - sum{(ortype,i,v)$[Sw_OpRes$opres_h(h)$reserve_frac(i,ortype)$valgen(i,v,r,t)$vre(i)], - OPRES.l(ortype,i,v,r,h,t) } -; - -curt_ann(r,t)$tmodel_new(t) = sum{h, curt_h(r,h,t) * hours(h) } ; - -curt_tech(i,r,t)$[tmodel_new(t)$vre(i)] = - sum{(v,h)$valcap(i,v,r,t), - m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } - - sum{(v,h)$valgen(i,v,r,t), - GEN.l(i,v,r,h,t) * hours(h) } - - sum{(ortype,v,h)$[Sw_OpRes$opres_h(h)$reserve_frac(i,ortype)$valgen(i,v,r,t)], - OPRES.l(ortype,i,v,r,h,t) * hours(h) } -; - -curt_rate_tech(i,r,t)$[tmodel_new(t)$vre(i)$(gen_ann(i,r,t) + curt_tech(i,r,t))] = - curt_tech(i,r,t) / (gen_ann(i,r,t) + curt_tech(i,r,t)) -; - -curt_rate(t) - $[tmodel_new(t) - $(sum{(i,r)$[vre(i) or storage_hybrid(i)$(not csp(i))], gen_ann(i,r,t) } + sum{r, curt_ann(r,t) })] - = sum{r, curt_ann(r,t) } - / (sum{(i,r)$[vre(i) or storage_hybrid(i)$(not csp(i))], gen_ann(i,r,t) } + sum{r, curt_ann(r,t) }) ; - -losses_ann('storage',t)$tmodel_new(t) = sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)], STORAGE_IN.l(i,v,r,h,t) * hours(h) } - - sum{(i,v,r,h)$[valcap(i,v,r,t)$storage_standalone(i)], GEN.l(i,v,r,h,t) * hours(h) } ; - -losses_ann('trans',t)$tmodel_new(t) = - sum{(rr,r,h,trtype)$routes(rr,r,trtype,t), - (tranloss(rr,r,trtype) * FLOW.l(rr,r,h,t,trtype) * hours(h)) - + ((CONVERSION.l(r,h,"AC","VSC",t) + CONVERSION.l(r,h,"VSC","AC",t))* (1 - converter_efficiency_vsc) * hours(h))$[val_converter(r,t)$Sw_VSC] - } ; - -losses_ann('curt',t)$tmodel_new(t) = sum{r, curt_ann(r,t) } ; - -losses_ann('load',t)$tmodel_new(t) = sum{(r,h), LOAD.l(r,h,t) * hours(h) } ; - -losses_tran_h(rr,r,h,trtype,t)$[routes(r,rr,trtype,t)$tmodel_new(t)] - = tranloss(rr,r,trtype) * FLOW.l(rr,r,h,t,trtype) - + ((CONVERSION.l(r,h,"AC","VSC",t) + CONVERSION.l(r,h,"VSC","AC",t))* (1 - converter_efficiency_vsc))$[val_converter(r,t)$Sw_VSC] ; - -*========================= -* CAPACITY -*========================= - -cap_deg_ivrt(i,v,r,t)$valcap(i,v,r,t) = CAP.l(i,v,r,t) / ilr(i) ; - -cap_ivrt(i,v,r,t)$[(not (upv(i) or wind(i)))$valcap(i,v,r,t)] = cap_deg_ivrt(i,v,r,t) ; -*upv, and wind have degradation, so use INV rather than CAP to get the reported capacity -cap_ivrt(i,v,r,t)$[(upv(i) or wind(i))$valcap(i,v,r,t)] = ( - m_capacity_exog(i,v,r,t)$tmodel_new(t) - + sum{tt$[inv_cond(i,v,r,t,tt)$[tmodel(tt) or tfix(tt)]], - INV.l(i,v,r,tt) + INV_REFURB.l(i,v,r,tt)$[refurbtech(i)$Sw_Refurb]}) / ilr(i) ; - -cap_out(i,r,t)$[valcap_irt(i,r,t)$tmodel_new(t)] = sum{v$valcap(i,v,r,t), cap_ivrt(i,v,r,t) } ; -* A small amount of upv capacity is actually csp-ns, so convert it back now. -* UPV capacity is already in MWac at this point (matching csp-ns), -* so don't need to account for ILR -cap_out("csp-ns",r,t)$[cap_cspns(r,t)$tmodel_new(t)] = cap_cspns(r,t) ; -* We have to take csp-ns capacity from somewhere, so take it from upv_6 (which all the -* csp-ns-containing regions have) -cap_out("upv_6",r,t)$[cap_cspns(r,t)$tmodel_new(t)] = cap_out("upv_6",r,t) - cap_cspns(r,t) ; -* Make sure it doesn't go negative, just in case -cap_out("upv_6",r,t)$[cap_cspns(r,t)$tmodel_new(t)$(cap_out("upv_6",r,t) < 0)] = 0 ; -cap_nat(i,t)$tmodel_new(t) = sum{r, cap_out(i,r,t) } ; - -* Exogenous capacity (used by reeds_to_rev) -cap_exog(i,v,r,t)$tmodel_new(t) = m_capacity_exog(i,v,r,t) ; - -*========================= -* NEW CAPACITY -*========================= - -cap_new_out(i,r,t)$[valcap_irt(i,r,t)] = [ - sum{v$valinv(i,v,r,t), - INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t) } - + sum{v$valcap(i,v,r,t), - (1 - upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))}$[upgrade(i)$Sw_Upgrades] - ] / ilr(i) ; -* Capacity of distpv is not tracked in INV because it is an exogenous input, so use the change in cap_out to calculate new capacity -* (except for the first year, in which all distpv capacity is counted as new) -cap_new_out("distpv",r,t)$[tfirst(t)$valcap_irt("distpv",r,t)] = cap_out("distpv",r,t) ; -cap_new_out("distpv",r,t)$[(not tfirst(t))$valcap_irt("distpv",r,t)] = cap_out("distpv",r,t) - sum{tt$tprev(t,tt), cap_out("distpv",r,tt) } ; -cap_new_ann(i,r,t)$cap_new_out(i,r,t) = cap_new_out(i,r,t) / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; -cap_new_ann_nat(i,t)$tmodel_new(t) = sum{r, cap_new_ann(i,r,t) } ; -cap_new_bin_out(i,v,r,t,rscbin)$[rsc_i(i)$valinv(i,v,r,t)] = INV_RSC.l(i,v,r,rscbin,t) / ilr(i) ; -cap_new_bin_out(i,v,r,t,"bin1")$[(not rsc_i(i))$valinv(i,v,r,t)] = INV.l(i,v,r,t) / ilr(i) ; -cap_new_ivrt(i,v,r,t)$[valcap(i,v,r,t)] = [ - [INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)]$valinv(i,v,r,t) - + [(1-upgrade_derate(i,v,r,t)) * (UPGRADES.l(i,v,r,t) - UPGRADES_RETIRE.l(i,v,r,t))]$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades] - ] / ilr(i) ; -cap_new_ivrt("distpv",v,r,t)$[tfirst(t)$valcap("distpv",v,r,t)] = cap_ivrt("distpv",v,r,t) ; -cap_new_ivrt("distpv",v,r,t)$[(not tfirst(t))$valcap("distpv",v,r,t)] = cap_ivrt("distpv",v,r,t) - sum{tt$tprev(t,tt), cap_ivrt("distpv",v,r,tt) } ; -cap_new_ivrt_refurb(i,v,r,t)$valinv(i,v,r,t) = INV_REFURB.l(i,v,r,t) / ilr(i) ; - -* Capacity by reV site -site_spurinv(x,t)$[tmodel_new(t)$xfeas(x)] = INV_SPUR.l(x,t) ; -site_spurcap(x,t)$[tmodel_new(t)$xfeas(x)] = CAP_SPUR.l(x,t) ; - -site_cap(i,x,t)$[tmodel_new(t)$sum{(r,rscbin), spurline_sitemap(i,r,rscbin,x)}] = - sum{(v,r,rscbin,tt) - $[spurline_sitemap(i,r,rscbin,x) - $cap_new_bin_out(i,v,r,tt,rscbin) - $(yeart(tt) <= yeart(t))], -* Multiply by ILR to get DC capacity for PV - cap_new_bin_out(i,v,r,tt,rscbin) * ilr(i) - } ; - -site_gir(i,x,t)$[site_cap(i,x,t)$site_spurcap(x,t)] = site_cap(i,x,t) / site_spurcap(x,t) ; - -site_pv_fraction(x,t)$sum{i$spur_techs(i), site_cap(i,x,t)} = - sum{i$upv(i), site_cap(i,x,t)} / (sum{i$spur_techs(i), site_cap(i,x,t)}) ; - -site_hybridization(x,t)$site_pv_fraction(x,t) = abs(1 - 2 * abs(site_pv_fraction(x,t) - 0.5)) ; - -*========================= -* AVAILABLE CAPACITY -*========================= -cap_avail(i,r,t,rscbin)$[tmodel_new(t)$rsc_i(i)$m_rscfeas(r,i,rscbin)$m_rsc_con(r,i)] = - m_rsc_dat(r,i,rscbin,"cap") - + hyd_add_upg_cap(r,i,rscbin,t)$(Sw_HydroCapEnerUpgradeType=1) - -- ( - sum{(ii,v,tt)$[valinv(ii,v,r,tt)$(yeart(tt) < yeart(t))$rsc_agg(i,ii)], - INV_RSC.l(ii,v,r,rscbin,tt) * resourcescaler(ii) } - - + sum{(ii,v,tt)$[tfirst(tt)$rsc_agg(i,ii)$exog_rsc(i)], - capacity_exog_rsc(ii,v,r,rscbin,tt) } -); - -capacity_offline(i,r,allh,t) - $[valcap_irt(i,r,t)$tmodel_new(t)$(h_stress_t(allh,t) or h_rep(allh))] = - cap_out(i,r,t) * (1 - avail(i,r,allh)) ; - -forced_outage(i) = sum{(r,h), outage_forced_h(i,r,h) * hours(h) } / sum{(r,h), hours(h) } ; -planned_outage(i) = sum{h, outage_scheduled_h(i,h) * hours(h) } / sum{h, hours(h) } ; - -*========================= -* UPGRADED CAPACITY -*========================= - -cap_upgrade(i,r,t)$[upgrade(i)$valcap_irt(i,r,t)] = sum{v, (1-upgrade_derate(i,v,r,t)) * UPGRADES.l(i,v,r,t) } ; -cap_upgrade_ivrt(i,v,r,t)$[valcap(i,v,r,t)$upgrade(i)$Sw_Upgrades] = (1-upgrade_derate(i,v,r,t)) * UPGRADES.l(i,v,r,t) ; - -*========================= -* RETIRED CAPACITY -*========================= - -ret_ivrt(i,v,r,t)$[(not tfirst(t))] = - sum{tt$tprev(t,tt), cap_ivrt(i,v,r,tt) } - cap_ivrt(i,v,r,t) + cap_new_ivrt(i,v,r,t) - - sum{ii$upgrade_from(ii,i), UPGRADES.l(ii,v,r,t) } ; -ret_ivrt(i,v,r,t)$[abs(ret_ivrt(i,v,r,t)) < 1e-6] = 0 ; - -ret_out(i,r,t)$[(not tfirst(t))] = sum{v, ret_ivrt(i,v,r,t) } ; -ret_out(i,r,t)$[abs(ret_out(i,r,t)) < 1e-6] = 0 ; -ret_ann(i,r,t)$ret_out(i,r,t) = ret_out(i,r,t) / (yeart(t) - sum{tt$tprev(t,tt), yeart(tt) }) ; -ret_ann_nat(i,t)$tmodel_new(t) = sum{r, ret_ann(i,r,t) } ; - -*================================== -* BINNED STORAGE CAPACITY -*================================== - -cap_sdbin_out(i,r,ccseason,sdbin,t)$valcap_irt(i,r,t) = sum{v, CAP_SDBIN.l(i,v,r,ccseason,sdbin,t)} ; - -* energy capacity of storage -stor_energy_cap(i,v,r,t)$[tmodel_new(t)$valcap(i,v,r,t)] = - storage_duration(i) * CAP.l(i,v,r,t) * (1$CSP_Storage(i) + 1$psh(i) + bcr(i)$[battery(i) or storage_hybrid(i)$(not csp(i))]) ; - -* add PSH energy capacity to cap_energy_ivrt -cap_energy_ivrt(i,v,r,t)$[valcap(i,v,r,t)$psh(i)] = CAP.l(i,v,r,t) * storage_duration(i) ; - -* battery storage duration -storage_duration_out(i,v,r,t)$[valcap(i,v,r,t)$battery(i)$CAP.l(i,v,r,t)] = - CAP_ENERGY.l(i,v,r,t) / CAP.l(i,v,r,t) ; - -*================================== -* CAPACITY CREDIT AND FIRM CAPACITY -*================================== - -cc_all_out(i,v,r,ccseason,t)$tmodel_new(t) = - cc_int(i,v,r,ccseason,t)$[(vre(i) or csp(i) or storage(i) or storage_hybrid(i)$(not csp(i)))$valcap(i,v,r,t)] + - m_cc_mar(i,r,ccseason,t)$[(vre(i) or csp(i) or storage(i) or storage_hybrid(i)$(not csp(i)))$valinv_init(i,v,r,t)] -; - -cap_new_cc(i,r,ccseason,t)$[(vre(i) or storage(i) or storage_hybrid(i)$(not csp(i)))$valcap_irt(i,r,t)] = sum{v$ivt(i,v,t),cap_new_ivrt(i,v,r,t) } ; - -cc_new(i,r,ccseason,t)$[valcap_irt(i,r,t)$cap_new_cc(i,r,ccseason,t)] = sum{v$ivt(i,v,t), cc_all_out(i,v,r,ccseason,t) } ; - -cap_firm(i,r,ccseason,t)$[valcap_irt(i,r,t)$[not consume(i)]$tmodel_new(t)$Sw_PRM_CapCredit] = - sum{v$[(not vre(i))$(not hydro(i))$(not storage(i))$(not storage_hybrid(i)$(not csp(i)))$valcap(i,v,r,t)], - CAP.l(i,v,r,t) * (1 + ccseason_cap_frac_delta(i,v,r,ccseason,t)) } - + cc_old(i,r,ccseason,t) - + sum{v$[(vre(i) or csp(i) or storage_hybrid(i)$(not csp(i)))$valinv(i,v,r,t)], - m_cc_mar(i,r,ccseason,t) * (INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)$[refurbtech(i)$Sw_Refurb]) } - + sum{v$[(vre(i) or csp(i) or storage_hybrid(i)$(not csp(i)))$valcap(i,v,r,t)], - cc_int(i,v,r,ccseason,t) * CAP.l(i,v,r,t) } - + cc_excess(i,r,ccseason,t)$[(vre(i) or csp(i) or storage_hybrid(i)$(not csp(i)))] - + sum{(v,h)$[hydro_nd(i)$valgen(i,v,r,t)$h_ccseason_prm(h,ccseason)], - GEN.l(i,v,r,h,t) } - + sum{v$[hydro_d(i)$valcap(i,v,r,t)], - CAP.l(i,v,r,t) * cap_hyd_ccseason_adj(i,ccseason,r) * (1 + hydro_capcredit_delta(i,t)) } - + sum{(v,sdbin)$[valcap(i,v,r,t)$(storage_standalone(i) or hyd_add_pump(i))], CAP_SDBIN.l(i,v,r,ccseason,sdbin,t) * cc_storage(i,sdbin) } - + sum{(v,sdbin)$[valcap(i,v,r,t)$storage_hybrid(i)], CAP_SDBIN.l(i,v,r,ccseason,sdbin,t) * cc_storage(i,sdbin) * hybrid_cc_derate(i,r,ccseason,sdbin,t) } ; - -* Capacity trading to meet PRM -captrade(r,rr,trtype,ccseason,t)$[routes(r,rr,trtype,t)$routes_prm(r,rr)$tmodel_new(t)] = PRMTRADE.l(r,rr,trtype,ccseason,t) ; - -*======================================== -* REVENUE LEVELS -*======================================== - -revenue('load',i,r,t)$valgen_irt(i,r,t) = sum{(v,h)$valgen(i,v,r,t), - GEN.l(i,v,r,h,t) * hours(h) * reqt_price('load','na',r,h,t) } ; - -*revenue from storage charging (storage charging from curtailment recovery does not have a cost) -revenue('charge',i,r,t)$[storage_standalone(i)$valgen_irt(i,r,t)] = - sum{(v,h)$valgen(i,v,r,t), - STORAGE_IN.l(i,v,r,h,t) * hours(h) * reqt_price('load','na',r,h,t) } ; - -revenue('res_marg',i,r,t)$[valgen_irt(i,r,t)$Sw_PRM_CapCredit] = sum{ccseason, - cap_firm(i,r,ccseason,t) * reqt_price('res_marg','na',r,ccseason,t) } ; -revenue('res_marg',i,r,t)$[valgen_irt(i,r,t)$(Sw_PRM_CapCredit=0)] = sum{allh$h_stress_t(allh,t), - gen_h_stress(i,r,allh,t) * reqt_price('res_marg','na',r,allh,t) } ; - -revenue('oper_res',i,r,t)$valgen_irt(i,r,t) = sum{(ortype,v,h)$valgen(i,v,r,t), - OPRES.l(ortype,i,v,r,h,t) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } ; - -revenue('rps',i,r,t)$valgen_irt(i,r,t) = - sum{(v,h,RPSCat)$[valgen(i,v,r,t)$sum{st$r_st(r,st), RecTech(RPSCat,i,st,t) }], - GEN.l(i,v,r,h,t) * sum{st$r_st(r,st), RPSTechMult(RPSCat,i,st) } * hours(h) * reqt_price('state_rps',RPSCat,r,'ann',t) } ; - -revenue_nat(rev_cat,i,t)$tmodel_new(t) = sum{r, revenue(rev_cat,i,r,t) } ; - -revenue_en(rev_cat,i,r,t) - $[tmodel_new(t) - $valgen_irt(i,r,t) - $sum{h, gen_h(i,r,h,t) * hours(h) } - $[not vre(i)]] = - revenue(rev_cat,i,r,t) / sum{h, gen_h(i,r,h,t) * hours(h) } ; - -revenue_en(rev_cat,i,r,t) - $[tmodel_new(t) - $sum{(v,h)$[valcap(i,v,r,t)], m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) } - $vre(i)] = - revenue(rev_cat,i,r,t) / sum{(v,h)$valcap(i,v,r,t), - m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; - -revenue_en_nat(rev_cat,i,t) - $[tmodel_new(t) - $sum{(r,h)$valgen_irt(i,r,t), gen_h(i,r,h,t) * hours(h) } - $[not vre(i)]] = - revenue_nat(rev_cat,i,t) / sum{(r,h)$valgen_irt(i,r,t), gen_h(i,r,h,t) * hours(h) } ; - -revenue_en_nat(rev_cat,i,t) - $[tmodel_new(t) - $sum{(v,r,h)$[valcap(i,v,r,t)], m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) } - $vre(i)] = - revenue_nat(rev_cat,i,t) / sum{(v,r,h)$valcap(i,v,r,t), - m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; - -revenue_cap(rev_cat,i,r,t)$[tmodel_new(t)$cap_out(i,r,t)] = - revenue(rev_cat,i,r,t) / cap_out(i,r,t) ; - -revenue_cap_nat(rev_cat,i,t)$[tmodel_new(t)$sum{r$valcap_irt(i,r,t), cap_out(i,r,t) }] = - revenue_nat(rev_cat,i,t) / cap_nat(i,t) ; - -*======================================== -* Value (Revenue) of new builds -*======================================== - -valnew('MW',i,r,t)$[(not tfirst(t))$valcap_irt(i,r,t)] = - sum{v$valinv(i,v,r,t), INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t) } / ilr(i) ; -valnew('inv_cap_ratio',i,r,t)$[valnew('MW',i,r,t)] = - sum{v$[valinv(i,v,r,t)$CAP.l(i,v,r,t)], (INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)) - / CAP.l(i,v,r,t) } ; -valnew('MWh',i,r,t)$[valnew('MW',i,r,t)] = - sum{v$valinv(i,v,r,t), gen_ivrt(i,v,r,t)} * valnew('inv_cap_ratio',i,r,t) ; -* Use uncurtailed energy for VRE -valnew('MWh',i,r,t)$[valnew('MW',i,r,t)$sum{v$valinv(i,v,r,t), gen_ivrt_uncurt(i,v,r,t)}] = - sum{v$valinv(i,v,r,t), gen_ivrt_uncurt(i,v,r,t)} * valnew('inv_cap_ratio',i,r,t) ; -valnew('MWh','benchmark',r,t)$tmodel_new(t) = sum{h, reqt_quant('load','na',r,h,t)} ; -valnew('MWh','benchmark','sys',t)$tmodel_new(t) = sum{(r,h), reqt_quant('load','na',r,h,t)} ; -valnew('MW','benchmark',r,t)$tmodel_new(t) = reqt_quant('res_marg_ann','na',r,'ann',t) ; -valnew('MW','benchmark','sys',t)$tmodel_new(t) = sum{r, reqt_quant('res_marg_ann','na',r,'ann',t)} ; - -valnew('val_load',i,r,t)$valnew('MW',i,r,t) = sum{(v,h)$valinv(i,v,r,t), - (GEN.l(i,v,r,h,t) - STORAGE_IN.l(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)]) - * hours(h) * reqt_price('load','na',r,h,t) } * valnew('inv_cap_ratio',i,r,t) ; -*'val_load_sys' is the val our tech would have if valued at the system-average load price profile. -valnew('val_load_sys',i,r,t)$valnew('MW',i,r,t) = sum{(v,h)$valinv(i,v,r,t), - (GEN.l(i,v,r,h,t) - STORAGE_IN.l(i,v,r,h,t)$[storage_standalone(i) or hyd_add_pump(i)]) - * hours(h) * reqt_price_sys('load','na',h,t) } * valnew('inv_cap_ratio',i,r,t) ; -*Annual-average price at each r: -valnew('val_load','benchmark',r,t)$tmodel_new(t) = - sum{h, reqt_price('load','na',r,h,t) * reqt_quant('load','na',r,h,t)} ; -*Annual-average price of the system -valnew('val_load','benchmark','sys',t)$tmodel_new(t) = - sum{(r,h), reqt_price('load','na',r,h,t) * reqt_quant('load','na',r,h,t)} ; - -valnew('val_resmarg',i,r,t)$[(Sw_PRM_CapCredit=0)$valnew('MW',i,r,t)] = - sum{(v,allh)$[h_stress_t(allh,t)$valinv(i,v,r,t)], - (GEN.l(i,v,r,allh,t) - STORAGE_IN.l(i,v,r,allh,t)$[storage_standalone(i) or hyd_add_pump(i)]) - * reqt_price('res_marg','na',r,allh,t)} * valnew('inv_cap_ratio',i,r,t) ; -* New VRE for the CapCredit formulation is a special case in that new is distinct from old of the same vintage -valnew('val_resmarg',i,r,t)$[(Sw_PRM_CapCredit=1)$vre(i)$valnew('MW',i,r,t)] = - sum{ccseason, m_cc_mar(i,r,ccseason,t) * valnew('MW',i,r,t) * reqt_price('res_marg','na',r,ccseason,t)}; -valnew('val_resmarg_sys',i,r,t)$[(Sw_PRM_CapCredit=0)$valnew('MW',i,r,t)] = - sum{(v,allh)$[h_stress_t(allh,t)$valinv(i,v,r,t)], - (GEN.l(i,v,r,allh,t) - STORAGE_IN.l(i,v,r,allh,t)$[storage_standalone(i) or hyd_add_pump(i)]) - * reqt_price_sys('res_marg','na',allh,t)} * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_resmarg_sys',i,r,t)$[(Sw_PRM_CapCredit=1)$vre(i)$valnew('MW',i,r,t)] = - sum{ccseason, m_cc_mar(i,r,ccseason,t) * valnew('MW',i,r,t) * reqt_price_sys('res_marg','na',ccseason,t)} ; -* Note: val_resmarg and val_resmarg_sys are missing for the capacity credit formulation for non-VRE. -* These would need cap_firm() but with vintage... -valnew('val_resmarg','benchmark',r,t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)] = - sum{allh$h_stress_t(allh,t), reqt_price('res_marg','na',r,allh,t) * reqt_quant('res_marg','na',r,allh,t)} ; -valnew('val_resmarg','benchmark','sys',t)$[(Sw_PRM_CapCredit=0)$tmodel_new(t)] = - sum{(r,allh)$h_stress_t(allh,t), reqt_price('res_marg','na',r,allh,t) * reqt_quant('res_marg','na',r,allh,t)} ; -valnew('val_resmarg','benchmark',r,t)$[(Sw_PRM_CapCredit=1)$tmodel_new(t)] = - sum{ccseason, reqt_price('res_marg','na',r,ccseason,t) * reqt_quant('res_marg','na',r,ccseason,t)} ; -valnew('val_resmarg','benchmark','sys',t)$[(Sw_PRM_CapCredit=1)$tmodel_new(t)] = - sum{(r,ccseason), reqt_price('res_marg','na',r,ccseason,t) * reqt_quant('res_marg','na',r,ccseason,t)} ; - -valnew('val_opres',i,r,t)$[(not (wind(i) or pv(i) or pvb(i)))$valnew('MW',i,r,t)] = sum{(ortype,v,h)$valinv(i,v,r,t), - OPRES.l(ortype,i,v,r,h,t) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_opres',i,r,t)$[wind(i)$valnew('MW',i,r,t)] = - -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$valinv(i,v,r,t)], - orperc(ortype,"or_wind") * GEN.l(i,v,r,h,t) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } - * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_opres',i,r,t)$[(pv(i) or pvb(i))$valnew('MW',i,r,t)] = - -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$dayhours(h)], - orperc(ortype,"or_pv") * CAP.l(i,v,r,t) / ilr(i) * hours(h) * reqt_price('oper_res',ortype,r,h,t) } - * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_opres_sys',i,r,t)$[(not (wind(i) or pv(i) or pvb(i)))$valnew('MW',i,r,t)] = sum{(ortype,v,h)$valinv(i,v,r,t), - OPRES.l(ortype,i,v,r,h,t) * hours(h) * reqt_price_sys('oper_res',ortype,h,t) } * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_opres_sys',i,r,t)$[wind(i)$valnew('MW',i,r,t)] = - -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$valinv(i,v,r,t)], - orperc(ortype,"or_wind") * GEN.l(i,v,r,h,t) * hours(h) * reqt_price_sys('oper_res',ortype,h,t) } - * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_opres_sys',i,r,t)$[(pv(i) or pvb(i))$valnew('MW',i,r,t)] = - -1 * sum{(ortype,v,h)$[Sw_OpRes$opres_model(ortype)$opres_h(h)$dayhours(h)], - orperc(ortype,"or_pv") * CAP.l(i,v,r,t) / ilr(i) * hours(h) * reqt_price_sys('oper_res',ortype,h,t) } - * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_opres','benchmark',r,t)$tmodel_new(t) = - sum{(ortype,h), reqt_price('oper_res',ortype,r,h,t) * reqt_quant('oper_res',ortype,r,h,t)} ; -valnew('val_opres','benchmark','sys',t)$tmodel_new(t) = - sum{(ortype,r,h), reqt_price('oper_res',ortype,r,h,t) * reqt_quant('oper_res',ortype,r,h,t)} ; - -valnew('val_rps',i,r,t)$valnew('MW',i,r,t) = - sum{(v,h,RPSCat)$[valinv(i,v,r,t)$sum{st$r_st(r,st), RecTech(RPSCat,i,st,t) }], - GEN.l(i,v,r,h,t) * sum{st$r_st(r,st), RPSTechMult(RPSCat,i,st) } * hours(h) * reqt_price('state_rps',RPSCat,r,'ann',t) } - * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_rps_sys',i,r,t)$valnew('MW',i,r,t) = - sum{(v,h,RPSCat)$[valinv(i,v,r,t)$sum{st$r_st(r,st), RecTech(RPSCat,i,st,t) }], - GEN.l(i,v,r,h,t) * sum{st$r_st(r,st), RPSTechMult(RPSCat,i,st) } * hours(h) * reqt_price_sys('state_rps',RPSCat,'ann',t) } - * valnew('inv_cap_ratio',i,r,t) ; -valnew('val_rps','benchmark',r,t)$tmodel_new(t) = - sum{RPSCat, reqt_price('state_rps',RPSCat,r,'ann',t) * reqt_quant('state_rps',RPSCat,r,'ann',t)} ; -*Annual-average price of the system -valnew('val_rps','benchmark','sys',t)$tmodel_new(t) = - sum{(r,RPSCat), reqt_price('state_rps',RPSCat,r,'ann',t) * reqt_quant('state_rps',RPSCat,r,'ann',t)} ; - -*========================= -* EMISSIONS -*========================= -* emit_r is calculated the same as the EMIT variable in the model. We do not use -* EMIT.l here because the emissions are only modeled for those in the emit_modeled -* set. -emit_r(etype,e,r,t)$tmodel_new(t) = - -* Emissions from generation - sum{(i,v,h)$[valgen(i,v,r,t)$h_rep(h)], - hours(h) * emit_rate(etype,e,i,v,r,t) - * (GEN.l(i,v,r,h,t) - + CCSFLEX_POW.l(i,v,r,h,t)$[ccsflex(i)$(Sw_CCSFLEX_BYP OR Sw_CCSFLEX_STO OR Sw_CCSFLEX_DAC)]) - } - -* Plus emissions produced via production activities (SMR, SMR-CCS, DAC) -* The "production" of negative CO2 emissions via DAC is also included here - + sum{(p,i,v,h)$[valcap(i,v,r,t)$i_p(i,p)$h_rep(h)], - hours(h) * prod_emit_rate(etype,e,i,t) - * PRODUCE.l(p,i,v,r,h,t) - } - -*[minus] co2 reduce from flexible CCS capture -*capture = capture per energy used by the ccs system * CCS energy - -* Flexible CCS - bypass - - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_byp(i)$h_rep(h)], - ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POW.l(i,v,r,h,t) })$[sameas(e,"co2")]$Sw_CCSFLEX_BYP - -* Flexible CCS - storage - - (sum{(i,v,h)$[valgen(i,v,r,t)$ccsflex_sto(i)$h_rep(h)], - ccsflex_co2eff(i,t) * hours(h) * CCSFLEX_POWREQ.l(i,v,r,h,t) })$[sameas(e,"co2")]$Sw_CCSFLEX_STO -; - -* Apply global warming potential to include CH4 and N2O in CO2(e) -emit_r(etype,"CO2e",r,t)$tmodel_new(t) = sum{e,emit_r(etype,e,r,t)*gwp(e)} ; -emit_nat(etype,eall,t)$tmodel_new(t) = sum{r, emit_r(etype,eall,r,t) } ; - -* Generation emissions by tech and region -emit_irt(etype,e,i,r,t)$[tmodel_new(t)$(not sameas(e,"CO2"))$valgen_irt(i,r,t)] = - sum{(v,h)$[valgen(i,v,r,t)], - hours(h) * emit_rate(etype,e,i,v,r,t) * GEN.l(i,v,r,h,t) } ; -* Production-related emissions by tech and region -emit_irt(etype,e,i,r,t)$[tmodel_new(t)$(not sameas(e,"CO2"))$sum{p, i_p(i,p)}] = - sum{(p,v,h)$i_p(i,p), - hours(h) * prod_emit_rate(etype,e,i,t) * PRODUCE.l(p,i,v,r,h,t) } ; -* CO2 generation emissions by tech and region -emit_irt(etype,"CO2",i,r,t)$[tmodel_new(t)$valgen_irt(i,r,t)] = sum{(v,h)$[valgen(i,v,r,t)], - hours(h) * emit_rate(etype,"CO2",i,v,r,t) * GEN.l(i,v,r,h,t) } ; -* CO2 production-related emissions by tech and region -emit_irt(etype,"CO2",i,r,t)$[tmodel_new(t)$sum{p, i_p(i,p)}] = - sum{(p,v,h)$i_p(i,p), - hours(h) * prod_emit_rate(etype,"CO2",i,t) * PRODUCE.l(p,i,v,r,h,t) } ; -* Apply global warming potential to include other GHGs in CO2(e) -emit_irt(etype,"CO2e",i,r,t)$tmodel_new(t) = sum{e,emit_irt(etype,e,i,r,t) * gwp(e)} ; - -emit_nat_tech(etype,eall,i,t) = sum{r, emit_irt(etype,eall,i,r,t)} ; - -emit_weighted(etype,eall) = sum{t$tmodel(t), emit_nat(etype,eall,t) * pvf_onm(t) } ; - -emit_rate_regional(r,t)$tmodel_new(t) = co2_emit_rate_r(r,t) ; - -* captured CO2 emissions from CCS and DAC -emit_captured_irt(i,r,t)$tmodel_new(t) = - sum{(v,h)$[valgen(i,v,r,t)], hours(h) * capture_rate("CO2",i,v,r,t) * GEN.l(i,v,r,h,t) } ; - -emit_captured_irt("smr_ccs",r,t)$tmodel_new(t) = - sum{(v,h,p)$[valcap("smr_ccs",v,r,t)$i_p("smr_ccs",p)], smr_capture_rate * hours(h) - * smr_co2_intensity * PRODUCE.l(p,"smr_ccs",v,r,h,t) } ; - -emit_captured_irt(i,r,t)$[tmodel_new(t)$dac(i)] = - sum{(v,h,p)$[dac(i)$valcap(i,v,r,t)$i_p(i,p)], hours(h) * PRODUCE.l(p,i,v,r,h,t)} ; - -emit_captured_nat(i,t)$tmodel_new(t) = sum{r, emit_captured_irt(i,r,t) } ; - - -*================================== -* National RE Constraint Marginals -*================================== - -RE_gen_price_nat(t)$tmodel_new(t) = (1/cost_scale) * crf(t) * eq_national_gen.m(t) ; - -*========================= -* [i,v,r,t]-level capital expenditures (for retail rate calculations) -*========================= - -capex_ivrt(i,v,r,t)$valcap(i,v,r,t) = - INV.l(i,v,r,t) * (cost_cap_fin_mult_no_credits(i,r,t) * cost_cap(i,t) ) - + INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult_no_credits(i,r,t) * cost_cap_energy(i,t) ) - + sum{(rscbin)$m_rscfeas(r,i,rscbin),INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * cost_cap_fin_mult_no_credits(i,r,t) } - + (INV_REFURB.l(i,v,r,t) * (cost_cap_fin_mult_no_credits(i,r,t) * cost_cap(i,t)))$[refurbtech(i)$Sw_Refurb] - + UPGRADES.l(i,v,r,t) * (cost_upgrade(i,v,r,t) * cost_cap_fin_mult_no_credits(i,r,t))$[upgrade(i)$Sw_Upgrades] ; - -*========================= -* Tech|BA-Level SYSTEM COST: Capital -*========================= - -* REPLICATION OF THE OBJECTIVE FUNCTION -* DOES NOT INCLUDE COSTS NOT INDEXED BY TECH (e.g., TRANSMISSION) - -systemcost_techba("inv_investment_capacity_costs",i,r,t)$tmodel_new(t) = -*investment costs (without the subtraction of any ITC/PTC value) - sum{v$valinv(i,v,r,t), - INV.l(i,v,r,t) * (cost_cap_fin_mult_noITC(i,r,t) * cost_cap(i,t) ) } -*plus investment energy costs (without the subtraction of any ITC/PTC value) - + sum{v$[valinv(i,v,r,t)$battery(i)], - INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult_noITC(i,r,t) * cost_cap_energy(i,t) ) } -*plus supply curve adjustment to capital cost (separated in outputs but part of m_rsc_dat(r,i,rscbin,"cost")) - + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$[not sccapcosttech(i)]$(not spur_techs(i))], - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_cap") * rsc_fin_mult_noITC(i,r,t) } -* Plus geo, hydro, and pumped-hydro techs, where costs are in the supply curves -*(Note that this deviates from the objective function structure) - + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$sccapcosttech(i)], - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult_noITC(i,r,t) } -*plus cost of upgrades - + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], - cost_upgrade(i,v,r,t) * cost_cap_fin_mult_noITC(i,r,t) * UPGRADES.l(i,v,r,t) } -*cost of capacity upsizing - + sum{(v,rscbin)$allow_cap_up(i,v,r,rscbin,t), - cost_cap_fin_mult_noITC(i,r,t) * INV_CAP_UP.l(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } -*cost of energy upsizing - + sum{(v,rscbin)$allow_ener_up(i,v,r,rscbin,t), - cost_cap_fin_mult_noITC(i,r,t) * INV_ENER_UP.l(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } -; - -systemcost_techba("inv_investment_spurline_costs_rsc_technologies",i,r,t)$tmodel_new(t) = -*costs of rsc spur line investment -*Note that cost_cap for hydro, pumped-hydro, and geo techs are zero -*but hydro and geo rsc_fin_mult is equal to the same value as cost_cap_fin_mult -*(Note that exclusions of geo and hydro here deviates from the objective function structure) - sum{(v,rscbin) - $[m_rscfeas(r,i,rscbin) - $valinv(i,v,r,t) - $rsc_i(i) - $[not sccapcosttech(i)] - $(not spur_techs(i)) - ], -*investment in resource supply curve technologies - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_trans") * rsc_fin_mult_noITC(i,r,t) } -; - -systemcost_techba("inv_itc_payments_negative",i,r,t)$tmodel_new(t) = -*investment costs (including reduction from ITC) - sum{v$valinv(i,v,r,t), - INV.l(i,v,r,t) * (cost_cap_fin_mult_out(i,r,t) * cost_cap(i,t) ) } -*energy investment costs (including reduction from ITC) - + sum{v$[valinv(i,v,r,t)$battery(i)], - INV_ENERGY.l(i,v,r,t) * (cost_cap_fin_mult_out(i,r,t) * cost_cap_energy(i,t) ) } -*plus supply curve adjustment to capital cost (separated in outputs but part of m_rsc_dat(r,i,rscbin,"cost")) - + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$[not sccapcosttech(i)]$(not spur_techs(i))], - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_cap") * rsc_fin_mult(i,r,t) } -* Plus geo, hydro, and pumped-hydro techs, where costs are in the supply curves -*(Note that this deviates from the objective function structure) - + sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$sccapcosttech(i)], - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost") * rsc_fin_mult(i,r,t) } -*plus cost of upgrades - + sum{v$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], - cost_upgrade(i,v,r,t) * cost_cap_fin_mult_out(i,r,t) * UPGRADES.l(i,v,r,t) } -*cost of capacity upsizing - + sum{(v,rscbin)$allow_cap_up(i,v,r,rscbin,t), - cost_cap_fin_mult_out(i,r,t) * INV_CAP_UP.l(i,v,r,rscbin,t) * cost_cap_up(i,v,r,rscbin,t) } -*cost of energy upsizing - + sum{(v,rscbin)$allow_ener_up(i,v,r,rscbin,t), - cost_cap_fin_mult_out(i,r,t) * INV_ENER_UP.l(i,v,r,rscbin,t) * cost_ener_up(i,v,r,rscbin,t) } -*minus capacity costs without ITC - - systemcost_techba("inv_investment_capacity_costs",i,r,t) -*plus supply curve transmission costs (including cost reductions from the ITC for applicable techs) - +sum{(v,rscbin)$[m_rscfeas(r,i,rscbin)$valinv(i,v,r,t)$rsc_i(i)$[not sccapcosttech(i)]$(not spur_techs(i))], - INV_RSC.l(i,v,r,rscbin,t) * m_rsc_dat(r,i,rscbin,"cost_trans") * rsc_fin_mult(i,r,t) } -*minus rsc transmission costs without ITC - - systemcost_techba("inv_investment_spurline_costs_rsc_technologies",i,r,t) -; - -*assign consume techs to their own category and then zero it out -systemcost_techba("inv_dac",i,r,t)$[tmodel_new(t)$dac(i)] = systemcost_techba("inv_investment_capacity_costs",i,r,t) ; -systemcost_techba("inv_h2_production",i,r,t)$[tmodel_new(t)$h2(i)] = systemcost_techba("inv_investment_capacity_costs",i,r,t) ; -systemcost_techba("inv_investment_capacity_costs",i,r,t)$[tmodel_new(t)$consume(i)] = 0 ; - -systemcost_techba("inv_investment_refurbishment_capacity",i,r,t)$tmodel_new(t) = -*costs of refurbishments of RSC tech (without the subtraction of any ITC/PTC value) - + sum{v$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], - (cost_cap_fin_mult_noITC(i,r,t) * cost_cap(i,t)) * INV_REFURB.l(i,v,r,t) } -; - -systemcost_techba("inv_itc_payments_negative_refurbishments",i,r,t)$tmodel_new(t) = -*costs of refurbishments of RSC tech (including reduction from ITC) - + sum{v$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], - (cost_cap_fin_mult_out(i,r,t) * cost_cap(i,t)) * INV_REFURB.l(i,v,r,t) } -*minus capacity costs without ITC - - systemcost_techba("inv_investment_refurbishment_capacity",i,r,t) -; - -systemcost_techba("inv_investment_water_access",i,r,t)$tmodel_new(t) = -*cost of water access - + (8760/1E6) * sum{ (v,w)$[i_w(i,w)$valinv(i,v,r,t)], sum{wst$i_wst(i,wst), m_watsc_dat(wst,"cost",r,t)} * water_rate(i,w) * - ( INV.l(i,v,r,t) + INV_REFURB.l(i,v,r,t)$[refurbtech(i)$Sw_Refurb] ) } - + sum{(rscbin,v)$[m_rscfeas(r,i,rscbin)$psh(i)], sum{wst$i_wst(i,wst), m_watsc_dat(wst,"cost",r,t) } * - ( INV_RSC.l(i,v,r,rscbin,t) * water_req_psh(r,rscbin) ) } -; - -*=============== -* Tech|BA-Level SYSTEM COST: Operational (the op_ prefix is used by the retail rate module to identify which costs are operational costs) -*=============== - -* DOES NOT INCLUDE COSTS NOT INDEXED BY TECH (e.g., ACP COMPLIANCE) - -systemcost_techba("op_vom_costs",i,r,t)$tmodel_new(t) = -*variable O&M costs - sum{(v,h)$[valgen(i,v,r,t)$cost_vom(i,v,r,t)], - hours(h) * cost_vom(i,v,r,t) * GEN.l(i,v,r,h,t) } - -* include production costs from production technologies - + sum{(p,v,h)$[(h2(i) or dac(i))$valcap(i,v,r,t)$i_p(i,p)], - hours(h) * cost_prod(i,v,r,t) * PRODUCE.l(p,i,v,r,h,t) }$Sw_Prod -; - -systemcost_techba("op_consume_vom",i,r,t)$[tmodel_new(t)$consume(i)] = systemcost_techba("op_vom_costs",i,r,t)$tmodel_new(t) ; -systemcost_techba("op_vom_costs",i,r,t)$[tmodel_new(t)$consume(i)] = 0 ; - -systemcost_techba("op_fom_costs",i,r,t)$tmodel_new(t) = -*fixed O&M costs for generation capacity - + sum{v$[valcap(i,v,r,t)$((not one_newv(i)) or retiretech(i,v,r,t))], - cost_fom(i,v,r,t) * cap_ivrt(i,v,r,t) * ilr(i) } -*for technologies with only one newv that are not allowed to retire, -*use the investments rather than the capacity to calculate FOM costs - + sum{(v,tt)$[inv_cond(i,v,r,t,tt)$one_newv(i)$(not retiretech(i,v,r,tt))], - INV.l(i,v,r,tt) * cost_fom(i,v,r,tt) * ilr(i) } - + sum{(v,tt)$[inv_cond(i,v,r,t,tt)$one_newv(i)$(not retiretech(i,v,r,tt))], - INV_ENERGY.l(i,v,r,tt) * cost_fom_energy(i,v,r,tt) * ilr(i) } -; - -systemcost_techba("op_consume_fom",i,r,t)$[tmodel_new(t)$consume(i)] = systemcost_techba("op_fom_costs",i,r,t)$tmodel_new(t) ; -systemcost_techba("op_fom_costs",i,r,t)$[tmodel_new(t)$consume(i)] = 0 ; - -systemcost_techba("op_operating_reserve_costs",i,r,t)$tmodel_new(t) = -*operating reserve costs - + sum{(v,h,ortype)$[valgen(i,v,r,t)$cost_opres(i,ortype,t)], - hours(h) * cost_opres(i,ortype,t) * OpRes.l(ortype,i,v,r,h,t) } -; - -systemcost_techba("op_fuelcosts_objfn",i,r,t)$tmodel_new(t) = -*cost of coal and nuclear fuel (except coal used for cofiring) - + sum{(v,h)$[valgen(i,v,r,t)$heat_rate(i,v,r,t) - $(not gas(i))$(not bio(i))$(not cofire(i)) - $((not h2_combustion(i)) or h2_combustion(i)$[(Sw_H2=0) or h_stress(h)])], - hours(h) * heat_rate(i,v,r,t) * fuel_price(i,r,t) * GEN.l(i,v,r,h,t) } - -*cofire coal consumption - cofire bio consumption already accounted for in accounting of BIOUSED - + sum{(v,h)$[valgen(i,v,r,t)$cofire(i)$heat_rate(i,v,r,t)], - (1-bio_cofire_perc) * hours(h) * heat_rate(i,v,r,t) - * fuel_price("coal-new",r,t) * GEN.l(i,v,r,h,t) } - -*cost of natural gas fuel - + sum{cendiv$r_cendiv(r,cendiv), gascost_cendiv(cendiv,t) * gasshare_techba(i,r,cendiv,t) } - -*cost biofuel consumption by the tech in the BA - + bioshare_techba(i,r,t) * sum{bioclass, BIOUSED.l(bioclass,r,t) * - (sum{usda_region$r_usda(r,usda_region), biosupply(usda_region, bioclass, "price") } + bio_transport_cost) } - -; - -systemcost_techba("op_emissions_taxes",i,r,t)$tmodel_new(t) = -*plus any taxes on emissions - sum{(e,v,h)$[valgen(i,v,r,t)], - hours(h) * (emit_rate("process",e,i,v,r,t) + emit_rate("upstream",e,i,v,r,t)$Sw_Upstream) * GEN.l(i,v,r,h,t) * emit_tax(e,r,t) } -; - -systemcost_techba("op_h2_fuel_costs",i,r,t)$tmodel_new(t) = -* H2 production costs - sum{(v,h,p)$[h2(i)$valcap(i,v,r,t)], - hours(h) * h2_fuel_cost(i,v,r,t) * PRODUCE.l(p,i,v,r,h,t) } -; - -systemcost_techba("op_h2combustion_fuel_costs",i,r,t)$[tmodel_new(t)$h2_combustion(i)$Sw_H2] = -* fuel costs for H2-CT/CC techs - + (1 / cost_scale) * (1 / pvf_onm(t)) -* when using national demand, calculate total annual demand and multiply by national average price -* [MW] * [hours] * [MMBTU/MWh] * [metric tons/MMBTU] * [$/metric ton] = [$] - * ( (sum{(v,h), GEN.l(i,v,r,h,t) * hours(h) * heat_rate(i,v,r,t) * h2_combustion_intensity } - * eq_h2_demand.m("H2",t) - )$[Sw_H2 = 1] -* when using regional demand by hour, apply price to each hour and then sum total costs -* [MW] * [hours] * [MMBTU/MWh] * [metric tons/MMBTU] * [$/[metric tons/hour]] / [hours] = [$] - + (sum{(v,h), GEN.l(i,v,r,h,t) * hours(h) * heat_rate(i,v,r,t) * h2_combustion_intensity - * eq_h2_demand_regional.m(r,h,t) / hours(h) } - )$[Sw_H2 = 2] - ) -; - -systemcost_techba("op_h2_vom",i,r,t)$tmodel_new(t) = -* vom costs from H2 production - sum{(v,h,p)$[h2(i)$valcap(i,v,r,t)], - hours(h) * h2_vom(i,t) * PRODUCE.l(p,i,v,r,h,t) } -; - -* transport and storage cost of captured CO2 -systemcost_techba("op_co2_transport_storage",i,r,t)$[tmodel_new(t)$(not Sw_CO2_Detail)] = - emit_captured_irt(i,r,t) * Sw_CO2_Storage -; - -systemcost_techba("op_co2_incentive_negative",i,r,t)$tmodel_new(t) = - - sum{(v,h)$[valgen(i,v,r,t)$co2_captured_incentive(i,v,r,t)], - (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * capture_rate("CO2",i,v,r,t) * GEN.l(i,v,r,h,t)} - - - sum{(p,v,h)$[dac(i)$valcap(i,v,r,t)], - (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * PRODUCE.l(p,i,v,r,h,t)} -; - -* PTC for generation -systemcost_techba('op_ptc_payments_negative',i,r,t)$tmodel_new(t) = - - sum{(v,h)$[valgen(i,v,r,t)$ptc_value_scaled(i,v,t)], - hours(h) * ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t) * GEN.l(i,v,r,h,t) } -; - -* PTC value for hydrogen production -* Note: all electrolyzers which produce H2 are assuming to be receiving hydrogen production credits during eligible years -systemcost_techba('op_h2_ptc_payments_negative','electrolyzer',r,t)$[tmodel_new(t)] = - - (sum{(p,v,h)$[valcap("electrolyzer",v,r,t)$(sameas(p,"H2"))$h2_ptc("electrolyzer",v,r,t)$h_rep(h)], - hours(h) * PRODUCE.l(p,"electrolyzer",v,r,h,t) * - (crf(t) / crf_h2_incentive(t)) * h2_ptc("electrolyzer",v,r,t) * 1e3} ) - $[Sw_H2_PTC$Sw_H2$h2_ptc_years(t)$(yeart(t) >= h2_demand_start)] -; - -* Startup/ramping costs -systemcost_techba('op_startcost',i,r,t)$[tmodel_new(t)$Sw_StartCost$startcost(i)] = - sum{(h,hh)$[numhours_nexth(h,hh)$valgen_irt(i,r,t)], - startcost(i) * numhours_nexth(h,hh) * RAMPUP.l(i,r,h,hh,t) } -; - - -*For bulk system costs present value as of model year, capital costs are unchanged, -*while operation costs use pvf_onm_undisc -systemcost_techba_bulk(sys_costs,i,r,t) = systemcost_techba(sys_costs,i,r,t) ; -systemcost_techba_bulk(sys_costs_op,i,r,t) = systemcost_techba(sys_costs_op,i,r,t) * pvf_onm_undisc(t) ; - -systemcost_techba_bulk_ew(sys_costs,i,r,t) = systemcost_techba_bulk(sys_costs,i,r,t) ; -systemcost_techba_bulk_ew(sys_costs_op,i,r,t)$tlast(t) = systemcost_techba(sys_costs_op,i,r,t) ; - -* Sum across technologies to get BA-level costs for all applicable categories -systemcost_ba(sys_costs,r,t) = sum{i,systemcost_techba(sys_costs,i,r,t)} ; - -*========================= -* BA-Level SYSTEM COST: Capital -*========================= - -* REPLICATION OF THE OBJECTIVE FUNCTION - -* Interzonal transmission: Split costs between the two connected zones -* DC: INVTRAN is defined (and is equal) in both directions, so just include (r,rr) and divide by 2 -systemcost_ba("inv_transmission_interzone_dc_investment",r,t)$tmodel_new(t) = - sum{(rr,trtype)$[routes_inv(r,rr,trtype,t)$(not aclike(trtype))], - trans_cost_cap_fin_mult(t) - * transmission_cost_nonac(r,rr,trtype) - * INVTRAN.l(r,rr,trtype,t) - / 2 } -; - -* AC: TRAN_CAPEX_BINS is only defined for r < rr, so add (r,rr) + (rr,r) and divide by 2 -* First get the cumulative investment cost, split across zones -parameter capex_transmission_interzone_ac(r,t) "Cumulative interzonal AC transmission capex" ; -capex_transmission_interzone_ac(r,t)$tmodel_new(t) = - sum{(rr,tscbin)$[routes_inv(r,rr,"AC",t)$tsc_binwidth(r,rr,tscbin)], - trans_cost_cap_fin_mult(t) * TRAN_CAPEX_BINS.l(r,rr,tscbin,t) / 2 } - + sum{(rr,tscbin)$[routes_inv(rr,r,"AC",t)$tsc_binwidth(rr,r,tscbin)], - trans_cost_cap_fin_mult(t) * TRAN_CAPEX_BINS.l(rr,r,tscbin,t) / 2 } -; -* Loop over each year and keep the capex difference to get model-year investment -loop(t$[tmodel_new(t)$(not tfirst(t))], - systemcost_ba("inv_transmission_interzone_ac_investment",r,t) - = - capex_transmission_interzone_ac(r,t) - - sum{tt$tprev(t, tt), - capex_transmission_interzone_ac(r,tt) - } ; -) ; - -systemcost_ba("inv_transmission_intrazone_investment",r,t)$[tmodel_new(t)$Sw_TransIntraCost] = -* cost of intra-zone network reinforcement - trans_cost_cap_fin_mult(t) * Sw_TransIntraCost * 1000 * INV_POI.l(r,t) -; - -systemcost_ba("op_transmission_fom",r,t)$tmodel_new(t) = -*fixed O&M costs for transmission lines - sum{(rr,trtype)$routes(r,rr,trtype,t), - transmission_line_fom(r,rr,trtype) * CAPTRAN_ENERGY.l(r,rr,trtype,t) } -*fixed O&M costs for LCC AC/DC converters - + sum{(rr,trtype)$[lcclike(trtype)$routes(r,rr,trtype,t)], - cost_acdc_lcc * 2 * trans_fom_frac * CAPTRAN_ENERGY.l(r,rr,trtype,t) } -*fixed O&M costs for VSC AC/DC converters - + cost_acdc_vsc * trans_fom_frac * CAP_CONVERTER.l(r,t) -; - -systemcost_ba("op_transmission_intrazone_fom",r,t)$[tmodel_new(t)$Sw_TransIntraCost] = -* FOM cost for intra-zone network reinforcement - Sw_TransIntraCost * 1000 * trans_fom_frac - * sum{tt$[(yeart(tt)<=yeart(t))$(tmodel(tt) or tfix(tt))], INV_POI.l(r,tt) } -; - -systemcost_ba("inv_converter_costs",r,t)$tmodel_new(t) = -* LCC and B2B AC/DC converter stations: each interface has two, one on either side of the interface, -* but each interface shows up in both INVTRAN(r,rr) and INVTRAN(rr,r) so don't multiply by 2 - sum{(rr,trtype)$[lcclike(trtype)$routes_inv(r,rr,trtype,t)], - trans_cost_cap_fin_mult(t) * cost_acdc_lcc * INVTRAN.l(r,rr,trtype,t) } -* VSC AC/DC converter stations - + trans_cost_cap_fin_mult(t) * cost_acdc_vsc * INV_CONVERTER.l(r,t) -; - -systemcost_ba("inv_spurline_investment",r,t)$[tmodel_new(t)$Sw_SpurScen] = -* capital cost of spur lines modeled explicitly - sum{x$[xfeas(x)$x_r(x,r)], spurline_cost(x) * Sw_SpurCostMult * INV_SPUR.l(x,t) } -; - -systemcost_ba("op_spurline_fom",r,t)$tmodel_new(t) = -* fixed O&M cost of spur lines modeled explicitly - sum{x$[Sw_SpurScen$xfeas(x)$x_r(x,r)], spurline_cost(x) * trans_fom_frac * CAP_SPUR.l(x,t) } -* fixed O&M cost of spur lines modeled as part of supply curve - + sum{(i,v,rscbin) - $[m_rscfeas(r,i,rscbin)$valcap(i,v,r,t) - $rsc_i(i)$(not spur_techs(i))$(not sccapcosttech(i))], - m_rsc_dat(r,i,rscbin,"cost_trans") * trans_fom_frac * CAP_RSC.l(i,v,r,rscbin,t) - } -; - -systemcost_ba("inv_co2_network_pipe",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = -*costs of co2 trunk pipeline investment (cost_co2_pipeline_cap already includes distance; see b_inputs) - + sum{rr$co2_routes(r,rr), cost_co2_pipeline_cap(r,rr,t) * - ( (CO2_TRANSPORT_INV.l(r,rr,t) + CO2_TRANSPORT_INV.l(rr,r,t) ) / 2 ) } -; - -systemcost_ba("inv_co2_network_spur",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = -*costs of co2 spurline investment (cost_co2_spurline_cap already includes distance; see b_inputs) - + sum{cs$r_cs(r,cs), cost_co2_spurline_cap(r,cs,t) * CO2_SPURLINE_INV.l(r,cs,t) } -; - -systemcost_ba("inv_h2_pipeline",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = -* H2 transport network investment costs (investments defined only for r < rr) - sum{rr$h2_routes_inv(r,rr), - cost_h2_transport_cap(r,rr,t) * H2_TRANSPORT_INV.l(r,rr,t) } -; - -systemcost_ba("inv_h2_storage",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = -* H2 storage investment costs - + sum{h2_stor$h2_stor_r(h2_stor,r), cost_h2_storage_cap(h2_stor,t) * H2_STOR_INV.l(h2_stor,r,t) } -; - -*=============== -* BA-Level SYSTEM COST: Operational (the op_ prefix is used by the retail rate module to identify which costs are operational costs) -*=============== - -systemcost_ba("op_co2_storage",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = - + sum{(h,cs)$r_cs(r,cs), hours(h) * CO2_STORED.l(r,cs,h,t) * cost_co2_stor_bec(cs,t) } -; - -* here following same logic of transmission pipelines -systemcost_ba("op_co2_network_fom_pipe",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = - sum{(rr)$[co2_routes(r,rr)], cost_co2_pipeline_fom(r,rr,t) - * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], - (CO2_TRANSPORT_INV.l(r,rr,tt) + CO2_TRANSPORT_INV.l(rr,r,tt) ) / 2 } - }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] -; - -systemcost_ba("op_co2_network_fom_spur",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = - sum{(cs)$r_cs(r,cs), cost_co2_spurline_fom(r,cs,t) - * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], - CO2_SPURLINE_INV.l(r,cs,tt) } - }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] -; - -systemcost_ba("op_co2_network_fom_spur",r,t)$[tmodel_new(t)$Sw_CO2_Detail] = - sum{(cs)$r_cs(r,cs), cost_co2_spurline_fom(r,cs,t) - * sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], - CO2_SPURLINE_INV.l(r,cs,tt) } - }$[Sw_CO2_Detail$(yeart(t)>=co2_detail_startyr)] -; - -* same for H2 pipelines and storage -systemcost_ba("op_h2_transport",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = - sum{rr$h2_routes_inv(r,rr), - cost_h2_transport_fom(r,rr,t) * - sum{tt$[(tfix(tt) or tmodel(tt))$(yeart(tt)<=yeart(t))], - H2_TRANSPORT_INV.l(r,rr,t) } } -; - -systemcost_ba("op_h2_transport_intrareg",r,t)$[tmodel_new(t)$Sw_H2] = -* H2 transport and storage intra-regional investment costs - sum{(i,v,h)$[valcap(i,v,r,t)$newv(v)$i_p(i,"H2")], - hours(h) * PRODUCE.l("H2",i,v,r,h,t) * (Sw_H2_IntraReg_Transport * 1e3) } -; - -systemcost_ba("op_h2_storage",r,t)$[tmodel_new(t)$(Sw_H2 = 2)] = - sum{h2_stor$h2_stor_r(h2_stor,r), - cost_h2_storage_fom(h2_stor,t) * H2_STOR_CAP.l(h2_stor,r,t) } -; - -systemcost_ba("op_acp_compliance_costs",r,t)$[tmodel_new(t)$(yeart(t)>=firstyear_RPS)] = -*plus ACP purchase costs, attributed to bas based on fraction of state requirement - + sum{(st,RPSCat) - $[stfeas(st)$r_st(r,st)$RecPerc(RPSCat,st,t) - $sum{rr$r_st(rr,st), reqt_quant('state_rps',RPSCat,rr,'ann',t) }], - acp_price(st,t) * ACP_PURCHASES.l(RPSCat,st,t) * reqt_quant('state_rps',RPSCat,r,'ann',t) - / sum{rr$r_st(rr,st), reqt_quant('state_rps',RPSCat,rr,'ann',t) } - } -* spread voluntary purchase costs based on BA load frac - + sum{RPSCat$RecPerc(RPSCat,"voluntary",t), acp_price("voluntary",t) * ACP_PURCHASES.l(RPSCat,"voluntary",t) } - * load_frac_rt(r,t) - -; - -systemcost_ba("op_co2_incentive_negative",r,t)$tmodel_new(t) = - - sum{(i,v,h)$[valgen(i,v,r,t)$co2_captured_incentive(i,v,r,t)], - (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * capture_rate("CO2",i,v,r,t) * GEN.l(i,v,r,h,t)} - - - sum{(i,p,v,h)$[dac(i)$valcap(i,v,r,t)], - (crf(t) / crf_co2_incentive(t)) * co2_captured_incentive(i,v,r,t) * hours(h) * PRODUCE.l(p,i,v,r,h,t)} -; - - -*If the op_h2combustion_fuel_costs are included in the systemcost_ba it will lead to double-counting -*but these fuel costs are needed for the retail rate module. We therefore zero them out here. -systemcost_ba_retailrate(sys_costs,r,t) = systemcost_ba(sys_costs,r,t) ; -systemcost_ba("op_h2combustion_fuel_costs",r,t) = 0 ; - -*For bulk system costs present value as of model year, capital costs are unchanged, -*while operation costs use pvf_onm_undisc -systemcost_ba_bulk(sys_costs,r,t) = systemcost_ba(sys_costs,r,t) ; -systemcost_ba_bulk(sys_costs_op,r,t) = systemcost_ba(sys_costs_op,r,t) * pvf_onm_undisc(t) ; - -systemcost_ba_bulk_ew(sys_costs,r,t) = systemcost_ba_bulk(sys_costs,r,t) ; -systemcost_ba_bulk_ew(sys_costs_op,r,t)$tlast(t) = systemcost_ba(sys_costs_op,r,t) ; - - -*========================= -* National System Cost -*========================= - -systemcost(sys_costs,t) = sum{r, systemcost_ba(sys_costs,r,t) } ; -systemcost_bulk(sys_costs,t) = systemcost(sys_costs,t) ; -systemcost_bulk(sys_costs_op,t) = systemcost(sys_costs_op,t) * pvf_onm_undisc(t) ; - -systemcost_bulk_ew(sys_costs,t) = systemcost_bulk(sys_costs,t) ; -systemcost_bulk_ew(sys_costs_op,t)$tlast(t) = systemcost(sys_costs_op,t) ; - -* Federal tax expenditure calculation -tax_expenditure_itc(t) = systemcost("inv_itc_payments_negative",t) - + systemcost("inv_itc_payments_negative_refurbishments",t) ; - -tax_expenditure_ptc(t) = systemcost("op_ptc_payments_negative",t) - + systemcost("op_co2_incentive_negative",t) ; - -raw_inv_cost(t) = sum{sys_costs_inv, systemcost(sys_costs_inv,t) } ; -raw_op_cost(t) = sum{sys_costs_op, systemcost(sys_costs_op,t) } ; - -*====================== -* Error Check -*====================== -* Objective function cost - reported system cost, adjusted for intentional differences -error_check('z') = ( - z.l - - sum{t$tmodel(t), -* Start with the system cost, then make adjustments for objective function values that are -* not intended to be in the system costs - cost_scale * (pvf_capital(t) * raw_inv_cost(t) + pvf_onm(t) * raw_op_cost(t)) -* Cost of growth penalties -* (Note: adjustments should have the same sign (+/-) as they do in the objective function) - + pvf_capital(t) * sum{(gbin,i,st)$[sum{r$[r_st(r,st)], valinv_irt(i,r,t) }$stfeas(st)], - cost_growth(i,st,t) * growth_penalty(gbin) * GROWTH_BIN.l(gbin,i,st,t) - * (yeart(t) - sum{tt$[tprev(t,tt)], yeart(tt) }) - }$[(yeart(t)>=model_builds_start_yr)$Sw_GrowthPenalties$(yeart(t)<=Sw_GrowthPenLastYear)] -* Small penalty to move storage into shorter duration bins - + pvf_capital(t) * sum{(i,v,r,ccseason,sdbin)$[valcap(i,v,r,t)$(storage(i) or hyd_add_pump(i))$(not csp(i))$Sw_PRM_CapCredit$Sw_StorageBinPenalty], - bin_penalty(sdbin) * CAP_SDBIN.l(i,v,r,ccseason,sdbin,t) } -* Retirement penalty - - pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$retiretech(i,v,r,t)$Sw_RetirePenalty], - cost_fom(i,v,r,t) * retire_penalty(t) - * (CAP.l(i,v,r,t) - INV.l(i,v,r,t) - INV_REFURB.l(i,v,r,t)$[refurbtech(i)$Sw_Refurb] - UPGRADES.l(i,v,r,t)$[upgrade(i)$Sw_Upgrades]) } -* Revenue from purchases of curtailed VRE - - pvf_onm(t) * sum{(r,h), CURT.l(r,h,t) * hours(h) * cost_curt(t) }$Sw_CurtMarket -* Hurdle costs - + pvf_onm(t) * sum{(r,rr,trtype)$cost_hurdle(r,rr,t), tran_hurdle_cost_ann(r,rr,trtype,t) } -* Penalty cost for dropped/excess load before Sw_StartMarkets - + pvf_onm(t) * sum{(r,h), (DROPPED.l(r,h,t) + EXCESS.l(r,h,t)) * hours(h) * cost_dropped_load } -* Retail adder for electricity consuming technologies - + pvf_onm(t) * sum{(p,i,v,r,h)$[valcap(i,v,r,t)$i_p(i,p)$h_rep(h)$Sw_RetailAdder$Sw_Prod], - hours(h) * Sw_RetailAdder * PRODUCE.l(p,i,v,r,h,t) / prod_conversion_rate(i,v,r,t) } -* Account for difference in fixed O&M between model (CAP.l(i,v,r,t)) -* and outputs (cap_ivrt(i,v,r,t) * ilr(i)) for techs with more than one newv - + pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$((not one_newv(i)) or retiretech(i,v,r,t))], - cost_fom(i,v,r,t) * (CAP.l(i,v,r,t) - cap_ivrt(i,v,r,t) * ilr(i)) } -* Account for difference in fixed O&M between model (CAP.l(i,v,r,t)) -* and outputs (based on INV.l) for techs with more only one newv that cannot retire - + pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$(one_newv(i))$(not retiretech(i,v,r,t))], - cost_fom(i,v,r,t) * CAP.l(i,v,r,t) - - sum{(tt)$[inv_cond(i,v,r,t,tt)$(not retiretech(i,v,r,tt))], - INV.l(i,v,r,tt) * cost_fom(i,v,r,tt) * ilr(i) } } -* Account for difference in fixed O&M between model (CAP_ENERGY.l(i,v,r,t)) -* and outputs (based on INV_ENERGY.l) for techs with more only one newv that cannot retire - + pvf_onm(t) * sum{(i,v,r)$[valcap(i,v,r,t)$(one_newv(i))$(not retiretech(i,v,r,t))], - cost_fom_energy(i,v,r,t) * CAP_ENERGY.l(i,v,r,t) - - sum{(tt)$[inv_cond(i,v,r,t,tt)$(not retiretech(i,v,r,tt))], - INV_ENERGY.l(i,v,r,tt) * cost_fom_energy(i,v,r,tt) * ilr(i) } } -* Objective function uses cumulative interzonal transmission capex but we report -* model-year investment, so subtract the difference between the two - - ( - sum{r, systemcost_ba("inv_transmission_interzone_ac_investment",r,t) } - - sum{r, capex_transmission_interzone_ac(r,t) } - ) -* Account for difference in capital costs of objective, which use cost_cap_fin_mult, -* and outputs, which use cost_cap_fin_mult_out - + pvf_capital(t) * ( - sum{(i,v,r)$[valinv(i,v,r,t)], - cost_cap(i,t) * INV.l(i,v,r,t) - * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } - - + sum{(i,v,r)$[valinv(i,v,r,t)$battery(i)], - cost_cap_energy(i,t) * INV_ENERGY.l(i,v,r,t) - * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } - - + sum{(i,v,r)$[upgrade(i)$valcap(i,v,r,t)$Sw_Upgrades], - cost_upgrade(i,v,r,t) * UPGRADES.l(i,v,r,t) - * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } - - + sum{(i,v,r,rscbin)$allow_cap_up(i,v,r,rscbin,t), - cost_cap_up(i,v,r,rscbin,t) * INV_CAP_UP.l(i,v,r,rscbin,t) - * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } - - + sum{(i,v,r,rscbin)$allow_ener_up(i,v,r,rscbin,t), - cost_ener_up(i,v,r,rscbin,t) * INV_ENER_UP.l(i,v,r,rscbin,t) - * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } - - + sum{(i,v,r)$[Sw_Refurb$valinv(i,v,r,t)$refurbtech(i)], - cost_cap(i,t) * INV_REFURB.l(i,v,r,t) - * (cost_cap_fin_mult(i,r,t) - cost_cap_fin_mult_out(i,r,t)) } - ) -* account for penalty paid to deploy capacity beyond interconnection queue limits - + sum{(tg,r), cap_penalty(tg) * CAP_ABOVE_LIM.l(tg,r,t) } - } -) / z.l ; - -*Round error_check for z because of small number differences that always show up due to machine rounding and tolerances -error_check('z') = round(error_check('z'), 6) ; - -* Check to see is any generation or capacity from dissallowed resources -error_check("gen") = sum{(i,v,r,allh,t)$[not valgen(i,v,r,t)], GEN.l(i,v,r,allh,t) } ; -error_gen(i,v,r,allh,t)$[not valgen(i,v,r,t)] = GEN.l(i,v,r,allh,t) ; -error_check("cap") = sum{(i,v,r,t)$[not valcap(i,v,r,t)], CAP.l(i,v,r,t) } ; -error_check("RPS") = sum{(RPSCat,i,st,ast,t)$[(not RecMap(i,RPSCat,st,ast,t))$[(not stfeas(ast)) or not sameas(ast,"voluntary")]], RECS.l(RPSCat,i,st,ast,t) } ; -error_check("OpRes") = sum{(ortype,i,v,r,h,t)$[not valgen(i,v,r,t)], OPRES.l(ortype,i,v,r,h,t) } ; -error_check("m_rsc_dat") = sum{(r,i,rscbin)$m_rsc_dat(r,i,rscbin,"cap"), m_rsc_dat_init(r,i,rscbin) - m_rsc_dat(r,i,rscbin,"cap") } ; - -* Check to make sure there's no dropped/excess load in or after Sw_StartMarkets -error_check("dropped") = sum{(r,h,t)$[yeart(t)>=Sw_StartMarkets], DROPPED.l(r,h,t) } ; -error_check("excess") = sum{(r,h,t)$[yeart(t)>=Sw_StartMarkets], EXCESS.l(r,h,t) } ; - -* Report DROPPED and EXCESS variable levels -dropped_load(r,h,t) = DROPPED.l(r,h,t) ; -excess_load(r,h,t) = EXCESS.l(r,h,t) ; - -*====================== -* Transmission -*====================== - -invtran_out(r,rr,trtype,t)$routes_inv(r,rr,trtype,t) = INVTRAN.l(r,rr,trtype,t) ; - -tran_cap_energy(r,rr,trtype,t)$routes(r,rr,trtype,t) = CAPTRAN_ENERGY.l(r,rr,trtype,t) ; -tran_cap_prm(r,rr,trtype,t)$routes(r,rr,trtype,t) = CAPTRAN_PRM.l(r,rr,trtype,t) ; -tran_cap_grp(transgrp,transgrpp,t)$trancap_init_transgroup(transgrp,transgrpp,"AC") - = CAPTRAN_GRP.l(transgrp,transgrpp,t) ; - -tran_out(r,rr,trtype,t)$[(ord(r)2}'.format) - return dfout_month - - -def postprocess_outputs(case, outputs_path=None, verbose=0): - ## Parse inputs - _outputs_path = os.path.join(case, 'outputs') if outputs_path is None else outputs_path - - ## System cost - reeds.output_calc.calc_systemcost(case).to_csv( - os.path.join(_outputs_path, 'post_systemcost_annualized.csv'), - index=False, - ) - - ## Reinforcement and spur-line - reeds.output_calc.calc_reinforcement_spur_capacity_miles(case).to_csv( - os.path.join(_outputs_path, 'post_tech_transmission.csv'), - index=False, - ) - - ## Hydrogen prices by month - sw = reeds.io.get_switches(case) - try: - dfin_timestamp = reeds.timeseries.timeslice_to_timestamp(case, 'h2_price_h') - dfout_month = timestamp_to_month(dfin_timestamp) - dfout_month.rename(columns={'Value':'$2004/kg'}).to_csv( - os.path.join(_outputs_path, 'h2_price_month.csv'), index=False) - except Exception: - if int(sw.GSw_H2): - print(traceback.format_exc()) - - -#%% Procedure -if __name__ == '__main__' and not hasattr(sys, 'ps1'): - tic = datetime.datetime.now() - # parse arguments - parser = argparse.ArgumentParser( - description="Convert ReEDS run results from gdx to specified filetype" - ) - parser.add_argument("case", help="ReEDS scenario name") - parser.add_argument('--csv', '-c', action='store_true', help='write csv files') - parser.add_argument('--xlsx', '-x', action='store_true', help='write xlsx file') - - args = parser.parse_args() - case = args.case - write_csv = args.csv - write_xlsx = args.xlsx - - # #%% Inputs for debugging - # case = os.path.join(reeds_path, 'runs', 'v20250312_scheduledM0_Pacific') - # write_csv = False - # write_xlsx = False - - #%% Set up logger - reeds_path = os.path.abspath(os.path.dirname(__file__)) - log = reeds.log.makelog(scriptname=__file__, logpath=os.path.join(case, "gamslog.txt")) - - print("Starting e_report_dump.py") - - # %%### Parse inputs and get switches - outputs_path = os.path.join(case, "outputs") - - ### Get switches - sw = reeds.io.get_switches(case) - - ### Get new file names if applicable - dfparams = pd.read_csv( - os.path.join(case, "e_report_params.csv"), - comment="#", - index_col="param", - ) - rename = dfparams.loc[~dfparams.output_rename.isnull(), "output_rename"].to_dict() - ## drop the indices - rename = {k.split("(")[0]: v for k, v in rename.items()} - print(f"renamed parameters: {rename}") - - # %%### Write results for each gdx file - ### outputs gdx - print("Loading outputs gdx") - dict_out = gdxpds.to_dataframes( - os.path.join(outputs_path, f"rep_{os.path.basename(case)}.gdx") - ) - print("Finished loading outputs gdx") - - write_dfdict( - dfdict=dict_out, - outputs_path=outputs_path, - write_csv=write_csv, - write_xlsx=write_xlsx, - rename=rename, - ) - - ### powerfrac results - if int(sw.GSw_calc_powfrac): - print("Loading powerfrac gdx") - dict_powerfrac = gdxpds.to_dataframes( - os.path.join(outputs_path, f"rep_powerfrac_{os.path.basename(case)}.gdx") - ) - print("Finished loading powerfrac gdx") - - dfdict_to_csv( - dict_powerfrac, - outputs_path=outputs_path, - rename=rename, - ) - - - #%% Special handling of particular outputs - postprocess_outputs(case, outputs_path=outputs_path) - - - #%% All done - print("Completed e_report_dump.py") - try: - toc(tic=tic, year=0, path=case, process="e_report_dump.py") - except NameError: - print("reeds/log.py not found, so not logging output") diff --git a/e_report_params.csv b/e_report_params.csv deleted file mode 100644 index 70b37020..00000000 --- a/e_report_params.csv +++ /dev/null @@ -1,276 +0,0 @@ -# Full-line and line-end comments can be indicated with # (but use the comment column when possible),,,,, -# This parameter list is in alphabetical order - please add new entries that way,,,,, -param,units,comment,reeds2x,output_rename,input -"acp_purchases_out(rpscat,st,t)",MWh,Annual alternative compliance credits from the variable ACP_PURCHASES,,, -"avg_cf(i,v,r,t)",frac,Annual average capacity factor for rsc technologies,,, -"avg_avail(i,v,r)",frac,Annual average avail factor,,, -"bioused_out(bioclass,r,t)",dry tons (imperial),biomass used by class in each model region (-> bioused.csv),,, -"bioused_usda(bioclass,usda_region,t)",dry tons (imperial),biomass used by class in each USDA region,,, -"state_cap_and_trade_price(st,t)",$/metric ton,marginal from state annual CO2 cap constraints,,, -"state_cap_and_trade_quant(st,t)",metric tons,state annual CO2 cap constraints,,, -"cap_above_limit(tg,r,t)",MW,Near-term capacity deployed above interconnection queue limit,,, -"cap_avail(i,r,t,rscbin)",MW,Available capacity at beginning of model year for rsc techs,,, -"cap_exog(i,v,r,t)",MW,Exogenous capacity from m_capacity_exog; used by reeds_to_rev,,, -"cap_firm(i,r,ccseason,t)",MW,firm capacity that counts toward the reserve margin constraint by BA and season,,, -"cap_nat(i,t)",MW,national capacity,,, -"cap_new_cc(i,r,ccseason,t)",MW,"new capacity that is VRE, for new capacity credit calculation",,, -"cc_new(i,r,ccseason,t)",frac,capacity credit for new VRE techs,,, -"cap_converter_out(r,t)",MW,AC/DC converter capacity,1,, -"cap_ivrt(i,v,r,t)",MW,"undegraded power capacity by tech, year, region, and class",1,, -"cap_energy_ivrt(i,v,r,t)",MWh,"undegraded energy capacity by tech, year, region, and class",1,, -"cap_sdbin_out(i,r,ccseason,sdbin,t)",MW,binned storage capacity by year,,, -"cap_deg_ivrt(i,v,r,t)",MW,"Degraded capacity, equal to CAP.l",,, -"cap_new_ann(i,r,t)",MW/yr,new annual capacity by region,,, -"cap_new_ann_nat(i,t)",MW/yr,new annual capacity national,,, -"cap_new_bin_out(i,v,r,t,rscbin)",MW,capacity of built techs,,, -"cap_new_ivrt(i,v,r,t)",MW,new capacity,1,, -"cap_new_ivrt_refurb(i,v,r,t)",MW,new refurbished capacity,,, -"cap_new_out(i,r,t)",MW,"new capacity by region, which are investments and upgrades from one solve year to the next",,, -"cap_energy_new_out(i,v,r,t)",MWh,"new energy capacity by region, which are investments from one solve year to the next",,, -"cap_out(i,r,t)",MW,capacity by region,1,cap, -"cap_out_ivrt(i,v,r,t)",MW,capacity by region and vintage,,, -"capacity_offline(i,r,allh,t)",MW,capacity offline due to forced or scheduled outages,,, -"capex_ivrt(i,v,r,t)",$,"capital expenditure for new capacity, no ITC/depreciation/PTC reductions",,, -"cap_upgrade(i,r,t)",MW,upgraded capacity by region,,, -"cap_upgrade_ivrt(i,v,r,t)",MW,upgraded capacity by region and vintage,,, -"CO2_CAPTURED_out(r,allh,t)",metric tons/hour,amount of CO2 captured_out from DAC and CCS technologies,,, -"CO2_STORED_out(r,cs,allh,t)",metric tons/hour,amount of CO2 stored_out underground,,, -"CO2_CAPTURED_out_ann(r,t)",metric tons,amount of CO2 captured_out from DAC and CCS technologies,,, -"CO2_STORED_out_ann(r,cs,t)",metric tons,amount of CO2 stored_out underground,,, -"CO2_TRANSPORT_INV_out(r,rr,t)",metric ton-hours,investment in interregional CO2 transport_out capacity,,, -"CO2_SPURLINE_INV_out(r,cs,t)",metric ton-hours,investment in spur line CO2 transport capacity between BAs and reservoirs,,, -"CO2_FLOW_out(r,rr,allh,t)",metric tons/hour,gross interregional flow of CO2 by timeslice,,, -"CO2_FLOW_out_ann(r,rr,t)",metric tons,gross interregional flow of CO2,,, -"CO2_FLOW_pos_out(r,rr,allh,t)",metric tons/hour,positive interregional flow of CO2 from region r to rr by timeslice,,, -"CO2_FLOW_pos_out_ann(r,rr,t)",metric tons,positive interregional flow of CO2 from region r to rr,,, -"CO2_FLOW_neg_out(r,rr,allh,t)",metric tons/hour,negative interregional flow of CO2 from region r to rr by timeslice (reported as a negative value),,, -"CO2_FLOW_neg_out_ann(r,rr,t)",metric tons,negative interregional flow of CO2 from region r to rr (reported as a negative value),,, -"CO2_FLOW_net_out(r,rr,allh,t)",metric tons/hour,net interregional flow of CO2 from region r to rr by timeslice,,, -"CO2_FLOW_net_out_ann(r,rr,t)",metric tons,net interregional flow of CO2 from region r to rr,,, -co2_price(t),$/metric ton,marginal from national annual CO2 cap constraint (eq_annual_cap),1,, -"cost_cap_fin_mult(i,r,t)",frac,final capital cost multiplier for regions and technologies - used in the objective function,,, -"cost_vom_rr(i,v,rr,t)",$/MWh,"vom cost for all regions, including resource regions",,, -"curt_tech(i,r,t)",MWh,annual curtailment resolved by technology,,, -"curt_h(r,allh,t)",MW,curtailment from VRE generators,,, -"curt_ann(r,t)",MWh,annual curtailment from VRE generators by region,,, -curt_rate(t),frac,fraction of VRE generation that is curtailed,,, -"curt_rate_tech(i,r,t)",frac,annual curtailment resolved by technology,,, -"curt_rr(i,r,allh,t)",frac,"curtailment fraction for all regions, including resource region",,, -"gen_new_uncurt(i,r,allh,t)",MWh,uncurtailed generation from new VRE techs,,, -"curt_new(i,r,allh,t)",frac,curtailment frac for new VRE techs,,, -"cc_all_out(i,v,r,ccseason,t)",frac,combined cc_int and m_cc_mar output,,, -"dropped_load(r,allh,t)",MW,level values of DROPPED variable for dropped load,,, -"emit_captured_irt(i,r,t)",metric tons,CO2 emissions captured by tech and region,,, -"emit_captured_nat(i,t)",metric tons,"CO2 emissions captured, national",,, -"emit_irt(etype,eall,i,r,t)",metric tons,"emissions by pollutant, tech, and region",,, -"emit_nat(etype,eall,t)",metric tons,"emissions by pollutant, national",,, -"emit_nat_tech(etype,eall,i,t)",metric tons,"emissions by pollutant and tech, national",,, -"emit_r(etype,eall,r,t)",metric tons,"emissions by pollutant, regional",,, -"emit_rate_regional(r,t)",metric tons,"regional average CO2 emissions rate, used in state CO2 caps",,, -"emit_weighted(etype,eall)",metric tons * pvf,national emissions * pvf_onm,,, -error_check(*),unitless,set of checks to determine if there is an error - values should be zero if there is no error,,, -"error_gen(i,v,r,allh,t)",MWh,"erroneous generation that disobeys valgen(i,v,r,t)",,, -"excess_load(r,allh,t)",MW,level values of EXCESS variable for surplus load,,, -"expenditure_flow(*,r,rr,t)",2004$,expenditures from flows of * moving from r to rr,,, -"expenditure_flow_rps(st,ast,t)",2004$,expenditures from trades of RECS from st to ast,,, -"expenditure_flow_int(r,t)",2004$,expenditures from exogenous international imports/exports,,, -"flex_load_out(flex_type,r,allh,t)",MWh,flexible load consumed in each timeslice,,, -forced_outage(i),fraction,average forced outage rate (h-weighted; r-unweighted) by technology during representative periods,,, -planned_outage(i),fraction,average scheduled outage rate (h-weighted) by technology during representative periods,,, -"gen_ivrt_uncurt(i,v,r,t)",MWh,annual uncurtailed generation from VREs,,, -"gen_ivrt(i,v,r,t)",MWh,annual generation,,, -"gen_h(i,r,allh,t)",MW,generation by timeslice with charge and production load as negative generation,1,, -"gen_h_nat(i,allh,t)",MW,national generation by timeslice with charge and production load as negative generation,,, -"gen_h_stress(i,r,allh,t)",MW,generation by stress timeslice with charge and production load as negative generation,,, -"gen_h_stress_nat(i,allh,t)",MW,national generation by stress timeslice with charge and production load as negative generation,,, -"gen_ann(i,r,t)",MWh,annual generation with charge and production load as negative generation,,, -"gen_ann_nat(i,t)",MWh,"annual generation with charge and production load as negative generation, nationwide",,, -"gen_uncurtailed(i,r,t)",MWh,annual generation where VRE uses uncurtailed gen and storage uses GEN,,, -"gen_uncurtailed_nat(i,t)",MWh,national annual generation where VRE uses uncurtailed gen and storage uses GEN,,, -"gen_rsc(i,v,r,t)",MWh/MW,Annual generation per MW from rsc techs,,, -"h2_demand_by_sector(h2_demand_type,t)",metric tons,national H2 demand by use,,, -"h2_inout(h2_stor,r,allh,t,*)",metric tons/hour,H2 moving in/out of storage,,, -"h2_price_h(r,allh,t)",$2004/kg,Marginal cost of H2 by region and timeslice,,, -"h2_price_szn(r,allszn,t)",$2004/kg,Marginal cost of H2 by region and season,,, -"h2_ptc_generation(i,v,r,allh,t)",MWh,generation of clean electricity for electrolyzers receiving the hydrogen production tax credit (derived from CREDIT_H2PTC variable),,, -"h2_ptc_marginal_region(h2ptcreg,t)",$2004/MWh,marginal cost of producing an Energy Attribute Credit with regional and annual matching,,, -"h2_ptc_marginal_region_hour(h2ptcreg,allh,t)",$2004/MWh,"marginal cost of producing an Energy Attribute Credit with regional, hourly and annual matching",,, -"h2_storage_level(h2_stor,r,actualszn,allh,t)",metric tons,H2 storage level by timeslice,,, -"h2_storage_level_szn(h2_stor,r,actualszn,t)",metric tons,H2 storage level by season,,, -"h2_storage_cap(h2_stor,r,t)",metric tons,total H2 storage capacity by BA and type,,, -"h2_trans_cap(r,rr,t)",metric tons/hour,H2 pipeline capacity between BAs,,, -"h2_trans_flow(r,rr,allh,t)",metric tons/hour,net flow of H2 between BAs,,, -"h2_trans_flow_all(r,rr,allh,t)",metric tons/hour,flow of H2 between BAs,,, -"h2_usage(r,allh,t)",metric tons/hour,total H2 usage by hour (H2-CT/CC and non-electric H2 consumption),,, -"load_cat(loadtype,r,t)",MWh,Annual exogenous load by category,,, -"load_rt(r,t)",MWh,Annual exogenous load,,, -"load_stress(r,allh,t)",MW,Timeslice load during stress periods,,, -"load_frac_rt(r,t)",fraction,Fraction of LOAD in each region,,, -"loadsite_cap(r,t)",MW,Capacity of flexibly sited load,,, -"loadsite_op(r,allh,t)",MW,Operations of flexibly sited load,,, -"invtran_out(r,rr,trtype,t)",MW,new transmission capacity,1,, -"lcoe(i,v,r,t,rscbin)",$/MWh,levelized cost of electricity for all tech options,,, -"lcoe_built(i,r,t)",$/MWh,levelized cost of electricity for technologies that were built,,, -"lcoe_built_nat(i,t)",$/MWh,national average levelized cost of electricity for technologies that were built,,, -"lcoe_cf_act(i,v,r,t,rscbin)",$/MWh,LCOE using actual (instead of max) capacity factors,,, -"lcoe_fullpol(i,v,r,t,rscbin)",$/MWh,"LCOE considering full ITC and PTC value, whereas the LCOE parameter considers the annualized objective function",,, -"lcoe_nopol(i,v,r,t,rscbin)",$/MWh,LCOE without considering ITC and PTC adjustments,,, -"lcoe_pieces(lcoe_cat,i,r,t)",varies,levelized cost of electricity elements for technologies that were built,,, -"lcoe_pieces_nat(lcoe_cat,i,t)",varies,national average levelized cost of electricity elements for technologies that were built,,, -"losses_ann(*,t)",MWh,annual losses by category,1,, -"losses_tran_h(rr,r,allh,trtype,t)",MW,transmission losses by timeslice,,, -objfn_raw,2004$ in net present value terms,the raw objective function value,,, -"opRes_supply_h(ortype,i,r,allh,t)",MW,supply of operating reserves by timeslice and region,,, -"opRes_supply(ortype,i,r,t)",MW-h,annual supply of operating reserves by region,,, -"opres_trade(ortype,r,rr,t)",MW-h,total annual trade of operating reserves between sending region (r) and receiving region (rr),,, -"peak_load_adj(r,ccseason,t)",MWh,peak load adjustment for each ccseason,,, -"prm(r,t)",fraction,final planning reserve margin for each BA in year t,,, -"prod_load(i,r,allh,t)",MW,additional load from production activities,,, -"prod_load_ann(i,r,t)",MWh/year,additional annual load from production activities,,, -"prod_cap(i,v,r,t)",metric tons/hour,"production capacity, note unit change from MW to metric tons/hour",,, -"prod_produce(i,r,allh,t)",metric tons/hour,"production activities by technology, BA, and timeslice",,, -"prod_produce_ann(i,r,t)",metric tons/year,annual production by technology and BA,,, -"prod_h2_price(p,t)",$2004/metric ton,annual national average marginal cost of producing H2 (see also h2_price_h and h2_price_szn),,, -"prod_h2comb_cost(p,t)",$2004/mmbtu,marginal cost of fuels used for H2-CT/CC combustion,,, -"prod_syscosts(sys_costs,i,r,t)",2004$,BA- and tech-specific investment and operation costs associated with production activities,,, -"prod_SMR_emit(e,r,t)",metric tons,emissions from SMR activities,,, -"ptc_out(i,v,t)",2004$/MWh,"value of the ptc, equal to ptc_value_scaled(i,v,t) * tc_phaseout_mult(i,v,t)",1,, -raw_inv_cost(t),2004$,sum of investment costs from systemcost,,, -raw_op_cost(t),2004$,sum of operational costs from systemcost,,, -"rec_outputs(RPSCat,i,st,ast,t)",MWh,quantity of RECs served from state st to state ast,,, -"reduced_cost(i,v,r,t,*,*)",2004$/kW undiscounted,the reduced cost of each investment option. All non-rsc are assigned to nobin,,, -RE_gen_price_nat(t),2004$/MWh,marginal cost of the national RE generation constraint,,, -"repbioprice(r,t)",2004$/MMBtu,highest marginal bioprice of utilized bins for each region,,, -"repgasprice(cendiv,t)",2004$/MMBtu,highest marginal gas price of utilized gas bins for each census division,,, -"repgasprice_r(r,t)",2004$/MMBtu,highest marginal gas price of utilized gas bins for each region,1,, -repgasprice_nat(t),2004$/MMBtu,weighted-average national natural gas price assuming that plants pay the marginal price,,, -"repgasquant(cendiv,t)",Quads,quantity of gas consumed in each census division,,, -"repgasquant_irt(i,r,t)",Quads,quantity of gas consumed by tech and region,,, -repgasquant_nat(t),Quads,national consumption of natural gas,,, -"reqt_price(*,*,r,*,t)",varies,Price of requirements,,, -"reqt_price_sys(*,*,*,t)",varies,System-average price of requirements,,, -"reqt_quant(*,*,r,*,t)",varies,Requirement quantity,,, -"reqt_quant_sys(*,*,*,t)",varies,System-wide requirement quantity,,, -"ret_ann(i,r,t)",MW/yr,annual retired capacity by region,,, -"ret_ann_nat(i,t)",MW/yr,annual retired capacity national,,, -"ret_ivrt(i,v,r,t)",MW,retired capacity by region and vintage,1,, -"ret_out(i,r,t)",MW,retired capacity by region,,, -"revenue(rev_cat,i,r,t)",2004$,sum of revenues,,, -"revenue_nat(rev_cat,i,t)",2004$,sum of revenues,,, -"revenue_en(rev_cat,i,r,t)",2004$/MWh,revenues per MWh of generation,,, -"revenue_en_nat(rev_cat,i,t)",2004$/MWh,revenues per MWh of generation,,, -"revenue_cap(rev_cat,i,r,t)",2004$/MW,revenues per MW of capacity,,, -"revenue_cap_nat(rev_cat,i,t)",2004$/MW,revenues per MW of capacity,,, -"site_cap(i,x,t)",MW,capacity by reV site,,, -"site_spurcap(x,t)",MW,spur-line capacity to reV site,,, -"site_spurinv(x,t)",MW,spur-line investment at reV site,,, -"site_gir(i,x,t)",MWgen/MWspur,Generator-to-interconnection ratio by tech,,, -"site_hybridization(x,t)",unitless,"Hybridization factor: 0 for all-PV or all-wind, 1 for 50:50 PV:wind",,, -"site_pv_fraction(x,t)",MWpv/MWgen,Fraction of capacity at reV site that is PV,,, -rggi_price(t),2004$/metric ton,shadow price from RGGI constraint,,, -rggi_quant(t),metric tons,annual CO2 cap for the regional greenhouse gas initiative (RGGI),,, -"stor_energy_cap(i,v,r,t)",MWh,"energy capacity of storage devices by tech, BA, vintage, and year",,, -"storage_duration_out(i,v,r,t)",h,"storage duration of battery",,, -"stor_inout(i,v,r,t,*)",MWh,Annual energy going into and out of storage,,, -"stor_in(i,v,r,allh,t)",MW,energy going into storage by timeslice,,, -"stor_interday_level(i,v,r,allszn,t)",MWh,storage level for inter-day technogies,,, -"stor_interday_dispatch(i,v,r,allh,t)",MW,storage net dispatch for inter-day technogies,,, -"stor_level(i,v,r,allh,t)",MWh,storage level,,, -"stor_out(i,v,r,allh,t)",MW,energy leaving storage,,, -# Begin system cost parameters,,,,, -"systemcost(sys_costs,t)",2004$,"reported system cost for each component, where inv costs are model year present value and op costs are single year",,, -"systemcost_bulk(sys_costs,t)",2004$,"reported system cost for each component, where inv costs are model year present value and op costs are model year present value until next model year",,, -"systemcost_bulk_ew(sys_costs,t)",2004$,"same as systemcost_bulk, but the end year is only 1 year of operation and CRF times the investment",,, -"systemcost_ba(sys_costs,r,t)",2004$,"reported ba-level system cost for each component, where inv costs are model year present value and op costs are single year",,, -"systemcost_ba_bulk(sys_costs,r,t)",2004$,"reported system cost for each component, where inv costs are model year present value and op costs are model year present value until next model year",,, -"systemcost_ba_bulk_ew(sys_costs,r,t)",2004$,"same as systemcost_ba_bulk, but the end year is only 1 year of operation and CRF times the investment",,, -"systemcost_ba_retailrate(sys_costs,r,t)",2004$,"same as systemcost_ba, but with outputs adapted for the retailrate module",,, -"systemcost_techba(sys_costs,i,r,t)",2004$,"reported tech|ba-level system cost for each component, where inv costs are model year present value and op costs are single year",,, -"systemcost_techba_bulk(sys_costs,i,r,t)",2004$,"reported tech|ba-level system cost for each component, where inv costs are model year present value and op costs are model year present value until next model year",,, -"systemcost_techba_bulk_ew(sys_costs,i,r,t)",2004$,"same as systemcost_techba_bulk, but the end year is only 1 year of operation and CRF times the investment",,, -# end system cost parameters,,,,, -tax_expenditure_itc(t),2004$,ITC tax expenditure,,, -tax_expenditure_ptc(t),2004$,PTC tax expenditure,,, -"tran_flow_all_rep(r,rr,allh,trtype,t)",MW,transmission flows between regions by representative time slice,,, -"tran_flow_all_stress(r,rr,allh,trtype,t)",MW,transmission flows between regions by stress time slice,,, -"tran_flow_rep(r,rr,allh,trtype,t)",MW,"power flow along each transmission line, losses NOT included",,, -"tran_flow_rep_ann(r,rr,trtype,t)",MWh,"annual net energy flow across each transmission interface, losses NOT included",,, -"tran_flow_stress(r,rr,allh,trtype,t)",MW,transmission flows between regions during stress periods,,, -"tran_util_h_rep(r,rr,allh,trtype,t)",fraction,Fractional transmission route utilization by timeslice during representative periods,,, -"tran_util_h_stress(r,rr,allh,trtype,t)",fraction,Fractional transmission route utilization by timeslice during stress periods,,, -"tran_util_ann_rep(r,rr,trtype,t)",fraction,Fractional transmission route utilization by year during representative periods,,, -"tran_util_ann_stress(r,rr,trtype,t)",fraction,Fractional transmission route utilization by year during stress periods,,, -"net_import_h_rep(r,allh,t)",MW,Net power imports (imports - exports with losses on imports) by timeslice during representative periods,,, -"net_import_h_stress(r,allh,t)",MW,Net power imports (imports - exports with losses on imports) by timeslice during stress periods,,, -"net_import_ann_rep(r,t)",MWh,Net energy imports (imports - exports with losses on imports) by year during representative periods,,, -"net_import_ann_stress(r,t)",MWh,Net energy imports (imports - exports with losses on imports) by year during stress periods,,, -"import_h_rep(r,allh,t)",MW,Power imports (with losses) by timeslice for representative periods,,, -"export_h_rep(r,allh,t)",MW,Power exports (no losses) by timeslice for representative periods,,, -"import_ann_rep(r,t)",MWh,Energy imports (with losses) by year for representative periods,,, -"export_ann_rep(r,t)",MWh,Energy exports (no losses) by year for representative periods,,, -"poi_capacity(r,t)",MW,total point-of-connection capacity (used for intra-zone transmission network reinforcement),,, -"tran_hurdle_cost_ann(r,rr,trtype,t)",2004$,annual monetary value associated with hurdle rates on transmission flows,,, -"tran_mi_out(trtype,t)",MW-mi,total transmission capacity*distance for energy trading,,, -"tran_prm_mi_out(trtype,t)",MW-mi,total transmission capacity*distance for capacity trading,,, -"tran_mi_out_detail(r,rr,trtype,t)",MW-mi,total transmission capacity by distance between region,,, -"tran_cap_energy(r,rr,trtype,t)",MW,total transmission capacity for energy trading,,, -"tran_cap_prm(r,rr,trtype,t)",MW,total transmission capacity for PRM trading,,, -"tran_cap_grp(transgrp,transgrpp,t)",MW,transmission flow limit between transgrp regions,,, -"tran_out(r,rr,trtype,t)",MW,"total transmission capacity for energy trading, averaging over forward and reverse directions",1,, -"tran_prm_out(r,rr,trtype,t)",MW,"total transmission capacity for PRM trading, averaging over forward and reverse directions",,, -"captrade(r,rr,trtype,ccseason,t)",MW,planning reserve margin capacity traded from r to rr,,, -"gasshare_ba(r,cendiv,t)",unitless,share of natural gas consumption in BA relative to corresponding cendiv consumption,,, -"gasshare_techba(i,r,cendiv,t)",unitless,share of natural gas consumption in tech-BA combination relative to corresponding cendiv consumption,,, -"gasshare_cendiv(cendiv,t)",unitless,share of natural gas consumption in cendiv relative to national consumption,,, -"bioshare_techba(i,r,t)",unitless,share of biofuel consumption in tech-BA combination relative to total BA biofuel consumption,,, -"gascost_cendiv(cendiv,t)",2004$,natual gas fuel cost at cendiv level,,, -"valnew(*,*,*,t)",varies,value of new investments,,, -"water_withdrawal_ivrt(i,v,r,t)",Mgal,"water withdrawal by tech, year, region, and class",,, -"water_consumption_ivrt(i,v,r,t)",Mgal,"water consumption by tech, year, region, and class",,, -"watcap_ivrt(i,v,r,t)",Mgal,"water capacity by tech, year, region, and class",,, -"watcap_out(i,r,t)",Mgal,water capacity by region,,, -"watcap_new_ivrt(i,v,r,t)",Mgal,new water capacity,,, -"watcap_new_out(i,r,t)",Mgal,"new water capacity by region, which are investments from one solve year to the next",,, -"watcap_new_ann_out(i,v,r,t)",Mgal/yr,new annual water capacity by region,,, -"watret_ivrt(i,v,r,t)",Mgal,retired water capacity by region and vintage,,, -"watret_out(i,r,t)",Mgal,retired water capacity by region,,, -"watret_ann_out(i,v,r,t)",Mgal/yr,annual retired water capacity by region,,, -# Parameters defined before e_report.gms - some are used in r2x,,,,, -bcr,,,,,1 -biosupply,,,,,1 -cap_hyd_szn_adj,,,,,1 -capture_rate,,,,,1 -cf_adj_t,,,,,1 -cf_hyd,,,,,1 -cost_cap,,,,,1 -cost_cap_energy,,,,,1 -cost_cap_fin_mult,,,,,1 -cost_cap_fin_mult_noITC,,,,,1 -cost_hurdle,,,,,1 -cost_scale,,,,,1 -cost_vom,,,,,1 -cendiv,,,,,1 -degrade_annual,,,,,1 -e,,,,,1 -emit_rate,,,,,1 -fuel_price,,,,,1 -fuel2tech,,,,,1 -h_szn,,,,,1 -heat_rate,,,,,1 -hierarchy,,,,,1 -hours,,,,,1 -hydmin,,,,,1 -ilr,,,,,1 -outage_scheduled_h,,,,,1 -pvf_capital,,,,,1 -pvf_onm,,,,,1 -r,,,,,1 -rsc_dat,,,,,1 -storage_duration,,,,,1 -storage_eff,,,,,1 -szn_stress_t,,,,,1 -tc_phaseout_mult,,,,,1 -tranloss,,,,,1 -v,,,,,1 -valcap_i,,,,,1 -z_rep,,,,,1 diff --git a/gurobi.opt b/gurobi.opt deleted file mode 100644 index cba6c407..00000000 --- a/gurobi.opt +++ /dev/null @@ -1,4 +0,0 @@ -objscale = 1e9 -ScaleFlag = 2 -Threads = 4 -method = 2 diff --git a/input_processing/WriteHintage.py b/input_processing/WriteHintage.py deleted file mode 100644 index 777f31e1..00000000 --- a/input_processing/WriteHintage.py +++ /dev/null @@ -1,774 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The purpose of this script is to group existing generating -units into historical, binned vintages (hintages) - -The primary arguments are the plant data file to use, the number of bins, -and the minimum deviation across bins. There are also operations specific -to proessing data for whether or not GSw_WaterMain are enabled. - -Kmeans clustering is the default option and the general sequence, by tech and -BA combinations, is as follows: - - 1. Check to see if the number of unique units is less than the number of bins - - if true, check to see if the deviation across all those different units exceeds the minimum deviation - - if true, use the raw data and assign bins to each individual units - - if false, proceed with binning - - if false, proceed with binning - 2. Perform capacity-weighted kmeans clustering with the maximum number of bins - - Maximum number of bins first defined as the minimum of.. - - number of bins assigned by user - - number of unique heat rates - - number of units in the tech/BA combination - 3. Check to see if the deviation across all heat rate centroids exceed the minimum deviation - - if so, proceed to '4' - - if not, return to '2' but reduce the number of bins by 1 - 4. Assign units to their nearest heat rate bin - - if only one unique unit in a bin, assign its original heat rate - - if more than one unit in a bin, assign the capacity-weighted average - 5. For all years from 2010-2100, compute the remaining amount of capacity - based on the units specified retirement date and compute the remaining - units' capacity-weighted-average characteristics (FOM/VOM/HR/...) - ---- - -For testing - the default arguments are passed in to the main(...) function - -""" -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import math -import numpy as np -import os -import sys -import pandas as pd -import warnings -import datetime -from sklearn.cluster import KMeans -from sklearn.exceptions import ConvergenceWarning -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -# Time the operation of this script -tic = datetime.datetime.now() - -# ignore ConvergenceWarnings that occur in this file from the kmeans function -warnings.filterwarnings("ignore", category=ConvergenceWarning) - - -#%% =========================================================================== -### --- FUNCTIONS AND CLASSES --- -### =========================================================================== -class grouping: - def __init__(self, nbins, *args, **kwargs): - #df = tdat - #nbins=6 - if nbins == 'unit': - self.df = self.unit(*args) - elif nbins == 'group': - self.df = self.group(*args) - else: - nbins = int(nbins) - self.df = self.kmeans(nbins, *args, **kwargs) - - - def unit(self, input_df, *args, **kwargs): - ''' - This method creates a unique hintage bin for every generator - - input_df: pandas DataFrame object containing ReEDS generators - *args: collects unused positional arguments to simplify code - **kwargs: collects unused keyword arguments to simplify code - ''' - output_df = pd.DataFrame() - # NOTE: The calculations here and below in group() can probably be done - # faster by group (without a loop). - for ba in input_df.r.unique(): - ba_df = input_df[input_df.r == ba].copy() - for tech in ba_df.TECH.unique(): - df = ba_df[ba_df.TECH == tech].copy() - df['bin'] = df.reset_index(drop=True).index + 1 - output_df = pd.concat([output_df, df]) - - return output_df - - - def group(self, input_df, col, *args, **kwargs): - ''' - This method creates hintage bins for unique region, tech, and specified - column combinations. - - input_df: (pandas.DataFrame object) containing ReEDS generators - col: (str) The name of the column used in binning - *args: collects unused positional arguments to simplify code - **kwargs: collects unused keyword arguments to simplify code - ''' - grouping_df = (input_df[['r', 'TECH', col]] - .drop_duplicates() - .reset_index(drop=True) - ) - output_df = pd.DataFrame() - for ba in grouping_df.r.unique(): - ba_df = grouping_df[grouping_df.r == ba].copy() - for tech in ba_df.TECH.unique(): - tech_df = ba_df[ba_df.TECH == tech].copy() - tech_df['bin'] = tech_df.reset_index().index + 1 - - output_df = pd.concat([output_df, tech_df.reset_index(drop=True)]) - - - output_df = input_df.merge(output_df, - on=['r','TECH', col], - how='left' - ) - return output_df - - - class _kmeans: - def __init__(self, input_df, col, bins, minSpread=2000, n_init=10): - ''' - bin and return the centroids or breakpoints of each bin - - df (DataFrame object): Pandas Dataframe containing data for binning - col (str): the column name that is to be binned - bins (int): the number of desired bins - ''' - self.bins = bins - df = input_df.copy() - - #if the number of unique heat rates is already less than the number of bins - if len(df[col].unique()) <= bins: - #no matter what, set the bins to the number of unique heat rates - self.bins = len(df[col].unique()) - - # in order for us to skip binning, we'll need to check - # if the minimum difference across all unique heat rates - # exceeds the minSpread - if so, we can justify - # avoiding the binning algorithm - if len(df[col].unique()) > 1: - mindiff_unique = min([abs(j-i) for i, j in zip(df[col].unique()[:-1], - df[col].unique()[1:])]) - - #note that if we only have one unique value for this tech/BA combo - #heat rate, then we can skip binning altogether - if len(df[col].unique()) == 1: - mindiff_unique = minSpread + 1 - - # if the number of unique elements is greater than the number of bins - # make sure we skip the following condition and perform binning - else: - mindiff_unique = minSpread + 1 - - # if you can use the raw data as is - ie if the observed heat rates - # are disparate enough such that they exceed minspread and the - # the number of units is not greater than the number of bins - - # then just use the raw data - if len(df[col].unique()) <= bins and mindiff_unique > minSpread: - bins = len(df[col].unique()) - # if the maximum deviation across all heat rates - # is less than the minimum deviation - if (df[col].max() - df[col].min()) < minSpread: - # put all units into one bin - df['centroid'] = df[col].mean() - df['bin'] = 1 - else: - df['centroid'] = df[col] - temp = pd.DataFrame(data=dict(centroid=df[col].unique(), bin=None)) - temp.bin = temp.index + 1 - df = df.merge(temp, on='centroid', how='left') - self.centers = df - - # if you can't just use the raw data - else: - # if the number of bins exceeds the number of observations - # reset the number of bins to the length of the data - # note if the heat rates were not disparate enough - # we would've caught that in the previous condition block - if bins > len(df.index): - self.bins = max(len(df)-1, 1) - - # make a temporary binning DF - bin_df = pd.concat([df[col], - pd.DataFrame(columns=['centroid', 'upper', - 'lower', 'bin'])]) - bin_df.rename(columns={0: col}, inplace=True) - - # establish parameters necessary for the while loop - spread = minSpread - 1 - nbins = self.bins - - # while the minimum spread hasn't been exceeded - # and the number of bins haven't been exhausted - # keep attempting to cluster - if these conditions - # haven't been met, try again with one fewer bin - while spread < minSpread or nbins == 1: - df = input_df.copy() - - # initialize the centroids - note that the - # random_state argument implies a static seed - # for the random processes/distribution-draws - # used in the kmeans function - centroids_obj = KMeans( - n_clusters=nbins, random_state=0, max_iter=1000, n_init=n_init, - ).fit(df[[col]].to_numpy(), sample_weight = df['Summer.capacity'].to_numpy()) - - #need to convert array of length-one arrays to one long array - centroids = [ i[0] for i in centroids_obj.cluster_centers_] - - # create a list of unique centroids - centroids = list(set(centroids)) - - # make the binning matrix - k = pd.DataFrame(index=df[col], columns=centroids).reset_index() - k = k.set_index('HR') - - # compute the difference between the observed heat rate and the centroid - for c in centroids: - k[c] = abs(k.index - c) - - # select the closest centroid - k['centroid'] = k.columns[k[centroids].values.argmin(1)] - - # Merge centroids onto original DF - k_bins = k.centroid.drop_duplicates().reset_index().copy() - k_bins['bin'] = k_bins.index + 1 - k_map = k.reset_index().merge(k_bins[['centroid', 'bin']], - on='centroid', - how='left').set_index('HR') - - # find the minimum deviation across all heat rate combinations - if len(centroids)>1: - spread = min([abs(j-i) for i, j in zip(k['centroid'].unique()[:-1], - k['centroid'].unique()[1:])]) - # if there is only one centroid, - # set the conditional to exit the while loop - else: - spread = minSpread + 1 - - #reset the index for formatting - k_map.reset_index(inplace=True) - - nbins -= 1 - #end of while loop for nbins and spread < minspread check - - # merge the binned heat rates with the original plant data - # this will be the output to the kmeans function - self.centers = df.merge(k_map.drop_duplicates()[[col, 'centroid', 'bin']], - how='left', on=col) - - - def kmeans(self, nbins, input_df, *args, **kwargs): - ''' - bin and return the centroids or breakpoints of each bin - - df (DataFrame object): Pandas Dataframe containing data for binning - col (str): the column name that is to be binned - bins (int): the number of desired bins - *args: collects unused positional arguments to simplify code - **kwargs: collects unused keyword arguments to simplify code - ''' - print("Starting kmeans clustering of existing generators") - print("using {} bins and a minimum deviation of {} mmBTU/MWh \n".format( - nbins, kwargs['minSpread'])) - print("Note that the clustering can result in warnings if the heat rates") - print("or number of unique plants exceeds the bins specified in the loop") - tdat=pd.DataFrame() - - # for all unique BA/technology combinations... - for i in input_df.id.unique(): - tdat = pd.concat([tdat, - self._kmeans(input_df[input_df.id == i], - 'HR', - nbins, - minSpread=int(kwargs['minSpread']) - ).centers]) - return tdat - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== -def main(reeds_path, inputs_case): - print('Starting WriteHintage.py') - - # #%% Settings for testing - # reeds_path = os.path.expanduser('~/github/ReEDS-2.0') - # inputs_case = os.path.join( - # reeds_path,'runs','v20231027_yamM0_Z45_h_d_365_transreg_z69_core','inputs_case') - - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - - nBin = int(sw.numhintage) - retscen = sw.retscen - mindev = int(sw.mindev) - GSw_WaterMain = sw.GSw_WaterMain - GSw_RetireYears_Coal = int(sw.GSw_RetireYears_Coal) - GSw_RetireYears_Thermal = int(sw.GSw_RetireYears_Thermal) - GSw_Clean_Air_Act = int(sw.GSw_Clean_Air_Act) - - #%% - # Inflation factor 1987$ to 2004$ - inflator = 1.69 - - # Dictionary of relevant technology groups - TECH = { - # This is not all technologies that do not having cooling, but technologies - # that are (or could be) in the plant database. - 'no_cooling':['upv', 'pvb', 'gas-ct', 'geohydro_allkm', - 'battery_li', 'pumped-hydro', 'pumped-hydro-flex', - 'hydUD', 'hydUND', 'hydD', 'hydND', 'hydSD', 'hydSND', 'hydNPD', - 'hydNPND', 'hydED', 'hydEND', 'wind-ons', 'wind-ofs' - ] - } - - # Import mapping files - r_county = pd.read_csv( - os.path.join(inputs_case,'r_county.csv'), index_col='county').squeeze(1) - - # Import generator database - indat = pd.read_csv(os.path.join(inputs_case,'unitdata.csv'), - low_memory=False - ) - - # Map counties to modeled regions - indat['r'] = indat.FIPS.map(r_county) - - # Apply inflation to VOM costs - indat['T_VOM'] *= inflator - indat['T_CCSV'] = indat.T_VOM + inflator * indat.T_CCSV - - # Apply inflation and add capital expenditures to FOM costs - indat['T_FOM'] = inflator * (indat.T_FOM + indat.T_CAPAD) - indat['T_CCSF'] = indat.T_FOM + inflator * indat.T_CCSF - - # Concatenate tech names based on whether water analysis is on -or- leave them alone - if GSw_WaterMain == '1': - # If techs do not have a cooling technology, then replace coolingwatertech with the tech name - indat.loc[indat['tech'].isin(TECH['no_cooling']), - 'coolingwatertech'] = indat.loc[indat['tech'].isin(TECH['no_cooling']),'tech'] - indat['tech'] = indat.coolingwatertech - - ### NOTE: New addition for columns AO:AR, AW:AX in the plant file - ad = indat[["tech", "r", "ctt", "summer_power_capacity_MW", "TC_WIN", retscen, - "StartYear", "IsExistUnit", "HeatRate", "T_VOM", "T_FOM", - "T_CCSROV", "T_CCSF", "T_CCSV", "T_CCSHR", "T_CCSCAPA", "T_CCSLOC"]].copy() - - # Rename columns in ad - rename = { - 'tech' : 'TECH', - 'r' : 'r', - 'ctt' : 'ctt', - 'summer_power_capacity_MW' : 'Summer.capacity', - 'TC_WIN' : 'Winter.capacity', - retscen : 'RetireYear', - 'StartYear' : 'onlineyear', - 'IsExistUnit' : 'EXIST', - 'HeatRate' : 'HR', - 'T_VOM' : 'VOM', - 'T_FOM' : 'FOM', - 'T_CCSROV' : 'CCS_Retro_OvernightCost', - 'T_CCSF' : 'CCS_Retro_FOM', - 'T_CCSV' : 'CCS_Retro_VOM', - 'T_CCSHR': 'CCS_Retro_HR', - 'T_CCSCAPA': 'CCS_Retro_CapAdjust', - 'T_CCSLOC': 'CCS_Retro_LocFactor' - } - ad.rename(columns=rename, inplace=True) - - # Subset only generators that exist - ad = ad[ad.EXIST] - - # Only want those with a heat rate - all other binning is arbitrary - # because the only data we get from generator database is the capacity and heat - # rate but O&M costs are assumed - df = ad[(~ad.HR.isna()) & (~ad.TECH.isin(['geohydro_allkm', 'CofireNew']))] - - # Adjust retirement dates of coal specifically or thermal techs generally based on switch - - tech_table = pd.read_csv( - os.path.join(inputs_case, 'tech-subset-table.csv')).set_index('Unnamed: 0') - coal_techs = [x.lower() for x in tech_table[tech_table['COAL'] == 'YES'].index.values.tolist()] - thermal_techs = [x.lower() for x in tech_table[(tech_table['COAL'] == 'YES') | - (tech_table['GAS'] == 'YES') | - (tech_table['NUCLEAR'] == 'YES') | - (tech_table['OGS'] == 'YES')].index.values.tolist()] - - current_yr = datetime.date.today().year - - if GSw_RetireYears_Thermal == 0: - # Thermal retirements are not adjusted, data is straight from EIA unit database - # Coal retirement can be adjusted separately when GSw_RetireYears_Thermal = 0 - # if GSw_RetireYears_Coal = 0, coal retirements are not adjusted, data is straight from EIA unit database - if GSw_RetireYears_Coal < 0: - # For coal units with retire year before current_yr - GSw_RetireYears_Coal, they are forced to retire in current_yr. - # For coal units with retire year after or equal to current_yr - GSw_RetireYears_Coal, their lifetime is shorted by |GSw_RetireYears_Coal|. - df.loc[(df['RetireYear'] < current_yr - GSw_RetireYears_Coal) & (df['RetireYear'] > current_yr) - & (df['TECH'].isin(coal_techs)), 'RetireYear'] = current_yr - df.loc[(df['RetireYear'] >= current_yr - GSw_RetireYears_Coal) & (df['TECH'].isin(coal_techs)), - 'RetireYear'] += GSw_RetireYears_Coal - - elif GSw_RetireYears_Coal > 0: - # Lifetime of all currently operating coal units is extended by GSw_RetireYears_Coal - df.loc[(df['RetireYear'] > current_yr) & (df['TECH'].isin(coal_techs)), - 'RetireYear'] += GSw_RetireYears_Coal - - # Adjust thermal techs' retirement dates when GSw_RetireYears_Thermal != 0 - elif GSw_RetireYears_Thermal < 0: - # For thermal units with retire year before current_yr - GSw_RetireYears_Thermal, they are forced to retire in current_yr. - # For thermal units with retire year after or equal to current_yr - GSw_RetireYears_Thermal, their lifetime is shorted by |GSw_RetireYears_Thermal|. - df.loc[(df['RetireYear'] < current_yr - GSw_RetireYears_Thermal) & (df['RetireYear'] > current_yr) - & (df['TECH'].isin(thermal_techs)), 'RetireYear'] = current_yr - df.loc[(df['RetireYear'] >= current_yr - GSw_RetireYears_Thermal) & (df['TECH'].isin(thermal_techs)), - 'RetireYear'] += GSw_RetireYears_Thermal - - elif GSw_RetireYears_Thermal > 0: - # Lifetime of all currently operating thermal units is extended by GSw_RetireYears_Thermal - df.loc[(df['RetireYear'] > current_yr) & (df['TECH'].isin(thermal_techs)), - 'RetireYear'] += GSw_RetireYears_Thermal - - # Group up similar generators - dat = df.groupby([ - 'TECH', 'r', 'HR', 'onlineyear', - 'RetireYear', 'VOM', 'FOM',"CCS_Retro_OvernightCost", "CCS_Retro_FOM", - "CCS_Retro_VOM", "CCS_Retro_HR", "CCS_Retro_CapAdjust", "CCS_Retro_LocFactor", - ])[['Summer.capacity','Winter.capacity']].sum().reset_index() - - # Remove 'others' category - dat = dat[dat.TECH != 'others'].copy() - - # Remove some generators based on retire year and online year - dat = dat[(dat.RetireYear >= 2010) & (dat['onlineyear'] < 2010)].copy() - - # Make unique ID column for generators - id_delimiter = '' - dat['id'] = dat.TECH + id_delimiter + dat.r - - # Bin hintage data - this leverages the kmeans function in - # the grouping class to perform the operations in the _kmeans sub-class - # and returns the 'dat' dataframe with the additional 'bin' column - # this needs to be done separately since coal techs are regulated at the unit level - dat_non_coal = dat[~dat.TECH.isin(coal_techs)] # all non-coal plants - dat_coal = dat[dat.TECH.isin(coal_techs)] # coal plants - - # treat the non-coal options regularly - tdat_non_coal = grouping(nBin, dat_non_coal, 'HR', minSpread=mindev).df - - # coal plants are grouped at the unit level if GSw_Clean_Air_Act is enabled - if GSw_Clean_Air_Act == 1: - tdat_coal = grouping('unit', dat_coal, 'HR', minSpread=mindev).df - else: - tdat_coal = grouping(nBin, dat_coal, 'HR', minSpread=mindev).df - - tdat = pd.concat([tdat_non_coal, tdat_coal], ignore_index=True, axis=0) - - # calculate the maximum hintage number, to be used in b_inputs.gms, and export it - max_hintage_number = tdat['bin'].max() - max_hintage_number_text = f'scalar max_hintage_number "--number-- the maximum number of bins used in this ReEDS run" /{max_hintage_number}/ ;' - with open(os.path.join(inputs_case,'max_hintage_number.txt'), 'w') as file: - file.write(f'{max_hintage_number_text}') - - # calculate the capacity-weighted average heat rate for each bin - # by taking the product of the sum of the capacity and the centroid of the bin - tdat['wHR'] = tdat.HR * tdat['Summer.capacity'] - tdat['wVOM'] = tdat.VOM * tdat['Summer.capacity'] - tdat['wFOM'] = tdat.FOM * tdat['Summer.capacity'] - tdat['solveYearOnline'] = tdat.onlineyear * tdat['Summer.capacity'] - tdat['wCCS_Retro_OvernightCost'] = tdat.CCS_Retro_OvernightCost * tdat['Summer.capacity'] - tdat['wCCS_Retro_FOM'] = tdat.CCS_Retro_FOM * tdat['Summer.capacity'] - tdat['wCCS_Retro_VOM'] = tdat.CCS_Retro_VOM * tdat['Summer.capacity'] - tdat['wCCS_Retro_HR'] = tdat.CCS_Retro_HR * tdat['Summer.capacity'] - tdat['wCCS_Retro_CapAdjust'] = tdat.CCS_Retro_CapAdjust * tdat['Summer.capacity'] - tdat['wCCS_Retro_LocFactor'] = tdat.CCS_Retro_LocFactor * tdat['Summer.capacity'] - - zout = pd.DataFrame() - level_cols = ['wHR', 'wVOM', 'wFOM', 'solveYearOnline','wCCS_Retro_OvernightCost', - 'wCCS_Retro_FOM','wCCS_Retro_VOM','wCCS_Retro_HR', - 'wCCS_Retro_CapAdjust','wCCS_Retro_LocFactor'] - - combine_cols = level_cols + ['Winter.capacity'] - -# Adjust the HR, VOM, FOM, solveYearOnline, and winter capacity - for i in list(range(2010, tdat.RetireYear.max() + 1)): - # Subset on years earlier than i - ydat = tdat.loc[tdat.RetireYear > i, ['id','bin','Summer.capacity'] + combine_cols] - - # Sum up the parameters by id and bin - ydat = ydat.groupby(['id','bin']).sum() - - # Levelize parameters - for j in level_cols: - ydat[j] /= ydat['Summer.capacity'] - - ydat['year'] = i - # Paste dataframes together - zout = pd.concat([zout, ydat]) - - # Parse id - zout.reset_index(inplace=True) - if GSw_WaterMain == '1': - zout['tech'] = zout.id.str.rsplit(id_delimiter, n=1, expand=True)[0] - zout['r'] = zout.id.str.rsplit(id_delimiter, n=1, expand=True)[1] - else: - zout['tech'] = zout.id.str.split(id_delimiter, n=1, expand=True)[0] - zout['r'] = zout.id.str.split(id_delimiter, n=1, expand=True)[1] - zout.drop(columns='id', inplace=True) - - #%%############################### - # -- Get DPV Generators -- # - ################################## - - dpv = pd.read_csv(os.path.join(inputs_case,'distpvcap.csv')).set_index('r') - - # Fill in odd years' values for dpv (only add odd year data if that - # data does not already exist) - firstyr = int(dpv.columns.min()) - lastyr = int(dpv.columns.max()) - oddyrs = [str(x) for x in np.arange(firstyr,lastyr+1) if x % 2 != 0] - for yr in oddyrs: - if yr not in dpv.columns: - dpv[yr] = (dpv[str(int(yr)-1)] + dpv[str(int(yr)+1)]) / 2 - dpv = pd.melt(dpv.reset_index(),id_vars=['r']) - dpv.rename(columns=dict(zip(dpv.columns,['r','year','Summer.capacity'])), - inplace=True) - - # Initialize columns for dpv dataframe - dpv['tech'] = 'distpv' - dpv['wHR'] = 0 - dpv['wVOM'] = 0 - dpv['wFOM'] = 0 - dpv['Winter.capacity'] = dpv['Summer.capacity'] - dpv['bin'] = 1 - dpv['solveYearOnline'] = 2010 - dpv['year'] = dpv['year'].astype(int) - - # Concat dpv and the output dataframes - zout = pd.concat([zout, dpv]) - - #%%############################################################################ - # -- Get forced retirement dataframe and merge onto output dataframe -- # - ############################################################################### - forced_retire = pd.read_csv( - os.path.join(inputs_case, 'forced_retirements.csv'), - header=0, names=['tech','st','retire_year']) - - # Forced retirements are at the state level, so use hierarchy to get the regions - state2r = ( - pd.read_csv( - os.path.join(inputs_case, 'hierarchy.csv'), - usecols=['*r', 'st'] - ) - .rename(columns={'*r': 'r'}) - ) - forced_retire = ( - pd.merge(forced_retire, state2r, on='st') - .drop(columns='st') - .drop_duplicates() - ) - - zout = zout.merge(forced_retire, how='left', on=['tech', 'r']).fillna(9000) - - # Zero out retired generators' capacity - zout.loc[zout.solveYearOnline >= zout.retire_year, 'Summer.capacity'] = 0 - zout.loc[zout.solveYearOnline >= zout.retire_year, 'Winter.capacity'] = 0 - - # Clean up output dataframe - zout['bin_int'] = zout['bin'] # keep integer bins in dataframe for ease of plotting - zout['bin'] = 'init-' + zout['bin'].astype(str) - zout['solveYearOnline'] = zout['solveYearOnline'].round() - zout['wFOM'] *= 1e3 - zout.rename(columns={'Summer.capacity': 'cap', - 'Winter.capacity': 'wintercap', - 'solveYearOnline': 'wOnlineYear', - 'year': 'yr', - 'tech': 'TECH'}, - inplace=True) - - zout.cap = zout.cap.round(decimals=1) - zout.wintercap = zout.wintercap.round(decimals=1) - zout.wHR = zout.wHR.round(decimals=1) - zout.wFOM = zout.wFOM.round(decimals=3) - zout.wVOM = zout.wVOM.round(decimals=3) - zout.wCCS_Retro_OvernightCost = zout.wCCS_Retro_OvernightCost.round(decimals=3) - zout.wCCS_Retro_FOM = zout.wCCS_Retro_FOM.round(decimals=3) - zout.wCCS_Retro_VOM = zout.wCCS_Retro_VOM.round(decimals=3) - zout.wCCS_Retro_HR = zout.wCCS_Retro_HR.round(decimals=1) - zout.wCCS_Retro_CapAdjust = zout.wCCS_Retro_CapAdjust.round(decimals=3) - zout.wCCS_Retro_LocFactor = zout.wCCS_Retro_LocFactor.round(decimals=3) - - #%% Save output dataframe in inputs_case folder - cols = ['TECH', 'bin', 'r', 'yr', 'cap', 'wintercap', 'wHR', - 'wFOM', 'wVOM', 'wOnlineYear', - 'wCCS_Retro_OvernightCost','wCCS_Retro_FOM','wCCS_Retro_VOM', # new addition: revised to include CCS retrofits - 'wCCS_Retro_HR','wCCS_Retro_CapAdjust','wCCS_Retro_LocFactor'] - - zout[cols].dropna().to_csv(os.path.join(inputs_case,'hintage_data.csv'), index=False) - - #%%#################################################################################### - # -- Make plots comparing actual unit heatrates with binned ones, if desired -- # - ####################################################################################### - - make_plots = 0 - - # Make plots comparing actual unit heatrates with binned ones, if desired - if make_plots: - import matplotlib.pyplot as plt - # Create facet plots for heatrate, FO&M, VO&M, and online year - allgens = pd.merge( - tdat.loc[ - (tdat.RetireYear > 2020) & (tdat['onlineyear'] < 2020), - ['TECH','r','bin','HR','FOM','VOM','onlineyear','Summer.capacity']], - zout.loc[zout.yr==2020], - left_on=['r','TECH','bin'], - right_on=['r','TECH','bin_int'], - how='left') - - ## Summary scatter plot for all techs - plt.close() - plt.scatter(allgens['HR'],allgens['wHR'],c=allgens['Summer.capacity'],alpha=0.15) - plt.rcParams["figure.figsize"] = (7,10) - color_bar = plt.colorbar() - color_bar.set_label('Summer Capacity (MW)') - #Plot an abline of slope 1 for reference - x_vals = np.array((0,45000)) - y_vals = 1 * x_vals - plt.plot(x_vals, y_vals) - plt.title('Hintage Binning Results for All BAs and All Techs') - plt.xlabel("Actual Heat Rate (MMBtu / MWh)") - plt.ylabel("Binned Heat Rate (MMBtu / MWh)") - plt.savefig(os.path.join(inputs_case, 'hintage_data_2020_heatrate_binning_summary.png')) - - - ## Faceted scatter plot showing a fairly random selection of nine BAs for all techs - # Get color axes range to set static across subplots: - color_col = 'onlineyear' - bas_to_plot = ['p1','p2','p3','p50','p51','p52','p110','p120','p130'] - vmin_global = min([ allgens.loc[allgens['r']==ba,color_col].min() for ba in bas_to_plot]) - vgmax_global = max([ allgens.loc[allgens['r']==ba,color_col].max() for ba in bas_to_plot]) - - plt.close() - f = plt.figure() - f, axes = plt.subplots(nrows = 3, ncols = 3, figsize=(12,12), sharex=True, sharey = True) - - axes = axes.ravel() - for i,ba in zip(range(9),bas_to_plot): - im = axes[i].scatter(allgens.loc[allgens['r']==ba,'HR'], - allgens.loc[allgens['r']==ba,'wHR'], - c=allgens.loc[allgens['r']==ba,color_col], - vmin=vmin_global, - vmax=vgmax_global) - - #Plot an abline of slope 1 for reference - x_vals = np.array((5000,25000)) - y_vals = 1 * x_vals - axes[i].plot(x_vals, y_vals) - axes[i].set_title(ba) - - # Add common axis labels: - f.add_subplot(111, frame_on=False) - plt.tick_params(labelcolor="none", bottom=False, left=False) - plt.xlabel("Actual Heat Rate (MMBtu / MWh)") - plt.ylabel("Binned Heat Rate (MMBtu / MWh)",labelpad=20) - - f.subplots_adjust(right=0.8) - cbar_ax = f.add_axes([0.85,0.1,0.03,0.8]) - color_bar = f.colorbar(im, cax=cbar_ax) - color_bar.set_label(color_col) - - plt.savefig(os.path.join(inputs_case, 'hintage_data_2020_BA_binning_examples_all_techs.png')) - - - ## Faceted scatter plot showing the BA with the highest number of units for each tech - # (i.e. where our binning assumptions have the most impact) - color_col = 'onlineyear' - allgens.groupby(["r", "TECH"]).count().groupby('TECH').max() - bas_with_max_num_units_by_tech = ( - allgens.groupby(["r", "TECH"]).count() - .groupby('TECH').idxmax()['bin_x'].tolist() - ) - - # Get color axes range to set static across subplots: - vmin_global = min( - [allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech), color_col].min() - for ba,tech in bas_with_max_num_units_by_tech]) - vgmax_global = max( - [allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech), color_col].max() - for ba,tech in bas_with_max_num_units_by_tech]) - - # Create plot: - plt.close() - f = plt.figure() - num_cols = math.floor(math.sqrt(len(bas_with_max_num_units_by_tech))) - add_row = math.ceil(math.sqrt(len(bas_with_max_num_units_by_tech)) % num_cols) - - f, axes = plt.subplots( - nrows=(num_cols + add_row), ncols=num_cols, figsize=(14,12), sharex=True, sharey=True) - - axes = axes.ravel() - i=0 - for ba,tech in bas_with_max_num_units_by_tech: - im = axes[i].scatter( - allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),'HR'], - allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),'wHR'], - c=allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),color_col], - vmin=vmin_global, - vmax=vgmax_global) - - #Plot an abline of slope 1 for reference - x_vals = np.array((5000,25000)) - y_vals = 1 * x_vals - axes[i].plot(x_vals, y_vals) - - num_units = len(allgens.loc[(allgens['r']==ba) & (allgens['TECH']==tech),'HR']) - axes[i].set_title(f'{tech} in {ba}: {num_units} units') - - i += 1 - - # Add common axis labels: - f.add_subplot(111, frame_on=False) - plt.tick_params(labelcolor="none", bottom=False, left=False) - plt.xlabel("Actual Heat Rate (MMBtu / MWh)") - plt.ylabel("Binned Heat Rate (MMBtu / MWh)",labelpad=20) - - f.subplots_adjust(right=0.8) - cbar_ax = f.add_axes([0.85,0.1,0.03,0.8]) - color_bar = f.colorbar(im, cax=cbar_ax) - color_bar.set_label(color_col) - - plt.savefig( - os.path.join( - inputs_case, 'hintage_data_2020_BAs_with_max_num_units_of_each_tech.png')) - - - corrcoef = allgens['HR'].corr(allgens['wHR']) - print(f'Pearson correlation coefficient between actual and binned heat rates is {corrcoef}') - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== -if __name__ == '__main__': - - ### Parse arguments - parser = argparse.ArgumentParser() - parser.add_argument('reeds_path', type=str, help='Path to local ReEDS repo') - parser.add_argument('inputs_case', type=str) - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, logpath=os.path.join(inputs_case,'..','gamslog.txt')) - - #%% Run it - main(reeds_path=reeds_path, inputs_case=inputs_case) - - reeds.log.toc(tic=tic, year=0, process='input_processing/WriteHintage.py', - path=os.path.join(inputs_case,'..')) - - print('Finished WriteHintage.py') - \ No newline at end of file diff --git a/input_processing/__init__.py b/input_processing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/input_processing/aggregate_regions.py b/input_processing/aggregate_regions.py deleted file mode 100644 index 48679a77..00000000 --- a/input_processing/aggregate_regions.py +++ /dev/null @@ -1,1182 +0,0 @@ -""" -prbrown 20220421 -Notes to user: --------------- -* This script loops over files in runs/{}/inputs_case/ and agg/disaggregates - them based on the directions given in runfiles.csv. - * If new files have been added to inputs_case, you'll need to add rows with - processing directions to runfiles.csv. - * The column names should be self-explanatory; most likely there's also at least - one similarly-formatted file in inputs_case that you can copy the settings for. -* Some files are agg/disaggregated in other scripts: - * WriteHintage.py (these files are handled upstream since - aggregation affects the clustering of generators into (b/h/v)intages): - * hintage_data.csv -""" - -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import numpy as np -import os -import pandas as pd -import gdxpds -import shutil -import sys -import datetime -from glob import glob -from warnings import warn -import h5py -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -## Time the operation of this script -tic = datetime.datetime.now() - - -#%% =========================================================================== -### --- FUNCTIONS AND DICTIONARIES --- -### =========================================================================== - -the_unnamer = {'Unnamed: {}'.format(i): '' for i in range(1000)} - -aggfuncmap = { - 'mode': pd.Series.mode, -} - -def logprint(filepath, message): - print('{:-<45}> {}'.format(filepath+' ', message)) - - -def refilter_regions(df, region_cols, region_col, val_r_all): - ''' - This function is used to filter by region again for cases when only a subset of a model - balancing area is represented - ''' - dfout = df.copy() - if ('*r' in region_cols) and ('rr' in region_cols): - dfout = dfout.loc[dfout.index.get_level_values('*r').isin(val_r_all)] - dfout = dfout.loc[dfout.index.get_level_values('rr').isin(val_r_all)] - else: - dfout = dfout.loc[dfout.index.get_level_values(region_col).isin(val_r_all)] - - return dfout - - -def aggreg_methods( - df, row, aggfunc, region_cols, region_col, - r2aggreg, r_ba, disagg_data, sw, indexnames, columns, -): - df1 = df.copy() - ### Pre-aggregation: Map old regions to new regions - if aggfunc in ['sum','mean','first','min','sc_cat','resources']: - # Exception for dr_shed_hourly becuase the wide column contains tech|region - if 'dr_shed_hourly' in row.name: - # Separate tech and region from the 'wide' column - df1[['tech', 'region']] = df1['wide'].str.split('|', expand=True) - # Map regions to aggregated regions - df1['region'] = df1['region'].map(r2aggreg) - # Recombine tech and aggregated region into the 'wide' column - df1['wide'] = df1['tech'] + '|' + df1['region'] - else: - for c in region_cols: - df1[c] = df1[c].map(lambda x: r2aggreg.get(x,x)) - if row.i_col: - df1[row.i_col] = df1[row.i_col].map(lambda x: new_classes.get(x,x)) - - if aggfunc == 'sc_cat': - ## Weight cost by cap; if there's no cap, use 1 MW as weight - for cost_type in sc_cost_types: - ## Geothermal doesn't have all sc_cost_types - if cost_type in df1: - df1[f'cap_times_{cost_type}'] = df1['cap'].fillna(1).replace(0,1) * df1[cost_type] - ## Sum everything - df1 = df1.groupby(row.fix_cols+[region_col]).sum() - ## Divide cost*cap by cap - for cost_type in sc_cost_types: - if cost_type in df1: - df1[cost_type] = df1[f'cap_times_{cost_type}'] / df1['cap'].fillna(1).replace(0,1) - df1.drop([f'cap_times_{cost_type}'], axis=1, inplace=True) - elif aggfunc == 'trans_lookup': - ## Get data for anchor zones - for c in region_cols: - df1 = df1.loc[df1[c].isin(aggreg2anchorreg)].copy() - ## Map to aggregated regions - for c in region_cols: - df1[c] = df1[c].map(anchorreg2aggreg) - elif aggfunc == 'mean_cap': - df1 = ( - df1.rename(columns={'value':columns[-1]}) - .merge((rscweight_nobin.rename(columns={'i':'*i'}) if '*i' in row.fix_cols - else rscweight_nobin), - on=['r',('*i' if '*i' in row.fix_cols else 'i')], how='left') - ## There are some nan's because we subtract existing capacity from the supply curve. - ## Fill them with 1 MW for now, but it would be better to change that procedure. - ## Note also that the weighting will be off - .fillna(1) - ) - ### Similar procedure as above for aggfunc == 'sc_cat' - if row.i_col: - df1[row.i_col] = df1[row.i_col].map(lambda x: new_classes.get(x,x)) - df1 = ( - df1.assign(r=df1.r.map(r_ba)) - .assign(cap_times_cf=df1.cf*df1.MW) - .groupby(row.fix_cols+region_cols).sum() - ) - df1.cf = df1.cap_times_cf / df1.MW - df1 = df1.drop(['cap_times_cf','MW'], axis=1) - elif aggfunc == 'resources': - ### Special case: Rebuild the 'resources' column as {tech}|{region} - df1 = ( - df1.assign(resource=df1.i+'|'+df1.r) - .drop_duplicates() - ) - ### Special case: If calculating capacity credit by r, replace ccreg with r - if sw['capcredit_hierarchy_level'] == 'r': - df1 = df1.assign(ccreg=df1.r).drop_duplicates() - elif aggfunc in ['recf', 'csp']: - ## Get correct rscweight_nobin tech value - rsctech = os.path.splitext(row.name)[0].split('_')[1] - rscweight_nobin_tech = rscweight_nobin.loc[rscweight_nobin['i'].str.contains(rsctech)] - ### Region is embedded in the 'resources' column as {tech}|{region} - col2r = dict(zip(columns, [c.split('|')[-1] for c in columns])) - col2i = dict(zip(columns, [c.split('|')[0] for c in columns])) - df1 = df1.rename(columns={'value':'cf'}) - df1['r'] = df1[region_col].map(col2r) - df1['i'] = df1[region_col].map(col2i) - ## rscweight_nobin data from writesupplycurves.py has tech values of {rsctech}|{class} - ## so replicate this in order to merge for capacities - df1['i'] = f'{rsctech}_' + df1['i'] - - ## Get capacities - df1 = df1.merge(rscweight_nobin_tech, on=['r','i'], how='left') - ## Similar procedure as above for aggfunc == 'sc_cat' - df1['i'] = df1['i'].map(lambda x: new_classes.get(x,x)) - df1 = df1.rename(columns={'value':'cf'}) - df1 = ( - df1 - .assign(r=df1.r.map(r_ba)) - .assign(cap_times_cf=df1.cf*df1.MW) - .groupby(indexnames + ['i','r']).sum() - ) - df1.cf = df1.cap_times_cf / df1.MW - df1 = df1.rename(columns={'cf':'value'}).reset_index() - # Revert i column so rsctech is not included in the name. - # This ensures the resource h5 files will be in the same format when read in recf.py - # regardless of the spatial resolution - df1['i'] = df1['i'].str.replace(f'{rsctech}_','') - ### Remake the resources (column names) with new regions - df1['wide'] = df1.i + '|' + df1.r - df1 = df1.set_index(indexnames + ['wide'])[['value']].astype(np.float32) - elif aggfunc in ['sum','mean','first','min']: - # Exception for dr_shed_hourly becuase the wide column contains tech|region - if 'dr_shed_hourly' in row.name : - # Group by relevant columns and aggregate values - df1 = df1.groupby(['datetime', 'wide', 'year'], as_index=False).sum().drop(columns=['tech', 'region']) - else: - df1 = df1.groupby(row.fix_cols+region_cols).agg(aggfunc) - - ### Disaggregation methods -------------------------------------------------------------------- - elif aggfunc == 'uniform': - for rcol in region_cols: - df1 = ( - df1.merge(r_ba, left_on=rcol, right_on='ba', how='inner') - .drop(columns=[rcol,'ba']) - .rename(columns={'FIPS':rcol}) - ) - # if the fixed column is wide, then 'wide' needs to be an index as well - if (len(row.fix_cols) == 1) and (row.fix_cols[0] == 'wide'): - df1.set_index([region_col,'wide'],inplace=True) - else: - df1.set_index(row.fix_cols+region_cols,inplace=True) - df1 = refilter_regions(df1, region_cols,region_col, val_r_all) - elif aggfunc in ['population','geosize','hydroexist']: - if 'sc_cat' in columns: - # Split cap and cost - df1_cap = df1[df1['sc_cat']=='cap'] - df1_cost = df1[df1['sc_cat']=='cost'] - - # Disaggregate cap using the selected aggfunc - fracdata= disagg_data[aggfunc] - rcol = region_cols[0] - df1cols = (df1.columns) - valcol = df1cols[-1] - # Identify the columns to merge from the fracdata - fracdata_mergecols = (['PCA_REG'] + [col for col in fracdata.columns - if col not in ['PCA_REG','nonUS_PCA','FIPS','fracdata']]) - # Identify the columns to merge from the original data - df1_mergecols = [rcol] + [col for col in df1cols if col in fracdata_mergecols] - # Merge the datasets using PCA_REG - df1_cap = pd.merge(fracdata, df1_cap, left_on='PCA_REG', right_on= df1_mergecols, how='inner') - # Apply the weights to create a new value - df1_cap['new_value'] = (df1_cap['fracdata'].multiply(df1_cap[valcol], axis='index')) - # Clean up dataframe before grabbing final values - df1_cap.drop(columns=[valcol]+[rcol],inplace=True) - df1_cap.rename(columns={'new_value':valcol,'FIPS':rcol},inplace=True) - new_index = df1cols[:-1] - df1_cap = df1_cap.set_index(new_index.to_list()) - # Keep cost uniform, so map costs to all subregions with the PCA_REG - df1_cost = pd.merge(fracdata, df1_cost, left_on='PCA_REG', right_on= df1_mergecols, how='inner') - df1_cost['new_value'] = df1_cost[valcol] - df1_cost.drop(columns=[valcol]+[rcol],inplace=True) - df1_cost.rename(columns={'new_value':valcol,'FIPS':rcol},inplace=True) - new_index = df1cols[:-1] - df1_cost = df1_cost.set_index(new_index.to_list()) - # Combine cap and cost to get back into original format - df1 = pd.concat([df1_cap, df1_cost]) - df1 = refilter_regions(df1, region_cols, region_col,val_r_all) - - elif row.name =='dr_shed_hourly.h5' : - # separate tech | region - rcol = df1.wide.str.split('|',expand=True)[1] - fracdata = disagg_data[aggfunc] - df1cols = (df1.columns) - valcol = df1cols[-1] - # Identify the columns to merge from the fracdata - fracdata_mergecols = ['PCA_REG'] + [col for col in fracdata.columns - if col not in ['PCA_REG','nonUS_PCA','FIPS','fracdata']] - # Identify the columns to merge from the original data - df1_mergecols = [rcol] + [col for col in df1cols if col in fracdata_mergecols] - # Merge the datasets using PCA_REG - df1 = pd.merge(fracdata, df1, left_on=fracdata_mergecols, right_on=df1_mergecols, how='inner') - # Multiply BA shed by population fraction - df1['new_value'] = (df1['fracdata'].multiply(df1[valcol], axis='index')) - # Clean up dataframe - df1['wide'] = df1.wide.str.split('|',expand=True)[0] +'|' + df1['FIPS'] - df1 = (df1.drop(columns=[valcol,'PCA_REG','fracdata','FIPS']) - .rename(columns={'new_value':valcol})) - else: - # Disaggregate cap using the selected aggfunc - fracdata = disagg_data[aggfunc] - rcol = region_cols[0] - df1cols = (df1.columns) - valcol = df1cols[-1] - # Identify the columns to merge from the fracdata - fracdata_mergecols = ['PCA_REG'] + [col for col in fracdata.columns - if col not in ['PCA_REG','nonUS_PCA','FIPS','fracdata']] - # Identify the columns to merge from the original data - df1_mergecols = [rcol] + [col for col in df1cols if col in fracdata_mergecols] - # Merge the datasets using PCA_REG - df1 = pd.merge(fracdata, df1, left_on=fracdata_mergecols, right_on=df1_mergecols, how='inner') - # Clean up dataframe before grabbing final values - df1['new_value'] = (df1['fracdata'].multiply(df1[valcol], axis='index')) - df1 = (df1.drop(columns=[valcol]+[rcol]) - .rename(columns={'new_value':valcol,'FIPS':rcol})) - new_index = df1cols[:-1] - df1 = df1.set_index(new_index.to_list()) - df1 = refilter_regions(df1, region_cols,region_col, val_r_all) - - else: - raise ValueError(f'Invalid choice of aggfunc: {aggfunc} for {row.name}') - - ## Filter by regions again for cases when only a subset of a model balancing area is represented - if agglevel == 'county': - if ('*r' in region_cols) and ('rr' in region_cols): - df1 = df1.loc[df1.index.get_level_values('*r').isin(val_r_all)] - df1 = df1.loc[df1.index.get_level_values('rr').isin(val_r_all)] - elif row.name =='dr_shed_hourly.h5': - # separate tech | region to filter - df1 = df1.loc[df1.wide.str.split('|',expand=True)[1].isin(val_r_all)] - else: - df1 = df1.loc[df1.index.get_level_values(region_col).isin(val_r_all)] - - ################################ - ### Put back in original format ### - - if row.name == 'dr_shed_hourly.h5': - dfout = df1.set_index(['datetime','year','wide']).unstack('wide')['value'].reset_index() - elif (aggfunc == 'sc_cat') and (not row.wide): - dfout = df1.stack().rename(row.key).reset_index()[columns] - elif (aggfunc == 'sc_cat') and row.wide and (len(row.fix_cols) == 1): - dfout = df1.stack().rename('value').unstack('wide').reset_index()[columns].fillna(0) - elif not row.wide: - dfout = df1.reset_index()[columns] - elif (row.wide) and (region_col != 'wide') and (len(row.fix_cols) == 1) and (row.fix_cols[0] == 'wide'): - dfout = df1.unstack('wide')['value'].reset_index() - elif (row.wide) and (region_col != 'wide') and (len(row.fix_cols) > 1) and ('wide' in row.fix_cols): - # In some cases disaggregating to county level can lead to empty - # dataframes, so address that here - if df1.empty: - dfout = pd.DataFrame(columns = columns) - else: - dfout = df1.unstack('wide')['value'].reset_index()[columns] - elif row.wide and (region_col == 'wide') and len(row.fix_cols): - if (len(row.fix_cols) == 1): - dfout = ( - df1.reset_index() - .set_index(row.fix_cols) - .pivot(columns='wide', values='value') - .reset_index() - ) - else: - dfout = df1.unstack('wide')['value'].reset_index() - - ### Drop rows where r and rr are the same - if row.key == 'drop_dup_r': - dfout = dfout.loc[dfout[region_cols[0]] != dfout[region_cols[1]]].copy() - - ### Other special-case processing - if (row.name == 'hierarchy.csv') and (sw['capcredit_hierarchy_level'] == 'r'): - dfout = dfout.assign(ccreg=dfout[region_col]).drop_duplicates() - dfout['ccreg'].to_csv(os.path.join(inputs_case, 'ccreg.csv'), index=False) - - dfout.rename(columns=the_unnamer, inplace=True) - - return dfout - - -def reshape_to_long( - dfin, - filepath, - row, - indexnames, - aggfunc, - region_col, - region_cols, -): - if (aggfunc == 'sc_cat') and (not row.wide): - ### Supply-curve format. Expect an sc_cat column with 'cap' and 'cost' values. - ## 'cap' values are summed; 'cost' values use the 'cap'-weighted mean - df = dfin.pivot( - index=row.fix_cols+region_cols, - columns='sc_cat', - values=row.key, - ).reset_index() - elif (aggfunc == 'sc_cat') and row.wide and (len(row.fix_cols) == 1): - ### Supply-curve format. Expect an sc_cat column with 'cap' and 'cost' values. - ## Some value other than region is in wide format - ## So turn region and the wide value into the index - df = dfin.set_index(region_cols+['sc_cat']).stack().rename('value') - df.index = df.index.rename(region_cols+['sc_cat','wide']) - ## Make value columns for 'cap' and 'cost' - df = df.unstack('sc_cat').reset_index() - elif not row.wide: - ### File is already long so don't do anything - df = dfin.copy() - elif ( - row.wide - and (region_col != 'wide') - and (len(row.fix_cols) == 1) - and (row.fix_cols[0] == 'wide') - ): - ## Some value other than region is in wide format - ## So turn region and the wide value into the index - df = dfin.set_index(region_col).stack().rename('value') - df.index = df.index.rename([region_col, 'wide']) - ## Turn index into columns - df = df.reset_index() - elif ( - row.wide - and (region_col != 'wide') - and (len(row.fix_cols) > 1) - and ('wide' in row.fix_cols) - ): - ## Some value other than region is in wide format - ## So turn region, other fix_cols, and the wide value into the index - df = ( - dfin - .set_index([region_col]+[c for c in row.fix_cols if c != 'wide']) - .stack() - .rename('value') - ) - df.index = df.index.rename([region_col]+row.fix_cols) - ## Turn index into columns - df = df.reset_index() - elif row.wide and (region_col == 'wide') and len(row.fix_cols): - ### File has some fixed columns and then regions in wide format - df = ( - ## Turn all identifying columns into indices, with region as the last index - dfin.set_index(row.fix_cols).stack() - ## Name the region column 'wide' - .rename_axis(row.fix_cols+['wide']).rename('value') - ## Turn index into columns - .reset_index() - ) - - return df - - -def separate_wide_mixed_data(dfin,columns,fix_cols,agglevel_list): - # Separate mixed BA and county data in wide format - - reg_cols = [col for col in columns if col not in fix_cols] - if all('|' in col for col in reg_cols): - #Find columns with regions that are being solved at BA or aggreg resolution - # Some inputs files have tech and regions combined as column header and - # need to be filtered differently - keep_col = [] - for col in dfin.columns: - new_col = col.split('|') - for part in new_col: - if part in agglevel_list : - keep_col.append(col) - - df_sep_in = dfin[fix_cols + keep_col] - - else: - col_list = fix_cols + agglevel_list - #Check if data exists for all BAs in agglevel list - #Loop through each ba in ba region list and add to region_list if it doesn't appear in input data - regions_list = [] - for ba in agglevel_list : - if ba not in dfin.columns : - regions_list.append(ba) - #If there is a ba for which there is no data, exclude this ba from the column list - if len(regions_list) >0 : - reduced_ba_regions = [x for x in agglevel_list if x not in regions_list] - df_sep_in = dfin[fix_cols + reduced_ba_regions] - #If not, rewrite col_list to exclude BAs for which there is no data - else: - df_sep_in = dfin[col_list] - - return df_sep_in - - -def agg_disagg(filepath, r2aggreg_glob, r_ba_glob, runfiles_row): - """ - filepath: input file to be aggregated/disaggregated/ignored - r2aggreg_glob: ba to aggreg mapping needed for single resolution aggreg cases. - For mixed resolutions runs the r2aggreg parameter is re-defined separately - within the agg_disagg function for data that are being aggregated and disaggregated - r_ba_glob: r to ba mapping for single resolution runs. For mixed resolution runs r_ba is re-defined - separately within the agg_disagg function for data that are aggregated and disaggregated - row : consists of data in runfiles.csv row that correspond with the filepath - - """ - #%% Continue loop - row = runfiles_row.copy() - filetic = datetime.datetime.now() - filename = row.name - if row['aggfunc']=='ignore' and row['disaggfunc']=='ignore': - if verbose > 1: - logprint(filepath, 'ignored') - return - ### Ensure the correct aggfunc/disaggfunc is chosen for the given agglevel - # This will never be true for mixed resolution runs where agglevel is assigned more than one value - elif ((agglevel in ['ba','aggreg'] and row['aggfunc']=='ignore') - or (agglevel in ['county'] and row['disaggfunc']=='ignore')): - if verbose > 1: - logprint(filepath, 'ignored') - return - elif (row['aggfunc']!='ignore') or (row['disaggfunc']!='ignore'): - pass - - # In mixed resolution runs, some of the inputfiles will contain mixed ba-county data - # created in copy_files. This data does not need to be aggregated or disaggregated - # if the resolution selected is ['ba','county'], so skip the file. - if agglevel_variables['lvl'] == 'mult': - list_check = ['county','ba'] - list_check= sorted(list_check) - agglevel_check = sorted(agglevel) - if list_check == agglevel_check : - if row['disaggfunc']=='ignore': - return - - # If the file isn't in inputs_case, skip it - if filename not in inputfiles: - if verbose > 1: - logprint(filepath, 'skipped since not in inputs_case') - return - - #%%############## - ### Settings ### - - ### header: 0 if file has column labels, otherwise 'None' - header = (None if row['header'] in ['None','none','',None,np.nan] - else 'keepindex' if row['header'] == 'keepindex' - else int(row['header'])) - ### region_col: usually 'r', 'rb', or 'region', or 'wide' if file uses regions as columns - region_col = row['region_col'] - # Some datasets have both rb regions and cendiv regions. Only use the r - # regions for these datasets - if region_col == 'r_cendiv': - region_col = 'r' - region_cols = region_col.split(',') - # Assign variable to track if region data exists in two columns - two_col = False - if region_col == '*r,rr' or region_col =='r,rr' or region_col == 'transgrp,transgrpp': - two_col = True - region_col = region_col.split(',') - - # If solving at mixed resolutions, both disagg and agg functions could be required - # Check if one of the desired spatial resolutions is county - if agglevel_variables['lvl'] == 'mult': - for val in agglevel: - if val in ['county']: - aggfunc_agg = aggfuncmap.get(row['aggfunc'], row['aggfunc']) - aggfunc_disagg = aggfuncmap.get(row['disaggfunc'], row['disaggfunc']) - #Single resolution procedure - else: - ### Set aggfunc to the aggregation setting if using ba or aggreg, - ### and set to the disaggregation setting if using county - if agglevel in ['ba','aggreg']: - aggfunc = aggfuncmap.get(row['aggfunc'], row['aggfunc']) - elif agglevel in ['county']: - aggfunc = aggfuncmap.get(row['disaggfunc'], row['disaggfunc']) - - ### wide: 1 if any parameters are in wide format, otherwise 0 - row.wide = int(row.wide) - ### Get the filetype of the input file from the filename string - filetype = os.path.splitext(filename)[1].strip('.') - ### key: only used for gdx files, indicating the parameter name. - ### gdx files need a separate line in runfiles.csv for each parameter. - ### fix_cols: indicate columns to use as for fields that should be projected - ### independently to future years (e.g. r, szn, tech) - row.fix_cols = ( - [] if row.fix_cols in ['None', 'none', '', None, np.nan] - else row.fix_cols.split(',') - ) - ### i_col: indicate technology column. Only used/needed if aggregating techs. - if row.i_col in ['None', 'none', '', np.nan]: - row.i_col = None - # indexnames will get overwritten for h5 files but needs to be defined for the aggreg_methods function - indexnames = None - - - #%%################### - ### Load the file ### - - if filetype in ['csv', 'gz']: - # Some csv files are empty and therefore cannot be opened. If that is - # the case, then skip them. - try: - dfin = pd.read_csv( - os.path.join(inputs_case, filepath), header=header, - dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, - ) - except pd.errors.EmptyDataError: - return - elif filetype == 'h5': - # Skip empty files - with h5py.File(os.path.join(inputs_case, filepath), 'r') as f: - if len(f.keys()) == 0: - return - - dfin = reeds.io.read_file(os.path.join(inputs_case, filepath)).copy() - if header == 'keepindex': - indexnames = (dfin.index.names) - if (len(indexnames) == 1) and (not indexnames[0]): - indexnames = ['index'] - dfin = dfin.reset_index() - elif filetype == 'gdx': - ### Read in the full gdx file, but only change the 'key' parameter - ### given in runfiles. That's wasteful, but there are currently no - ### active gdx files. - dfall = gdxpds.to_dataframes(os.path.join(inputs_case, filepath)) - dfin = dfall[row.key] - else: - raise Exception(f'Unsupported filetype: {filepath}') - - dfin.rename(columns={c:str(c) for c in dfin.columns}, inplace=True) - columns = dfin.columns.tolist() - ### Stop now if it's empty - if dfin.empty: - if verbose > 1: - logprint(filepath, 'empty') - return - - - #%%############################ - ### Reshape to long format ### - - ########## Mixed Resolution Procedure ########## - if agglevel_variables['lvl'] == 'mult': - ########## BA ########## - # If desired resolution is ['county', 'ba'], then BA data are already in correct format - # Filter dataframe to regions being solved at BA resolution - for val in agglevel: - if val in ['ba']: - if region_col != 'wide': - if two_col : - df_agg_in = dfin[dfin[region_col[0]].isin( - agglevel_variables['ba_regions'] + agglevel_variables['ba_transgrp'])] - df_agg_in = df_agg_in[ df_agg_in[region_col[1]].isin(agglevel_variables['ba_regions'])] - else: - df_agg_in = dfin[dfin[region_col].isin(agglevel_variables['ba_regions'])] - else: - col_list = row.fix_cols + agglevel_variables['ba_regions'] - # Check if data exists for all BAs in agglevel_variables['ba_regions'] list - # Loop through each ba in ba region list and add to region_list if it doesn't appear in input data - regions_list = [] - for ba in agglevel_variables['ba_regions'] : - if ba not in dfin.columns : - regions_list.append(ba) - # If there is a ba for which there is no data, exclude this ba from the column list - if len(regions_list) >1 : - reduced_ba_regions = [x for x in agglevel_variables['ba_regions'] if x not in regions_list] - df_agg_in = dfin[row.fix_cols + reduced_ba_regions] - #If not, rewrite col_list to exclude BAs for which there is no data - else: - df_agg_in = dfin[[c for c in col_list if c in dfin]].copy() - - df_agg = df_agg_in - - elif val in ['aggreg']: - # Subset to include regions being solved at Aggreg - # If column headers are region names, need to filter after reformatting - if region_col != 'wide': - if two_col : - df_agg_in = dfin[dfin[region_col[0]].isin( - agglevel_variables['ba_regions'] + agglevel_variables['ba_transgrp'])] - df_agg_in = df_agg_in[ df_agg_in[region_col[1]].isin(agglevel_variables['ba_regions'])] - else: - df_agg_in = dfin[dfin[region_col].isin(agglevel_variables['ba_regions'])] - - # Clause to separate mixed BA and county data in wide format - elif region_col == 'wide': - df_agg_in = separate_wide_mixed_data( - dfin, - columns, - row.fix_cols, - agglevel_variables['ba_regions'], - ) - - #Set aggfunc to aggregation function - aggfunc = aggfunc_agg - - ##### Reformat BA Data #### - df_agg = reshape_to_long( - df_agg_in, - filepath, - row, - indexnames, - aggfunc, - region_col, - region_cols, - ) - - ########## County ########## - # When aggfunc_disagg is set to ignore the county-level data do not need to be reformatted. - # Filter county data from BA data that will be aggregated to aggreg - if aggfunc_disagg == 'ignore': - if region_col != 'wide': - if two_col: - # County transmission data will have county-ba interfaces created in copy_files - # To maintain these interfaces the filtering of the data needs to ensure that BA-BA interfaces - # are dropped but county-county and county-BA interfaces are kept - df_disagg_list = [] - for idx, tx_row in dfin.iterrows(): - cond1 = ((tx_row[region_col[0]] in agglevel_variables['ba_regions']+agglevel_variables['ba_transgrp']) - and (tx_row[region_col[1]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp'])) - cond2 = ((tx_row[region_col[1]] in agglevel_variables['ba_regions']+ agglevel_variables['ba_transgrp']) - and (tx_row[region_col[0]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp'])) - cond3 = ((tx_row[region_col[0]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp']) - and (tx_row[region_col[1]] in agglevel_variables['county_regions']+agglevel_variables['county_transgrp'])) - - if cond1 or cond2 or cond3: - df_disagg_list.append(tx_row) - - df_disagg_in = pd.DataFrame(df_disagg_list).drop_duplicates() - - elif filename == 'county2zone.csv': - df_disagg_in = dfin[dfin[region_col].isin(agglevel_variables['county_regions2ba'])] - else: - df_disagg_in = dfin[dfin[region_col].isin(agglevel_variables['county_regions'])] - # Clause to separate mixed BA and county data in wide format - elif region_col == 'wide': - df_disagg_in = separate_wide_mixed_data( - dfin, - columns, - row.fix_cols, - agglevel_variables['county_regions'], - ) - - # Files where aggfunc_disagg is 'ignore' but aggfunc_agg is NOT 'ignore' - # indicate that the file will already have county-level data - # (e.g. csp.h5, recf.h5, load.h5). Copy the separated county-level columns - # to df_disagg so the script can add them back to dfout once the ba-level data - # is aggregated - if aggfunc_agg != 'ignore': - df_disagg = df_disagg_in.copy() - - else: - # Need to read in input data at BA level to be disaggregated - # If column headers are region names, need to filter after reformatting - if region_col != 'wide': - if two_col : - df_disagg_in = dfin[dfin[region_col[0]].isin(agglevel_variables['county_regions2ba'])] - df_disagg_in = df_disagg_in[df_disagg_in[region_col[1]] - .isin(agglevel_variables['county_regions2ba'])] - else: - df_disagg_in = dfin[dfin[region_col].isin(agglevel_variables['county_regions2ba'])] - else: - col_list = row.fix_cols + agglevel_variables['county_regions2ba'] - # Check if data exists for all BAs in county_regions2ba list - regions_list = [] - for ba in agglevel_variables['county_regions2ba']: - if ba not in dfin.columns : - regions_list.append(ba) - # If there is a ba for which there is no data, exclude this ba from the column list - if len(regions_list) >1 : - reduced_ba_regions = [x for x in agglevel_variables['county_regions2ba'] if x not in regions_list] - df_disagg_in = dfin[row.fix_cols + reduced_ba_regions] - else: - df_disagg_in = dfin[col_list] - - #Set aggfunc to disaggregation function - aggfunc = aggfunc_disagg - - #####Reformat county Data #### - df_disagg = reshape_to_long( - df_disagg_in, - filepath, - row, - indexnames, - aggfunc, - region_col, - region_cols, - ) - - ########### Single Resolution Procedure ########### - else: - df = reshape_to_long( - dfin, - filepath, - row, - indexnames, - aggfunc, - region_col, - region_cols, - ) - - #If the file is empty, move on to the next one as there is nothing to aggregate - if df.empty: - if verbose > 1: - logprint(filepath, 'empty') - return - - #%%####################################### - ### Aggregate/Dissaggregate by Region ### - - ########## Mixed Resolution Procedure ########## - if agglevel_variables['lvl'] == 'mult': - ########## BA, Aggreg ########## - # If there are no BA level data, set BA addition to False - # This will prevent appending an empty dataframe to the county level data below - if df_agg.empty: - ba_addition = False - else: - ba_addition = True - # Set aggregation function to aggfunc column value (Done in Settings section above) - aggfunc = aggfunc_agg - # Check if solving at aggreg, otherwise aggregating BA data to lower resolution is not necessary - if 'aggreg' in agglevel: - r2aggreg = r_aggreg - r_ba = r_aggreg - - # If aggfunc is not 'ignore', perform aggregation - if aggfunc != 'ignore': - df_agg = aggreg_methods( - df_agg, row, aggfunc, region_cols, region_col, - r2aggreg, r_ba, disagg_data, sw, indexnames, columns, - ) - - ########## County ########## - - # If there are no county level data, set county addition to False - # This will prevent appending an empty dataframe to the BA level data below - if df_disagg.empty: - county_addition = False - else: - county_addition = True - # Set aggregation function to aggfunc column value (Done in Settings section above) - aggfunc = aggfunc_disagg - # If aggfunc is not 'ignore', perform disaggregation - if aggfunc != 'ignore': - # Ensure correct r_ba is used - r_ba = r_ba_for_county - r2aggreg = r_ba - - # Disagg for county - df_disagg = aggreg_methods( - df_disagg, row, aggfunc, region_cols, region_col, - r2aggreg, r_ba, disagg_data, sw, indexnames, columns, - ) - - # Combine aggregated and disaggregated data - if ba_addition and county_addition: - # Combined BA and county data - if region_col == 'wide': - dfout = pd.merge(df_agg, df_disagg, how='inner', on=row.fix_cols) - else: - dfout = pd.concat([df_agg,df_disagg]) - # Aggregated data only - if ba_addition and not county_addition: - dfout = df_agg.copy() - # Disaggregated data only - if not ba_addition and county_addition: - dfout = df_disagg.copy() - - # If neither data exists, skip file - if not ba_addition and not county_addition: - if verbose > 1: - logprint(filepath, 'empty') - return - - - ########## Single Resolution Procedure ########## - else: - if agglevel_variables['lvl'] == 'county': - r2aggreg = r_county - else: - r2aggreg = r2aggreg_glob - r_ba = r_ba_glob - dfout = aggreg_methods( - df, row, aggfunc, region_cols, region_col, - r2aggreg, r_ba, disagg_data, sw, indexnames, columns, - ) - - #%%#################### - ### Data Write-Out ### - - if filetype in ['csv','gz']: - dfout.round(decimals).to_csv( - os.path.join(inputs_case, filepath), - header=(False if header is None else True), - index=False, - ) - elif filetype == 'h5': - if header == 'keepindex': - dfwrite = dfout.sort_values(indexnames).set_index(indexnames) - dfwrite.columns.name = None - else: - dfwrite = dfout - reeds.io.write_profile_to_h5(dfwrite, filepath, inputs_case) - elif filetype == 'gdx': - ### Overwrite the projected parameter - dfall[row.key] = dfout.round(decimals) - ### Write the whole file - gdxpds.to_gdx(dfall, inputs_case+filepath) - - if verbose > 1: - now = datetime.datetime.now() - logprint( - filepath, - 'aggregated ({:.1f} seconds)'.format((now-filetic).total_seconds())) - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -#%% Parse arguments -parser = argparse.ArgumentParser(description='Extend inputs to arbitrary future year') -parser.add_argument('reeds_path', help='path to ReEDS directory') -parser.add_argument('inputs_case', help='path to inputs_case directory') - -args = parser.parse_args() -reeds_path = args.reeds_path -inputs_case = os.path.join(args.inputs_case) - -# #%%## Settings for testing -# reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -# inputs_case = os.path.join( -# reeds_path,'runs','v20250301_hydroM0_Pacific','inputs_case') - -#%% Settings for debugging -### Set debug == True to copy the original files to a new folder (inputs_case_original). -### If debug == False, the original files are overwritten. -debug = True -### missing: 'raise' or 'warn' -missing = 'raise' -### verbose: 0, 1, 2 -verbose = 2 - -#%%################# -### FIXED INPUTS ### -decimals = 6 -### anchortype: 'load' sets rb with largest 2010 load as anchor reg; -### 'size' sets largest rb as anchor reg -anchortype = 'size' -### Types of cost data in files that use sc_cat -sc_cost_types = ['cost', 'cost_cap', 'cost_trans'] - -###################### -### DERIVED INPUTS ### -### Get the case name (ReEDS-2.0/runs/{casename}/inputscase/) -casename = inputs_case.split(os.sep)[-3] - -#%% Set up logger -log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case, '..', 'gamslog.txt'), -) - -#%% Inputs from switches -sw = pd.read_csv( - os.path.join(inputs_case, 'switches.csv'), header=None, index_col=0).squeeze(1) -endyear = int(sw.endyear) -GSw_CSP_Types = [int(i) for i in sw.GSw_CSP_Types.split('_')] - -scalars = pd.read_csv( - os.path.join(inputs_case, 'scalars.csv'), - header=None, usecols=[0,1], index_col=0).squeeze(1) - -# Use agglevel_variables function to obtain spatial resolution variables -agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) -agglevel = agglevel_variables['agglevel'] -# Regions present in the current run -val_r_all = sorted( - pd.read_csv( - os.path.join(inputs_case, 'val_r_all.csv'), header=None, - ).squeeze(1).tolist() -) -#%% -#DEBUG: Copy the original inputs_case files -if debug and (agglevel != 'ba'): - print('Copying original inputs_case file...') - import distutils.dir_util - os.makedirs(inputs_case+'_original', exist_ok=True) - distutils.dir_util.copy_tree(inputs_case, inputs_case+'_original', verbose=0) -#%% -### Mixed Resolution Procedure ### -if agglevel_variables['lvl'] == 'mult' : - # Get the various region maps created in copy_files.py - #Need to store separate r_ba values for county and BA data - r_county = pd.read_csv(os.path.join(inputs_case,'r_county.csv'), index_col='county').squeeze() - r_ba = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')) - r_ba_for_county = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')).rename(columns={'r':'FIPS'}) - r_ba_for_ba = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')).set_index('ba').squeeze() - - for val in agglevel_variables['agglevel'] : - if val == 'aggreg': - ### Get map from BA to aggreg - r_aggreg = pd.read_csv(os.path.join(inputs_case,'rb_aggreg.csv')).set_index('ba')['aggreg'].to_dict() - - -### Single Resolution Procedure ### -else: - # Get the various region maps created in copy_files.py - r_county = pd.read_csv(os.path.join(inputs_case,'r_county.csv'), index_col='county').squeeze() - r_ba = pd.read_csv(os.path.join(inputs_case,'r_ba.csv')) - # r_ba needs to be in different formats depending on whether you are aggregating - # or disaggregating - if agglevel in ['county']: - r_ba.rename(columns={'r':'FIPS'}, inplace=True) - elif agglevel in ['ba','aggreg']: - r_ba = r_ba.set_index('ba').squeeze() - ### Make all-regions-to-aggreg map - r2aggreg = pd.concat([r_county, r_ba]) - -##################################################### -### If using default 134 regions, exit the script ### - -if agglevel == 'ba': - print('all valid regions are BA, so skip aggregate_regions.py') - quit() -else: - print('Starting aggregate_regions.py', flush=True) - ## Read in disaggregation data - disagg_data = { - 'population': pd.read_csv( - os.path.join(inputs_case,'disagg_population.csv'), - header=0, - dtype={'fracdata':np.float32}, - usecols=["PCA_REG", "FIPS", "fracdata"] - ), - 'geosize': pd.read_csv( - os.path.join(inputs_case,'disagg_geosize.csv'), - header=0, - usecols=["PCA_REG", "FIPS", "fracdata"] - ), - 'hydroexist': pd.read_csv( - os.path.join(inputs_case,'disagg_hydroexist.csv'), - header=0, - usecols=["PCA_REG", "FIPS", "i", "fracdata"] - ) - } - -#%%###################################################### -### Get the "anchor" zone for each aggregation region ### - -# For transmission we want to use the old endpoints to avoid requiring a new run of the -# reV least-cost-paths procedure. - -if 'aggreg' in agglevel: - if agglevel_variables['lvl'] == 'mult': - r_ba = r_aggreg - - # Written in reeds.io.get_zonemap() - reeds.io.get_zonemap(reeds.io.standardize_case(inputs_case)) - aggreg2anchorreg = pd.read_csv(os.path.join(inputs_case, 'aggreg2anchorreg.csv')) - aggreg2anchorreg = aggreg2anchorreg.set_index('aggreg') - aggreg2anchorreg = aggreg2anchorreg.squeeze() - anchorreg2aggreg = pd.Series(index=aggreg2anchorreg.values, data=aggreg2anchorreg.index) - - ### Get RSC VRE available capacity to use in capacity-weighted averages - ### We need the original un-aggregated supply curves, so run writesupplycurves again - # rscweight = pd.read_csv(os.path.join(inputs_case, 'rsc_combined.csv')) - - # Read generator database and create rsc_wsc (for use in writesupplycurves function call below) - gendb = pd.read_csv(os.path.join(inputs_case,'unitdata.csv')) - import writecapdat - from writecapdat import create_rsc_wsc - # Set the 'r' column for the generator database - # Create the 'r_col' column - gendb = gendb.assign(r=gendb.reeds_ba.map(r_ba)) - startyear = int(sw.startyear) - rsc_wsc = create_rsc_wsc(gendb, TECH=writecapdat.TECH, scalars=scalars,startyear=startyear) - - import writesupplycurves - rscweight = writesupplycurves.main( - reeds_path, inputs_case, AggregateRegions=0, rsc_wsc_dat=rsc_wsc, write=False) - rscweight = ( - rscweight.loc[(rscweight.sc_cat=='cap')] - .rename(columns={'*i':'i'}) - .drop_duplicates(subset=['i','r','rscbin']) - [['i','r','rscbin','value']].rename(columns={'value':'MW'}) - ).copy() - - ## Add PVB values to rscweight_nobin in case we need them - rscweight_nobin = rscweight.groupby(['i','r'], as_index=False).sum(numeric_only=True) - pvbtechs = [f'pvb{i}' for i in sw.GSw_PVB_Types.split('_')] - tocopy = rscweight_nobin.loc[rscweight_nobin.i.str.startswith('upv')].copy() - rscweight_nobin = pd.concat( - [rscweight_nobin] - + [tocopy.assign(i=tocopy.i.str.replace('upv',pvbtech)) for pvbtech in pvbtechs] - ) - ## Remove duplicate CSP values for different solar multiples - rscweight_nobin.i.replace( - {f'csp{i+1}_{c+1}': f'csp_{c+1}' - for i in GSw_CSP_Types - for c in range(int(sw.GSw_NumCSPclasses))}, - inplace=True - ) - rscweight_nobin.drop_duplicates(['i','r'], inplace=True) - -# rscweight_nobin required to be defined for aggreg_methods function call to work -else: - rscweight_nobin=None - -#%% Get the mapping to reduced-resolution technology classes -original_num_classes = {**{f'csp{i}':12 for i in range(1,5)}} -new_classes = {} -for tech in [f'csp{i}' for i in range(1,5)]: - GSw_NumClasses = int(sw['GSw_Num{}classes'.format(tech.upper().strip('1234'))]) - ## Spread the new classes roughly evenly out over the old classes - num_in_step = original_num_classes[tech] // GSw_NumClasses - remainder = original_num_classes[tech] % GSw_NumClasses - new_classes[tech] = sorted( - list(np.ravel([[i]*num_in_step for i in range(1,GSw_NumClasses+1)])) - + list(range(1,remainder+1)) - ) - new_classes[tech] = dict(zip( - [f'{tech}_{i}' for i in range(1,original_num_classes[tech]+1)], - [f'{tech}_{i}' for i in new_classes[tech]] - )) -### Combine all the new classes into one dictionary -new_classes = {k:v for d in new_classes for k,v in new_classes[d].items()} - -#%% Get the settings file -runfiles = ( - pd.read_csv( - os.path.join(reeds_path, 'runfiles.csv'), - dtype={'fix_cols':str}, index_col='filename', - comment='#', - ).fillna({'fix_cols':''}) - .rename(columns={'wide (1 if any parameters are in wide format)':'wide', - 'header (0 if file has column labels)':'header'}) - ) -#%% If any files are missing, stop and alert the user -inputfiles = sorted([ - f.split('inputs_case'+os.sep)[1] - for f in glob(os.path.join(inputs_case,'**'), recursive=True) - if 'metadata' not in f -]) -## Drop the directories and backup h17 files -inputfiles = [f for f in inputfiles if (('.' in f) and not f.endswith('_h17.csv'))] -missingfiles = [f for f in inputfiles if (os.path.basename(f) not in runfiles.index.values) -] -if any(missingfiles): - if missing == 'raise': - raise Exception( - 'Missing aggregation method for:\n{}\n' - '>>> Need to add entries for these files to runfiles.csv' - .format('\n'.join(missingfiles)) - ) - else: - from warnings import warn - warn( - 'Missing aggregation directions for:\n{}\n' - '>>> For this run, these files are copied without modification' - .format('\n'.join(missingfiles)) - ) - for f in missingfiles: - shutil.copy(os.path.join(inputs_case, f), os.path.join(inputs_case, f)) - print(f'copied {f}, which is missing from runfiles.csv') - -#%% Maps (special case) -mapsfile = os.path.join(inputs_case, 'maps.gpkg') -if os.path.exists(mapsfile): - os.remove(mapsfile) -dfmap = reeds.io.get_dfmap(os.path.dirname(inputs_case)) -for level in dfmap: - dfmap[level].rename_axis(level).to_file(mapsfile, layer=level) - -dfmap = reeds.io.get_dfmap(os.path.dirname(inputs_case)) - -### Aggregate or disaggregate the 'r' map; none of the rest should change -# Mixed resolution maps are patched together in the get_zonemap() function -if agglevel_variables['lvl'] == 'mult' : - pass - -#Single resolution procedure -else: - match agglevel: - case 'aggreg': - r2aggreg = pd.read_csv( - os.path.join(inputs_case, 'hierarchy_original.csv') - ).rename(columns={'ba':'r'}).set_index('r').aggreg - case 'county': - aggreg2anchorreg = r2aggreg = r_county.copy() - - - dfmap_r_agg = dfmap['r'].reset_index().rename(columns={'rb':'r', 'ba':'r'}) - dfmap_r_agg.r = dfmap_r_agg.r.map(r2aggreg) - dfmap_r_agg = dfmap_r_agg.dissolve('r').loc[aggreg2anchorreg.index].copy() - - ## Map endpoints to anchor regions - for j in ['x','y']: - dfmap_r_agg[j] = dfmap['r'][j].loc[dfmap_r_agg[j].index.map(aggreg2anchorreg)].values - dfmap_r_agg[f'centroid_{j}'] = dfmap_r_agg.centroid.x if j == 'x' else dfmap_r_agg.centroid.y - - ## Overwrite the non-aggregated zone map - dfmap['r'] = dfmap_r_agg.drop(columns='county', errors='ignore') - - ## Write the aggregated maps - mapsfile = os.path.join(inputs_case, 'maps.gpkg') - if os.path.exists(mapsfile): - os.remove(mapsfile) - for level in dfmap: - ( - dfmap[level] - .drop(columns='aggreg', errors='ignore') - .rename_axis(level) - .to_file(mapsfile, layer=level) - ) - -#%% -if agglevel_variables['lvl'] == 'mult' or agglevel == 'county': - r2aggreg_glob = None -else: - r2aggreg_glob = r2aggreg -r_ba_glob = r_ba - -# loop over inputfiles from runfiles and call aggregation/disaggregation function -for filepath in inputfiles: - ### For debugging: Specify a file - # filepath = '' - ### Get the appropriate row from runfiles - row = runfiles.loc[os.path.basename(filepath)] - try: - agg_disagg(filepath, r2aggreg_glob, r_ba_glob, row) - except Exception as err: - print(f"Error processing {filepath}") - raise Exception(err) - -#%% Finish -reeds.log.toc(tic=tic, year=0, process='input_processing/aggregate_regions.py', - path=os.path.join(inputs_case,'..')) - -print('Finished aggregate_regions.py') diff --git a/input_processing/calc_financial_inputs.py b/input_processing/calc_financial_inputs.py deleted file mode 100644 index c0053265..00000000 --- a/input_processing/calc_financial_inputs.py +++ /dev/null @@ -1,526 +0,0 @@ -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import pandas as pd -import numpy as np -import os -import sys -import datetime -# Time the operation of this script -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -def calc_financial_inputs(inputs_case): - """ - Write the following files to runs/{batch_case}/inputs_case/: - - cap_cost_mult_noITC.csv - - co2_capture_incentive.csv - - crf.csv - - crf_co2_incentive.csv - - crf_h2_incentive.csv - - depreciation_schedules.csv - - h2_ptc.csv - - inflation.csv - - itc_fractions.csv - - ivt.csv - - ptc_values.csv - - ptc_value_scaled.csv - - pvf_onm_int.csv - - pvf_cap.csv - - reg_cap_cost_diff.csv - - retail_eval_period.h5 - - retail_depreciation_sch.h5 - """ - print('Starting calculation of financial parameters') - - # #%% Settings for testing - # reeds_path = '/Users/pbrown/github/ReEDS-2.0/' - # inputs_case = os.path.join(reeds_path,'runs','v20220621_NTPm0_ercot_seq_test','inputs_case') - - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - sw['endyear'] = int(sw['endyear']) - sw['sys_eval_years'] = int(sw['sys_eval_years']) - - scalars = reeds.io.get_scalars(inputs_case) - - #%% Import some general data and maps - - # Import inflation (which includes both historical and future inflation). - # Used for adjusting currency inputs to the specified dollar year, and financial calculations. - inflation_df = pd.read_csv(os.path.join(inputs_case,'inflation.csv')) - - # Import tech groups. Used to expand data inputs - # (e.g., 'UPV' expands to all of the upv subclasses, like upv_1, upv_2, etc) - tech_groups = reeds.techs.import_tech_groups(os.path.join(inputs_case, 'tech-subset-table.csv')) - - # Set up scen_settings object - scen_settings = reeds.financials.scen_settings( - dollar_year=int(sw['dollar_year']), tech_groups=tech_groups, inputs_case=inputs_case, - sw=sw) - - - #%% Ingest data, determine what regions have been specified, and build df_ivt - # Build df_ivt (for calculating various parameters at subscript [i,v,t]) - - techs = pd.read_csv(os.path.join(inputs_case,'techs.csv')) - techs = reeds.techs.expand_GAMS_tech_groups(techs) - vintage_definition = pd.read_csv(os.path.join(inputs_case, 'ivt.csv')).rename(columns={'Unnamed: 0':'i'}) - - annual_degrade = pd.read_csv(os.path.join(inputs_case,'degradation_annual.csv'), - header=None, names=['i','annual_degradation']) - annual_degrade = reeds.techs.expand_GAMS_tech_groups(annual_degrade) - ### Assign the PV+battery values to values for standalone batteries - annual_degrade = reeds.financials.append_pvb_parameters( - dfin=annual_degrade, - tech_to_copy='battery_li') - - years, modeled_years, year_map = reeds.financials.ingest_years( - inputs_case, sw['sys_eval_years'], sw['endyear']) - - df_ivt = reeds.financials.build_dfs(years, techs, vintage_definition, year_map) - print('df_ivt created') - - #%% Import and merge data onto df_ivt - - # Import system-wide real discount rates, calculate present-value-factors, merge onto df's - financials_sys = reeds.financials.import_sys_financials( - sw['financials_sys_suffix'], inflation_df, modeled_years, - years, year_map, sw['sys_eval_years'], scen_settings, scalars['co2_capture_incentive_length'],scalars['h2_ptc_length']) - financials_sys.to_csv(os.path.join(inputs_case,'financials_sys.csv'),index=False) - df_ivt = df_ivt.merge( - financials_sys[['t', 'pvf_capital', 'crf', 'crf_co2_incentive','crf_h2_incentive','d_real', 'd_nom', 'interest_rate_nom', - 'tax_rate', 'debt_fraction', 'rroe_nom']], - on=['t'], how='left') - - # The script only works for USA currently. Something needs to be added here - # to expand to other countries, if needed. - df_ivt['country'] = 'usa' - - - # Merge inflation into investment df - df_ivt = df_ivt.merge(inflation_df, on=['t'], how='left', ) - - # Merge annual degradation into investment df - df_ivt = df_ivt.merge(annual_degrade, on=['i'], how='left') - df_ivt['annual_degradation'] = df_ivt['annual_degradation'].fillna(0.0) - - ### Import financial assumptions - financials_tech = reeds.financials.import_data( - file_root='financials_tech', file_suffix=sw['financials_tech_suffix'], - indices=['i','country','t'], scen_settings=scen_settings) - # Apply the values for standalone batteries to PV+B batteries - financials_tech = reeds.financials.append_pvb_parameters( - dfin=financials_tech, - tech_to_copy='battery_li') - # If the battery in PV+B gets the ITC, it gets 5-year MACRS depreciation as well - if float(scen_settings.sw['GSw_PVB_BatteryITC']) >= 0.75: - financials_tech.loc[ - financials_tech.i.str.startswith('pvb') & (financials_tech.country == 'usa'), - 'depreciation_sch' - ] = 5 - - ### Project financials_tech forward - financials_tech_projected = financials_tech.pivot( - index=['i','country','depreciation_sch','eval_period','construction_sch'], - columns=['t'])['finance_diff_real'] - lastdatayear = max(financials_tech_projected.columns) - for addyear in range(lastdatayear+1, sw['endyear']+1): - financials_tech_projected[addyear] = financials_tech_projected[lastdatayear] - # Overwrite with projected values - financials_tech = financials_tech_projected.stack().rename('finance_diff_real').reset_index() - # Merge with df_ivt - df_ivt = df_ivt.merge(financials_tech, on=['i', 't', 'country'], how='left') - - # Calculate multipliers to account for evaluation periods. If a tech's eval period is - # the system-wide default, this will be 1. If not, the capital costs are adjusted accordingly. - financials_sys['sys_pvf_eval_period_sum'] = (1 - (1 / (financials_sys['d_real'])**(sw['sys_eval_years']-1))) / (financials_sys['d_real']-1.0) + 1 - df_ivt['pvf_eval_period_sum'] = (1 - (1 / (df_ivt['d_real'])**(df_ivt['eval_period']-1))) / (df_ivt['d_real']-1.0) + 1 - df_ivt = df_ivt.merge(financials_sys[['sys_pvf_eval_period_sum', 't']], on='t', how='left') - df_ivt['eval_period_adj_mult'] = df_ivt['sys_pvf_eval_period_sum'] / df_ivt['pvf_eval_period_sum'] - - #%% Process incentives - - # Import incentives, shift eligibility by safe harbor, merge incentives - incentive_df = reeds.financials.import_and_mod_incentives( - incentive_file_suffix=sw['incentives_suffix'], - inflation_df=inflation_df, scen_settings=scen_settings) - df_ivt = df_ivt.merge(incentive_df, on=['i', 't', 'country'], how='left') - df_ivt['safe_harbor'] = df_ivt['safe_harbor'].fillna(0.0) - df_ivt['co2_capture_value_monetized'] = df_ivt['co2_capture_value_monetized'].fillna(0.0) * (1 / (1 - df_ivt['tax_rate'])) - df_ivt['h2_ptc_value_monetized'] = df_ivt['h2_ptc_value_monetized'].fillna(0.0) * (1 / (1 - df_ivt['tax_rate'])) - - ### Calculate the tax impacts of the PTC, and calculate the adjustment to reflect the - # difference between the PTC duration and ReEDS evaluation period - df_ivt = reeds.financials.adjust_ptc_values(df_ivt) - - # Expand co2_capture_value by the duration of the incentive. - co2_capture_value = df_ivt[['i', 'v', 't', 'co2_capture_value_monetized', 'co2_capture_dur']].copy() - co2_capture_value = co2_capture_value[co2_capture_value['co2_capture_value_monetized']>0] - if len(co2_capture_value) > 0: - dur_list = [] # create year expander - for n in list(co2_capture_value['co2_capture_dur'].drop_duplicates()): - dur_df = pd.DataFrame() - dur_df['year_adder'] = np.arange(0,n) - dur_df['co2_capture_dur'] = n - dur_list += [dur_df.copy()] - expander = pd.concat(dur_list, ignore_index=True, sort=False) - co2_capture_value = co2_capture_value.merge(expander, on='co2_capture_dur', how='left') - co2_capture_value['t'] = co2_capture_value['t'] + co2_capture_value['year_adder'] - else: - co2_capture_value = df_ivt[['i', 'v', 't', 'co2_capture_value_monetized', 'co2_capture_dur']].iloc[0:5,:] - co2_capture_value = co2_capture_value.drop_duplicates(['i', 'v', 't']) - co2_capture_value['v'] = ['new%s' % v for v in co2_capture_value['v']] - co2_capture_value['t'] = co2_capture_value['t'].astype(int) - - # Expand h2_ptc_value by the duration of the incentive. - h2_ptc_value = df_ivt[['i', 'v', 't', 'h2_ptc_value_monetized', 'h2_ptc_dur']].copy() - h2_ptc_value = h2_ptc_value[h2_ptc_value['h2_ptc_value_monetized']>0] - if len(h2_ptc_value) > 0: - dur_list = [] # create year expander - for n in list(h2_ptc_value['h2_ptc_dur'].drop_duplicates()): - dur_df = pd.DataFrame() - dur_df['year_adder'] = np.arange(0,n) - dur_df['h2_ptc_dur'] = n - dur_list += [dur_df.copy()] - expander = pd.concat(dur_list, ignore_index=True, sort=False) - h2_ptc_value = h2_ptc_value.merge(expander, on='h2_ptc_dur', how='left') - h2_ptc_value['t'] = h2_ptc_value['t'] + h2_ptc_value['year_adder'] - else: - h2_ptc_value = df_ivt[['i', 'v', 't', 'h2_ptc_value_monetized', 'h2_ptc_dur']].iloc[0:5,:] - h2_ptc_value = h2_ptc_value.drop_duplicates(['i', 'v', 't']) - h2_ptc_value['v'] = ['new%s' % v for v in h2_ptc_value['v']] - h2_ptc_value['t'] = h2_ptc_value['t'].astype(int) - - # Expand the various ptc values by the duration of the incentive. - # We are tracking various ptc_values (e.g. with and without tax grossups) - # because different downstream processes require different forms of the ptc's value - ptc_values_df = df_ivt[['i', 'v', 't', 'ptc_value', 'ptc_dur', 'ptc_value_monetized', 'ptc_tax_equity_penalty', - 'ptc_value_monetized_posttax', 'ptc_grossup_value', 'ptc_value_scaled']].copy() - ptc_values_df = ptc_values_df[ptc_values_df['ptc_value']>0] - if len(ptc_values_df) > 0: - dur_list = [] # create year expander - for n in list(ptc_values_df['ptc_dur'].drop_duplicates()): - dur_df = pd.DataFrame() - dur_df['year_adder'] = np.arange(0,n) - dur_df['ptc_dur'] = n - dur_list += [dur_df.copy()] - expander = pd.concat(dur_list, ignore_index=True, sort=False) - ptc_values_df = ptc_values_df.merge(expander, on='ptc_dur', how='left') - ptc_values_df['t'] = ptc_values_df['t'] + ptc_values_df['year_adder'] - else: - ptc_values_df = df_ivt[['i', 'v', 't', 'ptc_value', 'ptc_dur', 'ptc_value_monetized', 'ptc_tax_equity_penalty', - 'ptc_value_monetized_posttax', 'ptc_grossup_value', 'ptc_value_scaled']].iloc[0:5,:] # this is just a hack because pjg didn't know how to have gams handle empty files - ptc_values_df = ptc_values_df.drop_duplicates(['i', 'v', 't']) - ptc_values_df['v'] = ['new%s' % v for v in ptc_values_df['v']] - ptc_values_df['t'] = ptc_values_df['t'].astype(int) - - - - #%% - # Import schedules for financial calculations - construction_schedules = pd.read_csv(os.path.join(inputs_case,'construction_schedules.csv')) - depreciation_schedules = pd.read_csv(os.path.join(inputs_case,'depreciation_schedules.csv')) - - ### Calculate financial multipliers - print('Calculating financial multipliers...') - df_ivt = reeds.financials.calc_financial_multipliers( - df_ivt, construction_schedules, depreciation_schedules, sw['timetype']) - - - #%%### Calculate financial multipliers for transmission - ### Load transmission data - dftrans = pd.read_csv(os.path.join(inputs_case,'financials_transmission.csv')) - ### Get transmission capital recovery period (CRP) from input scalars - dftrans['eval_period'] = int(scalars['trans_crp']) - ### Get online year - dftrans['t'] = dftrans.t_start_construction + dftrans.construction_time - ### Get ITC monetization - dftrans['itc_frac_monetized'] = dftrans.itc_frac * (1 - dftrans.itc_tax_equity_penalty) - ### Get sys financials - dftrans = dftrans.merge(financials_sys.dropna(how='any'), on='t', how='right') - ### Get financial multipliers - dftrans = reeds.financials.calc_financial_multipliers( - df_inv=dftrans, construction_schedules=construction_schedules, - depreciation_schedules=depreciation_schedules, timetype=sw['timetype'], - ) - - ### Get the CRF for transmission - dftrans['crf_tech'] = reeds.financials.calc_crf(dftrans['d_real'], dftrans['eval_period']) - - ### Get the final capital cost multiplier (including the CRF scaler above) - dftrans['cap_cost_mult'] = reeds.financials.calc_final_capital_cost_multiplier(dftrans) - dftrans['cap_cost_mult_noITC'] = reeds.financials.calc_final_capital_cost_multiplier(dftrans, mult_type='finMult_noITC') - - ### The transmission ITC is not meant to apply to currently-planned transmission. - ### So for years before firstyear_trans, use cap_cost_mult_noITC; - ### i.e. only start applying the ITC once the model switches to endogenous transmission. - firstyear_trans = int(scalars['firstyear_trans_longterm']) - dftrans.loc[dftrans.t lastdatayear: - dfout[endyear] = dfout.loc[:,lastdatayear] - # If data start after 2010, add a column for 2010 - if (2010 not in dfout.columns) and all(dfout.columns.values > 2010): - ## For UnappWaterSeaAnnDistr we give the new values directly rather than as a ratio, - ## so we backfill with the earliest available data - if infile == 'UnappWaterSeaAnnDistr': - dfout[2010] = dfout.iloc[:,0] - ## Otherwise we fill with 1 (i.e. no change). Note that since we interpolate to the - ## next value, the years between 2010 and the first year with data will not be 1. - else: - dfout[2010] = 1. - ## Move 2010 column to the front of the dataframe - dfout.sort_index(axis=1, inplace=True) - # Interpolate to missing years - dfinterp = ( - dfout - ## Switch column names from integer years to timestamps - .rename(columns={c: pd.Timestamp(str(c)) for c in dfout.columns}) - ## Add empty columns at year-starts between existing data (mean doesn't do anything) - .resample('YS', axis=1).mean() - ## Interpolate linearly to fill the new columns - .T.interpolate('linear').T - ) - dfout = ( - ## Switch back to integer year column names - dfinterp.rename(columns={c: c.year for c in dfinterp.columns}) - ## Drop extra data after the model end year - .loc[:,:endyear] - ) - - # Files indexed by month undergo additional processing in hourly_writetimeseries. Create - # intermediate filenames for these files and melt them back to long format - file_prefix = 'temp' if 'month' in dfout.index.names else 'climate' - keepindex = False if file_prefix == 'temp' else True - if file_prefix == 'temp': - dfout = pd.melt( - dfout.reset_index(), id_vars=index[infile], value_vars=dfout.columns, - var_name='t', value_name='Value' - ) - - # Write it to output folder - dfout.round(decimals).to_csv(os.path.join(inputs_case, f'{file_prefix}_{infile}.csv'), - index=keepindex) - - return dfout - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if GSw_ClimateWater: - print('Writing annual and seasonal water climate multipliers') - for infile in ['UnappWaterMult','UnappWaterMultAnn','UnappWaterSeaAnnDistr']: - readwrite(infile=infile) - -if GSw_ClimateHydro: - print('Writing annual and seasonal hydro climate multipliers') - for infile in ['hydadjann','hydadjsea']: - readwrite(infile=infile) - -if not any([GSw_ClimateWater,GSw_ClimateHydro]): - print("All climate switches are off.") - -reeds.log.toc(tic=tic, year=0, process='input_processing/climateprep.py', - path=os.path.join(inputs_case,'..')) - -print('Finished climateprep.py') diff --git a/input_processing/copy_files.py b/input_processing/copy_files.py deleted file mode 100644 index 90e22a09..00000000 --- a/input_processing/copy_files.py +++ /dev/null @@ -1,1682 +0,0 @@ -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== -import os -import sys -import datetime -import numpy as np -import pandas as pd -import argparse -import shutil -import yaml -import json -import h5py -from pathlib import Path -# Local Imports -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -#%% =========================================================================== -### --- General Read Functions--- -### =========================================================================== -def is_required_file(runfiles_row, sw): - """ - Determine whether or not the file corresponding to the provided row of - runfiles.csv is required using the row's "required_if" value. - - Note that the code snippets assume that a variable 'sw' has been - initialized and holds the result of reeds.io.get_switches(), which is why - this function takes 'sw' as an argument despite 'sw' not being used - explicitly. - """ - required_if_value = runfiles_row['required_if'] - is_required = eval(required_if_value) - if is_required not in [True, False, 1, 0]: - raise ValueError( - "The 'required_if' value must evaluate to a true/false statement." - f"Update the entry for {runfiles_row['filename']} in " - "runfiles.csv." - ) - return is_required - - -def read_runfiles(reeds_path, inputs_case, sw, agglevel_variables): - """ - Read runfiles.csv and return the runfiles dataframe - Identify files that have a region index versus those that do not. - """ - runfiles = ( - pd.read_csv( - os.path.join(reeds_path, 'runfiles.csv'), - dtype={'fix_cols':str, - 'depends_on_switch':str, - 'depends_on_switch_value': str}, - comment='#', - ).fillna({'fix_cols':'', - 'depends_on_switch':'', - 'depends_on_switch_value':''}) - ) - - runfiles['file_is_required'] = runfiles.apply( - axis=1, - func=is_required_file, - args=(sw,), - ) - - # If a filepath isn't specified, that means it is already in the - # inputs_case folder, otherwise use the filepath - # We leave the 'lvl' portion of 'full_filepath' unformatted because - # we may need to read multiple 'lvl' variants of the file at once later - runfiles['full_filepath'] = runfiles.apply( - axis=1, - func=lambda row: os.path.join(inputs_case, row['filename']) - if pd.isna(row['filepath']) - else os.path.join(reeds_path, row['filepath'].format(**{**sw, **{'lvl': '{lvl}'}})) - ) - - # Create a copy of runfiles that specifies the 'lvl' that applies to each file - # (only used to determine missing files, the original runfiles is used later). - # In general, the 'lvl' corresponds to GSw_RegionResolution. - # For mixed resolution, each 'lvl'-indexed file is split into two rows - - # one for the BA-level file and the other for the county-level file. - runfiles_with_lvls = runfiles.assign(lvl='') - lvl_indexed_file_mask = ( - (runfiles_with_lvls.filepath.notna()) - & (runfiles_with_lvls.filepath.str.contains('{lvl}')) - ) - if agglevel_variables['lvl'] == 'mult': - runfiles_with_lvls.loc[lvl_indexed_file_mask, 'lvl'] = 'ba,county' - runfiles_with_lvls['lvl'] = runfiles_with_lvls['lvl'].str.split(',') - runfiles_with_lvls = runfiles_with_lvls.explode('lvl') - else: - runfiles_with_lvls.loc[lvl_indexed_file_mask, 'lvl'] = agglevel_variables['lvl'] - - # Determine existence of each file - runfiles_with_lvls['full_filepath'] = runfiles_with_lvls.apply( - axis=1, - func=lambda x: x['full_filepath'].format(**{'lvl': x['lvl']}) - ) - runfiles_with_lvls['file_exists'] = ( - runfiles_with_lvls['full_filepath'].apply(lambda x: os.path.exists(x)) - ) - - # Raise an error if any of the required files with specified filepaths are missing - missing_required_files = list( - runfiles_with_lvls.loc[( - runfiles_with_lvls['file_is_required'] - & ~runfiles_with_lvls['file_exists'] - & ~runfiles_with_lvls['filepath'].isna() - )]['filepath'] - ) - if len(missing_required_files) > 0: - raise FileNotFoundError( - 'The following required files are missing. Add them ' - 'to the inputs directory or update runfiles.csv to specify optionality:\n{}\n' - .format('\n'.join(missing_required_files)) - ) - - # Add file existence information to runfiles (for lvl-indexed files, the file must exist - # at all resolutions required for the run). - # We have to add this to runfiles rather than using runfiles_with_lvls because later - # sections require the 'lvl' placeholder in the filename to be unformatted and the file - # to be represented by one row rather than the multiple split up rows in runfiles_with_lvls. - runfiles['file_exists'] = ( - runfiles['filename'].map(runfiles_with_lvls.groupby('filename')['file_exists'].min()) - ) - - # Non-region files that need copied either do not have an entry in region_col - # or have 'ignore' as the entry. They also have a filepath specified. - non_region_files = ( - runfiles[ - ( - (runfiles['region_col'].isna()) - | (runfiles['region_col'] == 'ignore') - ) - & (~runfiles['filepath'].isna())] - ) - - # Region files are those that have a region and do not specify 'ignore' - # Also ignore files that are created after this script runs (i.e., post_copy = 1) - region_files = ( - runfiles[ - (~runfiles['region_col'].isna()) - & (runfiles['region_col'] != 'ignore') - & (runfiles['post_copy'] != 1)] - ) - - return runfiles, non_region_files, region_files - - -def get_source_deflator_map(reeds_path): - """ - Get the deflator for each input file - """ - # Inflation-adjusted inputs - sources_dollaryear = pd.read_csv( - os.path.join(reeds_path,'sources.csv'), - usecols=["RelativeFilePath", "DollarYear"] - ) - deflator = pd.read_csv( - os.path.join(reeds_path,'inputs','financials','deflator.csv'), - header=0, names=['Dollar.Year','Deflator'], index_col='Dollar.Year').squeeze(1) - # Create a mapping between inputs' relative filepaths and their deflation - # multipliers based on the dollar years their monetary values are in - sources_dollaryear = ( - # Filter out rows that don't contain a valid dollar year - sources_dollaryear[pd.to_numeric(sources_dollaryear['DollarYear'], errors='coerce').notnull()] - # Note: We must remove the backslash that prepends each relative filepath - # for compatibility with the 'os' package (otherwise it is treated as an absolute path) - .assign(RelativeFilePath=sources_dollaryear["RelativeFilePath"].str[1:]) - .astype({"DollarYear": "int64"}) - .rename(columns={"DollarYear": "Dollar.Year"}) - .merge(deflator,on="Dollar.Year",how="left") - ) - - source_deflator_map = dict(zip(sources_dollaryear["RelativeFilePath"], sources_dollaryear["Deflator"])) - - return source_deflator_map - -def get_regions_and_agglevel( - reeds_path, - inputs_case, - save_regions_and_agglevel=True, -): - """ - Create a regional mapping to help filter for specific regions and aggregation levels. - This function reads various input files, processes them to create mappings of regions - at different levels of aggregation, and writes these mappings to csv files. - - If save_regions_and_agglevel is False do not save intermediate files - (You just want the mapping) - """ - sw = reeds.io.get_switches(inputs_case) - - ## TEMPORARY 20260402: Load the full regions list - ## Use the line below once we make the switch - # hierarchy = reeds.io.assemble_hierarchy(inputs_case) - hierarchy = pd.read_csv( - Path(reeds.io.reeds_path, 'inputs', 'zones', sw.GSw_ZoneSet, 'hierarchy_from134.csv') - ) - hierarchy['offshore'] = 0 - # Append offshore zones if using - if int(sw.GSw_OffshoreZones): - hierarchy_offshore = reeds.io.assemble_hierarchy( - fpath=os.path.join(reeds_path, 'inputs', 'zones', 'hierarchy_offshore.csv'), - extra=False, - ).assign(offshore=1) - hierarchy = pd.concat([hierarchy, hierarchy_offshore], ignore_index=True) - - # Save the original hierarchy file: used in recf.py and hourly_*.py scripts - if save_regions_and_agglevel: - hierarchy.to_csv( - os.path.join(inputs_case,'hierarchy_original.csv'), - index=False, header=True - ) - - # Add a row for each county - ## TEMPORARY 20260402: Use the old 134-zone county2zone until the aggregation approach is updated - county2zone = ( - reeds.io.get_county2zone(GSw_ZoneSet='z134', as_map=False) - .rename(columns={'r':'ba'}) - ) - county2zone['county'] = 'p' + county2zone.FIPS - county2zone.to_csv( - os.path.join(inputs_case, 'county2zone_original.csv'), - index=False - ) - - # Add county info to hierarchy - hierarchy = hierarchy.merge(county2zone.drop(columns=['FIPS','state']), on='ba', how='outer') - - # Subset hierarchy for the region of interest (based on the GSw_Region switch) - # Parse the GSw_Region switch. If it includes a '/' character, it has the format - # {column of hierarchy.csv}/{period-delimited entries to keep from that column}. - hier_sub = pd.DataFrame() - # allow the list defined by the user to include multiple spatial resolutions - region_groups = sw['GSw_Region'].split('//') if '//' in sw['GSw_Region'] else [sw['GSw_Region']] - # separate lists associated with each spatial resolution - for region_group in region_groups: - GSw_RegionLevel, GSw_Region = region_group.split('/') - GSw_Region = GSw_Region.split('.') - - hier_sub_partial = pd.concat([ - hierarchy[hierarchy[GSw_RegionLevel] == region] for region in GSw_Region - ]) - - hier_sub = pd.concat([hier_sub, hier_sub_partial]) - - # Read region resolution switch to determine agglevel - agglevel = sw['GSw_RegionResolution'].lower() - - # Check if desired spatial resolution is mixed - if agglevel == 'mixed': - #Set value in resolution column of hier_sub to match value assigned in modeled_regions.csv - region_def = pd.read_csv( - os.path.join(reeds_path,'inputs','userinput','modeled_regions.csv') - )[['r', sw.GSw_ZoneSet]] - - res_map = region_def.set_index('r').squeeze(1).to_dict() - hier_sub['resolution'] = hier_sub['ba'].map(res_map) - else: - hier_sub['resolution'] = agglevel - - - # Write out all unique aggregation levels present in the hierarchy resolution column - agglevels = hier_sub['resolution'].unique() - - # Write agglevel - if save_regions_and_agglevel: - pd.DataFrame(agglevels, columns=['agglevels']).to_csv( - os.path.join(inputs_case, 'agglevels.csv'), index=False) - - - # Create an r column at the front of the dataframe and populate it with the - # county-level regions (overwritten if needed) - hier_sub.insert(0,'r',hier_sub['county']) - - # Overwrite the regions with the ba, state, or aggreg values as specififed - for level in ['ba','aggreg']: - hier_sub.loc[hier_sub['resolution'] == level, 'r'] = ( - hier_sub.loc[hier_sub['resolution'] == level, level]) - - # Write out mappings of r and ba to all counties - r_county = hier_sub[['r','county']].dropna(subset='county') - ba_county = hier_sub[['ba','county']] - - # Rewrite county2zone for this case - county2zone_agg = county2zone.merge(r_county, on='county') - county2zone_agg.to_csv( - os.path.join(inputs_case, 'county2zone.csv'), - index=False - ) - - if save_regions_and_agglevel: - r_county.to_csv( - os.path.join(inputs_case, 'r_county.csv'), index=False) - - # Write out a mapping of r to ba regions - hier_sub[['r','ba']].drop_duplicates().to_csv( - os.path.join(inputs_case, 'r_ba.csv'), index=False) - # Write out mapping of r to census divisions - hier_sub[['r','cendiv']].drop_duplicates().to_csv( - os.path.join(inputs_case, 'r_cendiv.csv'), index=False) - - # Write out mapping of rb to aggreg (for writesupplycurves.py) - hier_sub[['ba','aggreg']].drop_duplicates().to_csv( - os.path.join(inputs_case, 'rb_aggreg.csv'), index=False) - - # Write out val_county and val_ba before collapsing to unique regions - hier_sub['county'].dropna().to_csv( - os.path.join(inputs_case, 'val_county.csv'), header=False, index=False) - hier_sub['ba'].drop_duplicates().to_csv( - os.path.join(inputs_case, 'val_ba.csv'), header=False, index=False) - - # Find all the unique elements that might define a region - val_r_all = [] - for column in hier_sub.columns.drop('offshore', errors='ignore'): - val_r_all.extend(hier_sub[column].dropna().unique().tolist()) - - # Converting to a set ensures that only unique values are kept - val_r_all = sorted(list(set(val_r_all))) - - if save_regions_and_agglevel: - pd.Series(val_r_all).to_csv( - os.path.join(inputs_case, 'val_r_all.csv'), header=False, index=False) - - # Rename columns and save as hierarchy_with_res.csv for use in agglevel_variables function - hier_sub.drop(columns='offshore', errors='ignore').rename(columns={'r':'*r'}).to_csv( - os.path.join(inputs_case, 'hierarchy_with_res.csv'), index=False) - - # Drop county name and resolution columns - hier_sub = hier_sub.drop(['county_name','resolution'],axis=1) - - - # Collapse to only unique regions - hier_sub = hier_sub.drop_duplicates(subset=['r']) - - # Sort hier_sub by r so that "ord(r)" commands in GAMS result in the properly - # ordered outputs - hier_sub['numeric_value'] = hier_sub['r'].str.extract('(\d+)').astype(float) - hier_sub = hier_sub.sort_values(by='numeric_value').drop('numeric_value', axis=1) - - # Output the itlgrp files for mixed and county resolution - - if sw.GSw_RegionResolution == 'aggreg': - hier_sub['itlgrp'] = hier_sub['aggreg'] - else: - hier_sub['itlgrp'] = hier_sub['ba'] - - if sw.GSw_RegionResolution == 'mixed': - mod_reg = pd.read_csv( - os.path.join(reeds_path,'inputs','userinput','modeled_regions.csv')) - if 'aggreg' in mod_reg[sw.GSw_ZoneSet].tolist(): - hier_sub['itlgrp'] = hier_sub['aggreg'] - hier_sub[['r','itlgrp']].rename(columns={'r':'*r'}).to_csv( - os.path.join(inputs_case, 'hierarchy_itlgrp.csv'), index=False) - - # save the itlgrp values - hier_sub[['itlgrp']].drop_duplicates().to_csv( - os.path.join(inputs_case, 'val_itlgrp.csv'), header=False, index=False) - - # Drop any substate region columns as these will no longer be needed - hier_sub = hier_sub.drop(['county', 'ba', 'itlgrp'], axis=1) - - # Populate val_st as unique states (not st_int) from the subsetted hierarchy table - # Also include "voluntary" state for modeling voluntary market REC trading - val_st = list(hier_sub['st'].unique()) + ['voluntary'] - - # Write out the unique values of each column in hier_sub to val_[column name].csv - # Note the conversion to a pd Series is necessary to leverage the to_csv function - if save_regions_and_agglevel: - for i in hier_sub.columns.drop('offshore', errors='ignore'): - pd.Series(hier_sub[i].unique()).to_csv( - os.path.join(inputs_case,'val_' + i + '.csv'),index=False,header=False) - - # Overwrite val_st with the val_st used here (which includes 'voluntary') - pd.Series(val_st).to_csv( - os.path.join(inputs_case, 'val_st.csv'), header=False, index=False) - - # Rename columns and save as hierarchy.csv - ( - hier_sub - .rename(columns={'r':'*r'}) - .drop(columns=['aggreg','offshore'], errors='ignore') - ).to_csv(os.path.join(inputs_case, 'hierarchy.csv'), index=False) - - # Write offshore zones - hier_sub.loc[hier_sub.offshore == 1, 'r'].to_csv( - os.path.join(inputs_case, 'offshore.csv'), index=False, header=False, - ) - - levels = [i for i in hier_sub if i != 'offshore'] - valid_regions = {level: list(hier_sub[level].unique()) for level in levels} - - val_r = sorted(valid_regions['r']) - - # Export region files - if save_regions_and_agglevel: - pd.Series(val_r).to_csv( - os.path.join(inputs_case, 'val_r.csv'), header=False, index=False) - - regions_and_agglevel = { - "valid_regions": valid_regions, - "val_r_all": val_r_all, - "val_st": val_st, - "r_county": r_county, - "ba_county": ba_county, - "levels": levels - } - - return regions_and_agglevel - - -def read_banned_tech_file(full_path, filepath, inputs_case, r_county): - """ - Parses the list of regionally (state/county-level) banned techs from the - provided YAML file and reformats it as a GAMS-compatible table. - Regions banning nuclear are exported as their own list, as nuclear bans - have their own switches and functionalities that are handled separately. - """ - if not (full_path.endswith('yaml') or full_path.endswith('yml')): - raise TypeError(f'filetype for {full_path} is not .yaml/.yml') - - with open(full_path) as f: - techs_banned = yaml.safe_load(f) - - hierarchy = pd.read_csv(os.path.join(inputs_case, 'hierarchy.csv')) - df = pd.DataFrame(data=0, columns=hierarchy['*r'], index=techs_banned.keys()) - - # Nuclear bans are handled specially in the model, - # so we create a separate dataframe for those regions. - nuclear_ban_regions = pd.DataFrame(data=[], columns=['*r']) - for tech, ban_lists in techs_banned.items(): - ban_regions = [] - - if not all([x in ['st', 'county'] for x in ban_lists.keys()]): - raise NotImplementedError( - "The regional scope of banned techs must be either 'st' or 'county'. " - f"Update the nested keys in {filepath}." - ) - - # Apply state-wide bans to all of the regions belonging to those states - if 'st' in ban_lists.keys(): - ban_states = ban_lists['st'] - state_ban_regions = list(hierarchy.loc[hierarchy.st.isin(ban_states)]['*r']) - ban_regions.extend(state_ban_regions) - - # Apply county-wide bans to regions where all of the counties have banned the tech - if 'county' in ban_lists.keys(): - ban_counties = ['p' + str(fips).zfill(5) for fips in ban_lists['county']] - r_ban_counties_map = ( - r_county.loc[r_county.county.isin(ban_counties)] - .groupby('r') - ['county'] - .apply(list) - .apply(sorted) - ) - r_all_counties_map = ( - r_county.groupby('r') - ['county'] - .apply(list) - .apply(sorted) - ) - county_ban_regions = list( - r_ban_counties_map - .loc[(r_ban_counties_map.isin(r_all_counties_map))] - .index - ) - ban_regions.extend(county_ban_regions) - - if tech == 'nuclear': - nuclear_ban_regions['*r'] = ban_regions - else: - df.loc[tech, ban_regions] = 1 - - df = df.reset_index(names=['i']) - - return df, nuclear_ban_regions - - -def read_special_h5file(full_path): - """ - Read .h5 file and make special-case adjustments if necessary: - - recf_distpv: drop 'distpv|' from column titles - - transmission_cost_ac: reset index and decode strings - - transmission_distance: stack from wide into long and decode strings - """ - filename = os.path.basename(full_path) - df = reeds.io.read_file(full_path, parse_timestamps=True) - - if filename.startswith('recf_distpv'): - df.columns = df.columns.str.replace('distpv|','') - elif filename.startswith('transmission_cost_ac'): - df = df.reset_index() - for col in ['r', 'rr', 'tscbin']: - df[col] = df[col].str.decode('utf-8') - elif filename.startswith('transmission_distance'): - df = df.stack().rename('miles').reset_index() - df['r'] = df['r'].str.decode('utf-8') - - return df - - -def subset_to_valid_regions( - sw, - region_file_entry, - agglevel_variables, - regions_and_agglevel, - inputs_case=None, - agg=True, -): - """ - Filter data for valid regions and return a dataframe - """ - levels = regions_and_agglevel["levels"] - val_r_all = regions_and_agglevel["val_r_all"] - val_st = regions_and_agglevel["val_st"] - valid_regions = regions_and_agglevel["valid_regions"] - - # Read file and return dataframe filtered for valid regions - filepath = region_file_entry['filepath'] - filename = region_file_entry['filename'] - full_path = region_file_entry['full_filepath'] - - # Get the filetype of the input file from the filepath string - filetype_in = os.path.splitext(filepath)[1].strip('.') - - region_col = region_file_entry['region_col'] - fix_cols = [i for i in region_file_entry['fix_cols'].split(',') if i != ''] - - sc_point_gid_index = False - if ( - filename.startswith('supplycurve') - or filename.startswith('exog_cap') - or filename.startswith('prescribed_builds') - ): - sc_point_gid_index = True - - # When running at mixed resolution we need to copy both ba and county resolution data - if (agglevel_variables['lvl'] == 'mult') and ('lvl' in filepath) and (not sc_point_gid_index): - full_path_ba = full_path.replace('{lvl}', 'ba') - full_path_county = full_path.replace('{lvl}', 'county') - match filetype_in: - case 'h5': - df_ba = read_special_h5file(full_path_ba) - df_county = read_special_h5file(full_path_county) - case 'csv': - df_ba = pd.read_csv( - full_path_ba, - dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, comment='#', - ) - df_county = pd.read_csv( - full_path_county, - dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, comment='#', - ) - case _: - raise TypeError(f'filetype for {full_path} is not .csv or .h5') - - # Single resolution procedure - else: - # Replace '{switchnames}' in full_path with corresponding switch values - full_path = full_path.format(**{**sw, **{'lvl':agglevel_variables['lvl']}}) - ## Filename conditions - if filename.startswith('supplycurve'): - df = reeds.io.assemble_supplycurve( - full_path, - case=os.path.dirname(os.path.normpath(inputs_case)), - agg=agg, - ).reset_index() - elif filename.startswith('exog_cap'): - df = reeds.io.assemble_exog_cap( - full_path, - case=os.path.dirname(os.path.normpath(inputs_case)), - ) - elif filename.startswith('prescribed_builds'): - df = reeds.io.assemble_prescribed_builds( - full_path, - case=os.path.dirname(os.path.normpath(inputs_case)), - ) - elif filename == 'techs_banned.csv': - df, nuclear_ban_regions = read_banned_tech_file( - full_path, - filepath, - inputs_case, - r_county=regions_and_agglevel['r_county'] - ) - nuclear_ban_regions.to_csv( - os.path.join(inputs_case,'nuclear_ba_ban_list.csv'), - index=False - ) - ## Filetype conditions - elif filetype_in == 'h5': - df = read_special_h5file(full_path) - elif filetype_in == 'csv': - df = pd.read_csv(full_path, dtype={'FIPS':str, 'fips':str, 'cnty_fips':str}, comment='#') - else: - raise ValueError(f'Unmatched filename ({filename}) or filetype ({filetype_in})') - - # ---- Filter data to valid regions ---- - # If running at mixed resolution we need to remove BA level data for regions that are being solved at county resolution - if (agglevel_variables['lvl'] == 'mult') and ('lvl' in filepath) and (not sc_point_gid_index): - hier = pd.read_csv(os.path.join(inputs_case,'hierarchy_with_res.csv')).rename(columns = {'*r':'r'}) - # Filter function parameters to only include BA resolution regions - valid_regions_ba = {level: list(hier[hier['r'] - .isin(agglevel_variables['ba_regions'])][level].unique()) for level in levels} - val_st_ba = valid_regions_ba['st'] - val_r_all_ba = [] - for value in valid_regions_ba.values(): - val_r_all_ba.extend(value) - val_r_all_ba = list(set(val_r_all_ba)) - # Add BA regions associated with states and aggregs being run at BA resolution - val_r_all_ba.extend(x for x in agglevel_variables['ba_regions']if x not in val_r_all_ba) - df_ba = filter_data( - df_ba, - region_col, - fix_cols,levels, - val_r_all=val_r_all_ba, - valid_regions=valid_regions_ba, - val_st=val_st_ba, - filename=filename - ) - - - # Transmission files need to be filtered differently to allow interfaces between BA and county resolution regions - transmission_files = [ - 'transmission_cost_ac.csv', - 'transmission_cost_dc.csv', - 'transmission_distance.csv', - ] - - if filename in transmission_files: - # Filter county data to include regions being solved at both BA and county resolution - df_county = filter_data( - df_county, - region_col, - fix_cols, - levels, - val_r_all, - valid_regions, - val_st, - filename=filename - ) - # Assign the counties that are not being solved at county resolution to the correct BA - tx_region_col = '*r' if '*r' in df_county.columns.values else 'r' - for idx, region in df_county[tx_region_col].items(): - if region not in agglevel_variables['county_regions']: - df_county.loc[idx, tx_region_col] = agglevel_variables['BA_2_county'][df_county.loc[idx,tx_region_col]] - - for idx,region in df_county['rr'].items(): - if region not in agglevel_variables['county_regions']: - df_county.loc[idx, 'rr'] = agglevel_variables['BA_2_county'][df_county.loc[idx, 'rr']] - # Drop if *r and rr are same region - df_county = df_county.drop(df_county[df_county[tx_region_col]==df_county['rr']].index) - # Drop if line is BA-to-BA - drop_list = [] - for idx,region in df_county.iterrows(): - if ( - (region[tx_region_col] in agglevel_variables['ba_regions']) - and (region['rr'] in agglevel_variables['ba_regions']) - ): - drop_list.append(idx) - - df_county = df_county.drop(drop_list) - # Group lines going between same BA and county - if 'distance' in filename: - df_county = df_county.groupby([tx_region_col,'rr'] + fix_cols).mean().reset_index() - elif 'cost_ac' in filename: - df_county = df_county.groupby([tx_region_col,'rr'] + fix_cols).sum().reset_index() - # Drop reverse interfaces - # Keep only the interface where the first region is alphabetically first - df_county['region_pair'] = df_county.apply( - lambda x: '||'.join(sorted([x[tx_region_col], x['rr']])), axis=1) - df_county = df_county.sort_values(by=['region_pair','tscbin']) - df_county = df_county.drop_duplicates(subset=['region_pair','tscbin'], keep='first') - df_county = df_county.drop(columns=['region_pair']) - else: - df_county = df_county.groupby([tx_region_col,'rr'] + fix_cols).sum().reset_index() - - df_county['interface'] = df_county[tx_region_col] + '||'+df_county['rr'] - df_county.reset_index(drop=True,inplace=True) - - else: - # Filter function parameters to only include county resolution regions - valid_regions_county = {level: (hier[hier['r'] - .isin(agglevel_variables['county_regions'])][level].unique()) for level in levels} - val_st_county = valid_regions_county['st'] - val_r_all_county = [] - for value in valid_regions_county.values(): - val_r_all_county.extend(value) - val_r_all_county = list(dict.fromkeys(val_r_all_county)) - - df_county = filter_data( - df_county, - region_col, - fix_cols, - levels, - val_r_all=val_r_all_county, - valid_regions=valid_regions_county, - val_st=val_st_county, - filename=filename - ) - - # Combine BA and county data - - # The filter data function returns a dataframe with NAN values to prevent empty H5 files - # If either the BA data or county data are populated we can drop the nan data - if df_county.isna().all().all() and not df_ba.isna().all().all(): - df = df_ba - elif not df_county.isna().all().all() and df_ba.isna().all().all(): - df = df_county - else: - # Combine BA and county data - if region_file_entry['wide'] == 1 : - df = pd.concat([df_ba,df_county],axis =1) - else: - df = pd.concat([df_ba,df_county]) - - # Single resolution procedure - # Or procedure for input data that exist at single resolution and - # are aggregated/disaggregated later - else: - df = filter_data( - df, - region_col, - fix_cols, - levels, - val_r_all, - valid_regions, - val_st, - filename=filename - ) - - return df - - -#%% =========================================================================== -### --- General Write Functions--- -### =========================================================================== -def write_empty_file(filepath): - filetype = os.path.splitext(filepath)[1].strip('.') - if filetype == 'h5': - with h5py.File(filepath, 'w'): - pass - else: - open(filepath, 'a').close() - - return - - -def scalar_csv_to_txt(path_to_scalar_csv): - """ - Write a scalar csv to GAMS-readable text - Format of csv should be: scalar,value,comment - """ - # Load the csv - dfscalar = pd.read_csv( - path_to_scalar_csv, - header=None, names=['scalar','value','comment'], index_col='scalar').fillna(' ') - # Create the GAMS-readable string (comments can only be 255 characters long) - scalartext = '\n'.join([ - 'scalar {:<30} "{:<5.255}" /{}/ ;'.format( - i, row['comment'], row['value']) - for i, row in dfscalar.iterrows() - ]) - # Write it to a file, replacing .csv with .txt in the filename - with open(path_to_scalar_csv.replace('.csv','.txt'), 'w') as w: - w.write(scalartext) - - return dfscalar - - -def param_csv_to_txt(path_to_param_csv, writelist=True): - """ - Write a parameter csv to GAMS-readable text - Format of csv should be: parameter(indices),units,comment - """ - # Load the csv - dfparams = pd.read_csv( - path_to_param_csv, - index_col='param', comment='#', - ) - # Create the GAMS-readable param definition string (comments must be ≤255 characters) - paramtext = '\n'.join([ - f'parameter {i:<50} "--{row.units}-- {row.comment:.255}" ;' - # Don't define parameters with an input flag because they already exist - for i, row in dfparams.loc[dfparams.input != 1].iterrows() - ]) - # Write it to a file, replacing .csv with .gms in the filename - param_gms_path = path_to_param_csv.replace('.csv','.gms') - with open(param_gms_path, 'w') as w: - w.write(paramtext) - # Write the list of parameters if desired - if writelist: - # Create the GAMS-readable list of parameters (without indices) - paramlist = '\n'.join(dfparams.index.map(lambda x: x.split('(')[0]).tolist()) - param_list_path = ( - path_to_param_csv.replace('params','paramlist').replace('.csv','.txt')) - with open(param_list_path, 'w') as w: - w.write(paramlist) - - return dfparams - -# Function to filter data to valid regions -def filter_data( - df, - region_col, - fix_cols, - levels, - val_r_all, - valid_regions, - val_st, - filename -): - if region_col == 'wide': - # Check to see if the regions are listed in the columns. If they are, - # then use those columns - - # Need check for case where there are no data for val_r_all (but not because of the column headr formatting) and empty dataframe needs to be returned - if (len([x for x in val_r_all if x in df.columns])==0) & ( not any('|' in col for col in df.columns)) & ( not any('_' in col for col in df.columns)): - df = df.loc[:,df.columns.isin(fix_cols + val_r_all)] - elif df.columns.isin(val_r_all).any(): - df = df.loc[:,df.columns.isin(fix_cols + val_r_all)] - else: - # Checks if regions are in columns as '[class]|[region]' or '[class]_[region]' (e.g. in 8760 RECF data). - # This 'try' attempts to split each column name using '|' and '_' as delimiters and checks the second - # value for any regions. - # If it can't do so, it will instead use a blank dataframe. - try: - if any('|' in col for col in df.columns): - delim = '|' - elif '_' in df.columns[0]: - delim = '_' - else: - raise ValueError(f"Cannot split columns in {filename} by '|' or '_' (example: {df.columns[0]}).") - column_mask = (df.columns.str.split(delim,expand=True) - .get_level_values(1) - .isin(val_r_all) - .tolist() - ) - df = df.loc[:, column_mask | df.columns.isin(fix_cols)] - # Empty h5 files cannot be read in, causing errors in recf.py. - # In the case that val_r_all filters out all columns, leaving an empty dataframe, - # fill a single column with NaN to preserve the file index for use in recf.py - if len(df.columns) == 0: - df = pd.DataFrame(np.nan, index = df.index,columns = val_r_all) - except Exception: - df = pd.DataFrame() - - # If there is a region-to-region mapping set - elif region_col.strip('*') in ['r,rr','transgrp,transgrpp']: - # make sure both the r and rr regions are in val_r - r,rr = region_col.split(',') - df = df.loc[df[r].isin(val_r_all) & df[rr].isin(val_r_all)] - - # Subset on the valid regions except for r regions - # (r regions might also include s regions, which complicates things...) - elif ((region_col.strip('*') in levels) & (region_col.strip('*') != 'r')): - df = df.loc[df[region_col].isin(valid_regions[region_col.strip('*')])] - - # Subset both column of 'st' and columns of state if st_st - elif (region_col.strip('*') == 'st_st'): - # make sure both the state regions are in val_st - df = df.loc[df['st'].isin(val_st)] - df = df.loc[:,df.columns.isin(fix_cols + val_st)] - - elif (region_col.strip('*') == 'r_cendiv'): - # Make sure both the r is in val_r_all and cendiv is in val_cendiv - val_cendiv = valid_regions['cendiv'] - df = df.loc[df['r'].isin(val_r_all)] - df = df.loc[:,df.columns.isin(["r"] + val_cendiv)] - - # Subset on val_{level} if region_col == 'wide_{level}' - elif (region_col.split('_')[0] == 'wide') and (region_col.split('_')[1] in valid_regions.keys()): - # Check to see if the region values are listed in the columns. If they are, - # then use those columns - val_reg = valid_regions[region_col.split('_')[1]] - if df.columns.isin(val_reg).any(): - df = df.loc[:,df.columns.isin(fix_cols + val_reg)] - # Otherwise just use an empty dataframe - else: - df = pd.DataFrame() - - # If region_col is not wide, st, or aliased.. - else: - df = df.loc[df[region_col].isin(val_r_all)] - - return df - - -def write_scalars(scalars, inputs_case): - """ - Write modified scalars.csv file - Special-case handling of scalars.csv: turn years_until into firstyear - """ - toadd = scalars.loc[scalars.index.str.startswith('years_until')].copy() - toadd.index = toadd.index.map(lambda x: x.replace('years_until','firstyear')) - toadd.value += scalars.loc['this_year','value'] - scalars_write = pd.concat([scalars, toadd], axis=0) - - # Trim trailing decimal zeros - scalars_write.value = scalars_write.value.astype(str).replace('\.0+$', '', regex=True) - scalars_write.to_csv(os.path.join(inputs_case, 'scalars.csv'), header=False) - - # Rewrite the scalar tables as GAMS-readable definition - scalar_csv_to_txt(os.path.join(inputs_case,'scalars.csv')) - - -def write_GAMS_sets(runfiles, reeds_path, inputs_case): - """ - Write GAMS-readable sets to the inputs_case directory - """ - casedir = os.path.dirname(inputs_case) - - # Create Sets folder - shutil.copytree( - os.path.join(reeds_path,'inputs','sets'), - os.path.join(inputs_case,'sets'), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns('README*','readme*'), - ) - - # Write commands to load sets - sets = runfiles.loc[runfiles.GAMStype=='set'].copy() - settext = '$offlisting\n' + '\n\n'.join([ - f"set {row.GAMSname} '{row.comment:.255}'" - '\n/' - f"\n$include inputs_case%ds%{row.filename}" - '\n/ ;' - for i, row in sets.iterrows() - ]) + '\n$onlisting\n' - # Write to file - with open(os.path.join(casedir,'b_sets.gms'), 'w') as f: - f.write(settext) - - -def write_non_region_file(filename, filepath, src_file, dir_dst, sw, regions_and_agglevel, source_deflator_map): - """ - Copy a non-region specific file (filename) from src_file to dir_dst - """ - # Check if source file exists and is not rev_paths.csv - if (os.path.exists(src_file)) and (filename!='rev_paths.csv'): - - # Special Case: Values in load_multiplier.csv need to be rounded prior to copy - if filename == 'load_multiplier.csv': - df_load_multiplier = pd.read_csv(src_file).round(6) - df_load_multiplier.to_csv(os.path.join(dir_dst,'load_multiplier.csv'),index=False) - - elif filename == 'h2_exogenous_demand.csv': - # h2_exogenous_demand.csv has a path in runfiles.csv (considered a non-region file) - df=pd.read_csv(src_file,index_col=['p','t']) - df[sw['GSw_H2_Demand_Case']].round(3).rename_axis(['*p','t']).to_csv( - os.path.join(dir_dst,'h2_exogenous_demand.csv') - ) - - elif filename in ['energy_communities.csv', 'nuclear_energy_communities.csv']: - # Map energy communities to regions and compute the percentage of energy communities - # within each region to assign a weighted bonus. - - # Rename column in energy_communities.csv and map county to r, save as energy_communities.csv - energy_communities = pd.read_csv(src_file) - energy_communities.rename(columns={'County Region': 'county'}, inplace=True) - - r_county = regions_and_agglevel ['r_county'] - # Map energy communities to regions - e_df = pd.merge(energy_communities, r_county, on='county', how='left').dropna() - - # Group energy community regions and count the number of counties in each - energy_county_counts = e_df.groupby('r')['county'].nunique() - - # Group all regions and count the number of counties in each - total_county_counts = r_county.groupby('r')['county'].nunique() - - # Calculate the percentage of counties that are energy communities in each region - e_df = (energy_county_counts / total_county_counts).round(3).reset_index().dropna() - - # Rename columns from ['r','county'] to ['r','percentage_energy_communities'] - e_df.columns = ['r', 'percentage_energy_communities'] - - e_df.to_csv(os.path.join(dir_dst,filename),index=False) - - elif filename == 'co2_site_char.csv': - # Adjust for inflation - df = pd.read_csv(src_file) - df[f"bec_{sw['GSw_CO2_BEC']}"] *= source_deflator_map[filepath] - df.to_csv(os.path.join(dir_dst, 'co2_site_char.csv'), index=False) - - else: - shutil.copy(src_file, os.path.join(dir_dst,filename)) - - if filename == 'e_report_params.csv': - # Rewrite e_report_params as GAMS-readable definition - param_csv_to_txt(os.path.join(dir_dst,'e_report_params.csv')) - - if filename == 'scalars.csv': - # Rewrite scalars.csv as GAMS-readable definition - scalars = reeds.io.get_scalars(full=True) - write_scalars(scalars, dir_dst) - - -def write_non_region_files(non_region_files, sw, inputs_case, regions_and_agglevel, source_deflator_map): - """ - Copy non-region specific files to the input case directory. - """ - print('Copying non-region-indexed files') - - for _, row in non_region_files.iterrows(): - if row['filepath'].split('/')[0] in ['inputs','postprocessing','tests']: - dir_dst = inputs_case - else: - dir_dst = os.path.dirname(inputs_case) - - # If the file is missing and not required, - # an empty file is written with the given filename. - if (not row['file_exists']) and (not row['file_is_required']): - print(f'...writing empty file {row.filename}') - write_empty_file(os.path.join(dir_dst,row['filename'])) - else: - print(f'...copying {row.filename}') - filename = row['filename'] - filepath = row['filepath'] - src_file = row['full_filepath'] - write_non_region_file(filename, filepath, src_file, dir_dst, sw, regions_and_agglevel, source_deflator_map) - - -def calculate_county_fractions(df, county2zone): - """ - Calculates the values associated with each county as a percentage - of the total values for the county's state, BA, and model region - (where "BA" means a zone from the set of default 134 zones and - "model region" means a zone from the set of zones specific to this run). - Note the calculation of the county-to-BA fractions will eventually - be deprecated once the 134-zone structure is removed from all spatial - inputs (see https://github.nrel.gov/ReEDS/ReEDS-2.0/issues/1889). - The provided dataframe must have columns 'FIPS' and 'value'. - """ - required_columns = ['FIPS', 'value'] - missing_columns = [col for col in required_columns if col not in df.columns] - if len(missing_columns) > 0: - raise KeyError(f"Provided dataframe is missing required columns {missing_columns}") - - df = ( - df.merge( - county2zone.drop(columns='FIPS') - .rename(columns={'county': 'FIPS'}) - ) - .rename(columns={'ba': 'PCA_REG'}) - ) - df['fracdata'] = ( - df.groupby('PCA_REG') - ['value'] - .transform(lambda x: x / x.sum()) - ) - for col in ['r', 'state']: - df[f'{col}_frac'] = ( - df.groupby(col) - ['value'] - .transform(lambda x: x / x.sum()) - ) - - df = ( - df.dropna(subset='r') - [['PCA_REG', 'FIPS', 'fracdata', 'r', 'state', 'r_frac', 'state_frac']] - ) - - return df - -def write_disagg_data_files(runfiles, inputs_case): - """ - Write files that will be used for disaggregation. - """ - # Get the county2zone file specific to this run (a mapping from counties - # to model regions) and the original county2zone file (including all - # counties) and combine them. The former is needed to calculate model - # region-to-county fractions, and the latter is needed to calculate - # state-to-county and BA-to-county fractions. - county_r_map = reeds.io.get_county2zone(os.path.dirname(inputs_case)) - ## TEMPORARY 20260402: Use the old 134-zone county2zone until the aggregation approach is updated - county2zone = ( - reeds.io.get_county2zone(GSw_ZoneSet='z134', as_map=False) - .rename(columns={'r':'ba'}) - ) - county2zone['county'] = 'p' + county2zone['FIPS'].astype(str).str.zfill(5) - county2zone['r'] = county2zone['FIPS'].map(county_r_map) - - filename_filepath_map = runfiles.set_index('filename')['full_filepath'] - for filename in [ - 'disagg_geosize.csv', - 'disagg_population.csv', - 'disagg_state_lpf.csv' - ]: - if filename == 'disagg_geosize.csv': - # Calculate county land areas from the county shapefile - dfcounty = reeds.io.get_countymap(exclude_water_areas=True) - df = ( - dfcounty.set_index('rb') - .rename_axis('FIPS') - .area - .rename('value') - .reset_index() - ) - else: - # Read the raw data file for the disagg variable (e.g., - # county population data for disagg_population.csv) - filepath = filename_filepath_map[filename] - df = pd.read_csv(filepath) - - # Calculate state/region/BA-to-county fractions for the - # disagg variable and write to inputs_case - df = calculate_county_fractions(df, county2zone) - df.to_csv(os.path.join(inputs_case, filename), index=False) - - return - -def map_and_aggregate( - df, - regions_and_agglevel, - region_file_entry, - region_col, - aggfunc=None -): - ''' - Maps counties to BAs and aggregates according to aggfunc if provided. - ''' - merged = ( - df.set_index(region_col) - .merge(regions_and_agglevel['ba_county'], left_index=True, right_on='county') - .drop('county', axis=1) - .rename(columns={'ba': region_col}) - ) - - if aggfunc: - fix_cols = region_file_entry['fix_cols'].split(',') - if all([fix_col in merged.columns for fix_col in fix_cols]): - groupby_cols = list(set(fix_cols + [region_col])) - df = merged.groupby(groupby_cols, as_index=False).agg(aggfunc) - else: - df = merged.groupby(region_col, as_index=False).agg(aggfunc) - - - return df - -def upscale_from_county_to_ba( - df, - region_file_entry, - agglevel_variables, - regions_and_agglevel, - aggfunc=None -): - """ - Changes the resolution of the provided region_col from county to BA - or mixed resolution and aggregates according to the provided aggfunc. - """ - original_cols = df.columns - region_col = region_file_entry['region_col'] - - # Exception for cendiv - if region_col.strip('*') == 'r_cendiv': - val_cendiv = regions_and_agglevel['valid_regions']['cendiv'] - df = df.loc[df['r'].isin(regions_and_agglevel['r_county']['county'])] - df = df.loc[:, df.columns.isin(["r"] + val_cendiv)].reset_index(drop=True) - region_col = 'r' - - # Aggregate values to ba resolution if not running county-level resolution - # and if county level is not a desired resolution in mixed resolution runs - if 'county' not in agglevel_variables['agglevel']: - df = map_and_aggregate(df,regions_and_agglevel,region_file_entry,region_col,aggfunc) - - - # Mixed resolution procedure - elif agglevel_variables['lvl'] == 'mult': - df_ba = df[df[region_col].isin(agglevel_variables['BA_county_list'])] - df_ba = map_and_aggregate(df_ba,regions_and_agglevel,region_file_entry,region_col,aggfunc) - - # Filter out county regions - df_county = df[df[region_col].isin(agglevel_variables['county_regions'])] - # Combine county and BA - df = pd.concat([df_ba, df_county]) - - else: - pass - - df = df[original_cols] - - return df - - -def write_region_indexed_file( - df, - dir_dst, - source_deflator_map, - sw, - region_file_entry, - regions_and_agglevel, - agglevel_variables -): - """ - Write a single region-indexed file to the dir_dst directory - """ - filename = region_file_entry['filename'] - # Get the filetype of the output file from the filename string - filetype_out = os.path.splitext(filename)[1].strip('.') - - #---- Write data to dir_dst (inputs_case) folder ---- - if filetype_out == 'h5': - reeds.io.write_profile_to_h5(df, filename, dir_dst) - else: - # Special cases: These files' values need to be adjusted to copy - filepath = region_file_entry['filepath'] - match filename: - case 'bio_supplycurve.csv': - # Adjust for inflation - df['price'] = df['price'].astype(float) * source_deflator_map[filepath] - case ( - 'can_exports.csv' - | 'can_imports.csv' - | 'demonstration_plants.csv' - | 'distpvcap.csv' - | 'h2_ba_share.csv' - | 'regional_cap_cost_diff.csv' - | 'cendivweights.csv' - | 'cap_existing_psh.csv' - ): - # The upscale_from_county_to_ba function correctly handles the different spatial resolution options - # This sections just needs to check if the run is at pure county resolution - # The above listed data need to be upscaled if the run includes anything coarser than county resolution - if agglevel_variables['lvl'] != 'county': - df = upscale_from_county_to_ba( - df=df, - region_file_entry=region_file_entry, - agglevel_variables=agglevel_variables, - regions_and_agglevel=regions_and_agglevel, - aggfunc=region_file_entry.aggfunc - ) - case 'unitdata.csv': - fips_ba_map = regions_and_agglevel['ba_county'].dropna().set_index('county')['ba'] - df['reeds_ba'] = df['FIPS'].map(fips_ba_map) - ## If using offshore zones, map offshore wind units from land to offshore zones - if int(sw.GSw_OffshoreZones): - df = reeds.spatial.assign_to_offshore_zones(df) - num_units_missing_bas = len(df.loc[df.reeds_ba.isna()]) - if num_units_missing_bas > 0: - raise ValueError( - f"{num_units_missing_bas} units were not mapped to any BAs." - ) - case _: - pass - - df.to_csv(os.path.join(dir_dst,filename), index=False) - - -def write_region_indexed_files( - inputs_case, - sw, - region_files, - regions_and_agglevel, - agglevel_variables, - source_deflator_map -): - """ - Filter and copy data for files with regions - """ - print('Copying region-indexed files: filtering for valid regions') - for _, region_file_entry in region_files.iterrows(): - # If the file is missing and not required, - # an empty file is written with the given filename. - if ( - (not region_file_entry['file_exists']) - and (not region_file_entry['file_is_required']) - ): - print(f'...writing empty file {region_file_entry.filename}') - write_empty_file(os.path.join(inputs_case,region_file_entry['filename'])) - else: - print(f'...copying {region_file_entry.filename}') - # Read file and return dataframe filtered for valid regions - df = subset_to_valid_regions( - sw, - region_file_entry, - agglevel_variables, - regions_and_agglevel, - inputs_case - ) - write_region_indexed_file( - df, - inputs_case, - source_deflator_map, - sw, - region_file_entry, - regions_and_agglevel, - agglevel_variables - ) - - -def write_miscellaneous_files( - sw, - regions_and_agglevel, - agglevel_variables, - inputs_case, - reeds_path -): - """ - Handle miscellaneous files. - Many of these files are not in the non_region_files and region_files set - (runfiles.csv - from function read_runfiles). - """ - # ---- Miscellaneous files not in non_region_files or region_files ---- - pd.DataFrame( - {'*pvb_type': [f'pvb{i}' for i in sw['GSw_PVB_Types'].split('_')], - 'ilr': [np.around(float(c) / 100, 2) for c in sw['GSw_PVB_ILR'].split('_') - ][0:len(sw['GSw_PVB_Types'].split('_'))]} - ).to_csv(os.path.join(inputs_case, 'pvb_ilr.csv'), index=False) - - pd.DataFrame( - {'*pvb_type': [f'pvb{i}' for i in sw['GSw_PVB_Types'].split('_')], - 'bir': [np.around(float(c) / 100, 2) for c in sw['GSw_PVB_BIR'].split('_') - ][0:len(sw['GSw_PVB_Types'].split('_'))]} - ).to_csv(os.path.join(inputs_case, 'pvb_bir.csv'), index=False) - - # Constant value if input is float, otherwise named profile - # Methane leakage rate: - try: - rate = float(sw['GSw_MethaneLeakageScen']) - pd.Series(index=range(2010,2051), data=rate, name='constant').rename_axis('*t').round(5).to_csv( - os.path.join(inputs_case,'methane_leakage_rate.csv')) - except ValueError: - pd.read_csv( - os.path.join(reeds_path,'inputs','emission_constraints','methane_leakage_rate.csv'), - index_col='t', - )[sw['GSw_MethaneLeakageScen']].rename_axis('*t').round(5).to_csv( - os.path.join(inputs_case,'methane_leakage_rate.csv')) - - # H2 leakage rate: - pd.read_csv( - os.path.join(reeds_path,'inputs','emission_constraints','h2_leakage_rate.csv'), - index_col='i', - )[sw['GSw_H2LeakageScen']].rename_axis('*i').round(5).to_csv( - os.path.join(inputs_case,'h2_leakage_rate.csv')) - - # Add this_year to years_until_endogenous to generate the tech-specific firstyear.csv file - scalars = reeds.io.get_scalars(full=True) - ( - pd.read_csv( - # years_until_endogenous created using function write_non_region_files - os.path.join(inputs_case, 'years_until_endogenous.csv'), - index_col=0, - ).squeeze(1) - + int(scalars.loc['this_year','value']) - ).rename_axis('*i').rename('t').to_csv(os.path.join(inputs_case, 'firstyear.csv')) - - - ### Single column from input table ### - - pd.read_csv( - os.path.join(reeds_path,'inputs','emission_constraints','ng_crf_penalty.csv'), index_col='t', - )[sw['GSw_NG_CRF_penalty']].rename_axis('*t').to_csv( - os.path.join(inputs_case,'ng_crf_penalty.csv') - ) - - gwp = pd.read_csv( - os.path.join(reeds_path,'inputs','emission_constraints','gwp.csv'), - index_col='e', - ) - if sw['GSw_GWP'] in gwp: - gwp_write = gwp[sw['GSw_GWP']].copy() - else: - gwp_ch4, gwp_n2o = [float(i.split('_')[1]) for i in sw['GSw_GWP'].split('/')] - gwp_write = pd.Series({'CO2':1, 'CH4':gwp_ch4, 'N2O':gwp_n2o}) - - gwp_write['H2'] = scalars.loc['h2_gwp','value'].copy() - - gwp_write.to_csv(os.path.join(inputs_case,'gwp.csv'), header=False) - - # Calculate CO2 cap based on GSw_Region chosen (national or sub-national regions) - # Read in national co2 cap - co2_cap = ( - pd.read_csv( - os.path.join(reeds_path, 'inputs', 'emission_constraints', 'co2_cap.csv'), - index_col='t', - ) - .loc[sw['GSw_AnnualCapScen']] - .rename_axis('*t') - .rename('tonne_per_year') - ) - - # Read in 2022 CO2 emission share by county calculated from 2022 eGrid emission data: - em_share = pd.read_csv( - os.path.join( - reeds_path,'inputs','emission_constraints','county_co2_share_egrid_2022.csv'), - index_col=0) - - # Filter the counties that are in chosen GSw_Region - val_county = pd.read_csv(os.path.join(inputs_case,'val_county.csv'),names=['r']) - - # Merge emission share by county with the counties in GSw_Region and calculate emission share of GSw_Region - region_em_share = val_county.merge(em_share, on='r', how='left').fillna(0) - region_em_share = region_em_share['share'].sum() - - # Apply the emission share to national cap to get the emission cap trajectory of GSw_Region - co2_cap *= region_em_share - co2_cap.round(0).to_csv(os.path.join(inputs_case, 'co2_cap.csv')) - - # CO2 tax - pd.read_csv( - os.path.join(reeds_path,'inputs','emission_constraints','co2_tax.csv'), index_col='t', - )[sw['GSw_CarbTaxOption']].rename_axis('*t').round(2).to_csv( - os.path.join(inputs_case,'co2_tax.csv') - ) - - solveyears = reeds.inputs.parse_yearset(sw['yearset']) - if int(sw['startyear']) not in solveyears: - solveyears.append(int(sw['startyear'])) - solveyears = sorted(solveyears) - solveyears = [y for y in solveyears if (y >= int(sw['startyear'])) and (y <= int(sw['endyear']))] - pd.DataFrame(columns=solveyears).to_csv( - os.path.join(inputs_case,'modeledyears.csv'), index=False) - - pd.read_csv( - os.path.join(reeds_path,'inputs','national_generation','gen_mandate_trajectory.csv'), - index_col='GSw_GenMandateScen' - ).loc[sw['GSw_GenMandateScen']].rename_axis('*t').round(5).to_csv( - os.path.join(inputs_case,'gen_mandate_trajectory.csv') - ) - - pd.read_csv( - os.path.join(reeds_path,'inputs','national_generation','gen_mandate_tech_list.csv'), - index_col='*i', - )[sw['GSw_GenMandateList']].to_csv( - os.path.join(inputs_case,'gen_mandate_tech_list.csv') - ) - - pd.read_csv( - os.path.join(reeds_path,'inputs','climate','climate_heuristics_yearfrac.csv'), - index_col='*t', - )[sw['GSw_ClimateHeuristics']].round(3).to_csv( - os.path.join(inputs_case,'climate_heuristics_yearfrac.csv') - ) - - pd.read_csv( - os.path.join(reeds_path,'inputs','climate','climate_heuristics_finalyear.csv'), - index_col='*parameter', - )[sw['GSw_ClimateHeuristics']].round(3).to_csv( - os.path.join(inputs_case,'climate_heuristics_finalyear.csv') - ) - - pd.read_csv( - os.path.join(reeds_path,'inputs','upgrades','upgrade_costs_ccs_coal.csv'), - index_col='t', - )[sw['ccs_upgrade_cost_case']].round(3).rename_axis('*t').to_csv( - os.path.join(inputs_case,'upgrade_costs_ccs_coal.csv') - ) - - pd.read_csv( - os.path.join(reeds_path,'inputs','upgrades','upgrade_costs_ccs_gas.csv'), - index_col='t', - )[sw['ccs_upgrade_cost_case']].round(3).rename_axis('*t').to_csv( - os.path.join(inputs_case,'upgrade_costs_ccs_gas.csv') - ) - - ccseason_dates = pd.read_csv( - os.path.join(reeds_path, 'inputs', 'reserves', 'ccseason_dates.csv'), - index_col=['month', 'day'], - )[sw['GSw_PRM_CapCreditSeasons']].rename('ccseason') - ccseason_dates.to_csv(os.path.join(inputs_case, 'ccseason_dates.csv')) - ccseason_dates.drop_duplicates().to_csv( - os.path.join(inputs_case, 'ccseason.csv'), - index=False, header=False, - ) - - prm_profiles = pd.read_csv( - os.path.join(reeds_path,'inputs','reserves','prm_annual.csv'), - ).rename(columns={'*nercr':'nercr'}).set_index(['nercr','t']) - if sw['GSw_PRM_scenario'] in prm_profiles: - prm = prm_profiles[sw['GSw_PRM_scenario']] - else: - prm = pd.Series(index=prm_profiles.index, data=float(sw['GSw_PRM_scenario'])) - - ## Broadcast PRM from nercr to r and backfill missing years - hierarchy = reeds.io.get_hierarchy(reeds.io.standardize_case(inputs_case)) - prm_initial = ( - prm - .unstack('nercr') - .reindex(solveyears) - .bfill().ffill() - [hierarchy['nercr']] - ) - prm_initial.columns = hierarchy.index.rename('*r') - prm_initial = prm_initial.stack('*r').reorder_levels(['*r','t']).round(4).rename('fraction') - prm_initial.to_csv(os.path.join(inputs_case, 'prm_initial.csv')) - for t in solveyears: - stresspath = os.path.join(inputs_case, f'stress{t}i0') - os.makedirs(stresspath, exist_ok=True) - prm_initial.xs(t, 0, 't').to_csv(os.path.join(stresspath, 'prm.csv')) - - # Add capacity deployment limits based on interconnection queue data - cap_queue = pd.read_csv( - os.path.join(reeds_path,'inputs','capacity_exogenous','interconnection_queues.csv')) - # Filter the counties that are in chosen GSw_Region - cap_queue = cap_queue[cap_queue['r'].isin(val_county['r'])] - - # Single resolution procedure - if (agglevel_variables["lvl"] != 'county') and ('county' not in agglevel_variables['agglevel']): - cap_queue = cap_queue.rename(columns={'r':'county'}) - cap_queue = pd.merge(cap_queue, regions_and_agglevel["r_county"], on='county', how='left').dropna() - cap_queue = cap_queue.drop('county', axis=1) - - # Mixed resolution procedure - elif agglevel_variables['lvl'] == 'mult': - # Filter out BA regions and aggregate - cap_queue_ba = cap_queue[cap_queue['r'].isin(agglevel_variables['BA_county_list'])].copy() - if 'aggreg' in agglevel_variables['agglevel'] : - r_county_dict = regions_and_agglevel["r_county"].set_index('county')['r'].to_dict() - cap_queue_ba['r'] = cap_queue_ba['r'].map(r_county_dict) - - else: - cap_queue_ba['r'] = cap_queue_ba['r'].map(agglevel_variables['BA_2_county']) - - # Filter out county regions - cap_queue_county = cap_queue[cap_queue['r'].isin(agglevel_variables['county_regions'])] - - #combine BA and county - cap_queue = pd.concat([cap_queue_ba,cap_queue_county]) - - cap_queue = cap_queue.groupby(['tg','r'],as_index=False).sum() - cap_queue.to_csv(os.path.join(inputs_case,'cap_limit.csv'), index=False) - # ---- Miscelanous files in non_region_files or region_files (in this case we are overwriting them) - # Expand i (technologies) set if modeling water use. Overwrite originals. - if int(sw['GSw_WaterMain']): - pd.concat([ - pd.read_csv( - os.path.join(inputs_case,'i.csv'), - comment='*', header=None).squeeze(1), - pd.read_csv( - os.path.join(inputs_case,'i_coolingtech_watersource.csv'), - comment='*', header=None).squeeze(1), - pd.read_csv( - os.path.join(inputs_case,'i_coolingtech_watersource_upgrades.csv'), - comment='*', header=None).squeeze(1), - ]).to_csv(os.path.join(inputs_case,'i.csv'), header=False, index=False) - - ## Unit sizes for ReEDS2PRAS - fpath_out = os.path.join(inputs_case, 'unitsize.csv') - if sw['pras_unitsize_source'] == 'atb': - shutil.copy( - os.path.join(reeds_path, 'inputs', 'plant_characteristics', 'unitsize_atb.csv'), - fpath_out, - ) - elif sw['pras_unitsize_source'] == 'r2x': - fpath_in = os.path.join(reeds_path, 'inputs', 'plant_characteristics', 'pcm_defaults.json') - with open(fpath_in) as f: - pcm_defaults = json.load(f) - unitsize = pd.Series( - index=pcm_defaults.keys(), - data=[pcm_defaults[tech]['avg_capacity_MW'] for tech in pcm_defaults.keys()], - name='MW', - ).rename_axis('tech').dropna().astype(int) - unitsize.to_csv(fpath_out) - - -def generate_maps_gpkg(inputs_case): - """ - Write maps.gpkg to speed up map visualization in postprocessing. - If using region dis/aggregation, maps.gpkg is overwritten in aggregation_regions.py. - """ - mapsfile = os.path.join(inputs_case, 'maps.gpkg') - if os.path.exists(mapsfile): - os.remove(mapsfile) - - dfmap = reeds.io.get_dfmap(os.path.abspath(os.path.join(inputs_case,'..'))) - for level in dfmap: - dfmap[level].to_file(mapsfile, layer=level) - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== -def main(reeds_path, inputs_case): - """ - Run copy_files.py for use in the ReEDS workflow - - Parameters: - reeds_path : str (Path to the ReEDS directory) - inputs_case : str (Path to the run/inputs_case directory) - - Returns: - None (Writes files to the inputs_case directory) - """ - #%% =========================================================================== - ### --- Gather dataframes and dictionaries necessary for the script execution --- - ### =========================================================================== - # Obtain data necessary to filter and aggregate regions - regions_and_agglevel = get_regions_and_agglevel(reeds_path, inputs_case) - # Use agglevel_variables function to obtain spatial resolution variables - agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) - - #%% =========================================================================== - ### --- Copying files --- - ### =========================================================================== - - sw = reeds.io.get_switches(inputs_case) - - runfiles, non_region_files, region_files = read_runfiles( - reeds_path, - inputs_case, - sw, - agglevel_variables - ) - - # Write general GAMS files - # Write GAMS-readable sets to the inputs_case directory - write_GAMS_sets(runfiles, reeds_path, inputs_case) - - # Rewrite the switches tables as GAMS-readable definition - # (gswitches.csv is first written at runbatch.py) - scalar_csv_to_txt(os.path.join(inputs_case,'gswitches.csv')) - - source_deflator_map = get_source_deflator_map(reeds_path) - - # Copy non-region files - write_non_region_files(non_region_files, sw, inputs_case, regions_and_agglevel, source_deflator_map) - - # Copy region files - write_region_indexed_files( - inputs_case, - sw, - region_files, - regions_and_agglevel, - agglevel_variables, - source_deflator_map - ) - - # Write files used for disaggregation - write_disagg_data_files(runfiles, inputs_case) - - # Create a maps.gpkg for this run - # Skip if using region dis/aggregation, maps will be written in aggregation_regions.py. - # Run if using mixed resolution aggreg-county combination - if agglevel_variables['lvl'] == 'ba' or ( - agglevel_variables['lvl'] == 'mult' and 'aggreg' in agglevel_variables['agglevel']): - generate_maps_gpkg(inputs_case) - - #%% =========================================================================== - ### --- Exceptions --- - ### =========================================================================== - # Handle miscellaneous files not included in non_region_files, region_files. - # Needs to run after copy of non-region files - write_miscellaneous_files( - sw, - regions_and_agglevel, - agglevel_variables, - inputs_case, - reeds_path - ) - - -#%% Procedure -if __name__ == '__main__' and not hasattr(sys, 'ps1'): - # ---- Parse arguments ---- - parser = argparse.ArgumentParser(description="Copy files needed for this run") - parser.add_argument('reeds_path', help='ReEDS directory') - parser.add_argument('inputs_case', help='output directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - # #%% Settings for testing ### - # reeds_path = reeds.io.reeds_path - # inputs_case = os.path.join(reeds_path,'runs','v20260305_itlM0_USA_defaults','inputs_case') - - - # ---- Set up logger ---- - tic = datetime.datetime.now() - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - print('Starting copy_files.py') - main(reeds_path, inputs_case) - - reeds.log.toc(tic=tic, year=0, process='input_processing/copy_files.py', - path=os.path.join(inputs_case,'..')) - print('Finished copy_files.py') diff --git a/input_processing/forecast.py b/input_processing/forecast.py deleted file mode 100644 index d9cfaa1e..00000000 --- a/input_processing/forecast.py +++ /dev/null @@ -1,525 +0,0 @@ -""" -prbrown 20201109 16:22 - -Notes to user: --------------- -* This script loops over files in runs/{}/inputs_case/ and projects them forward -based on the directions given in inputs/userinput/futurefiles.csv. -If new files have been added to inputs_case, you'll need to add rows with -processing directions to futurefiles.csv. -The column names should be self-explanatory; most likely there's also at least -one similarly-formatted file in inputs_case that you can copy the settings for. -""" - -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import pandas as pd -import numpy as np -import gdxpds -import os -import sys -import shutil -from glob import glob -from warnings import warn -import datetime -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -# Time the operation of this script -tic = datetime.datetime.now() - -#%%################# -### FIXED INPUTS ### -decimals = 6 - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -the_unnamer = {f'Unnamed: {i}' : '' for i in range(1000)} - -def interpolate_missing_years(df, forecast_fit, method='linear'): - """ - Interpolates data for only the years required to perform the given file's forecast_fit. - e.g. if forecast_fit == 'linear_5', then interpolate any missing years' data for the - 5 years leading up to the last model year. - - Inputs - ------ - df : pd.DataFrame with columns as integer years - method : interpolation method [default = 'linear'] - """ - # Extract the number of years prior to last data year required for interpolation from - # forecast_fit string - fitlength = int(forecast_fit.split('_')[-1]) - - ### Gather a list of all years greater than the interpolation startyear up to the last - ### data year, and also include the column of the closest year <= interpolation startyear - interp_startyr = df.columns.max()-fitlength - g_yrs = df.columns[df.columns > (interp_startyr)] - l_eq_yrs = df.columns[df.columns <= (interp_startyr)] - if not l_eq_yrs.empty: - c_yr = l_eq_yrs.max() - interp_yrs = [c_yr] + list(g_yrs) - keep_yrs = l_eq_yrs[:-1] - - dfinterp = ( - ### Use only years required for interpolation - df[interp_yrs] - ### Switch column names from integer years to timestamps - .rename(columns={c: pd.Timestamp(str(c)) for c in df.columns}) - ### Add empty columns at year-starts between existing data - ### (mean doesn't do anything) - .resample('YS', axis=1).mean() - ### Interpolate linearly to fill the new columns - ### (interpolate only works on rows, so pivot, interpolate, pivot again) - .T.interpolate(method).T - ) - ### Switch back to integer-year column names - dfadd = dfinterp.rename(columns={c: c.year for c in dfinterp.columns}) - ### Remove any column in df that is in dfadd - dfout = df[keep_yrs].copy() - ### Add interpolated columns to dfout - dfout = pd.concat([dfout,dfadd], axis=1) - - return dfout - - -def forecast( - dfi, lastdatayear, addyears, forecast_fit, - clip_min=None, clip_max=None): - """ - Project additional year columns and add to df based on directions in forecast_fit. - forecast_fit can be in ['constant','linear_X','yoy_X','cagr_X','log_X'], - where 'X' is the number of years to use for the fit. - 'linear' projects linearly, while 'yoy','cagr','log' (all the same) projects - a constant compound annual growth rate. - """ - dfo = dfi.copy() - if forecast_fit == 'constant': - ### Assign each future year to last data year - for addyear in addyears: - dfo[addyear] = dfo[lastdatayear].copy() - elif forecast_fit.startswith('linear'): - ### Get the number of years to fit from the futurefiles entry - fitlength = int(forecast_fit.split('_')[1]) - fityears = list(range(lastdatayear-fitlength, lastdatayear+1)) - ### Get linear fits for all rows in parallel - x = np.vstack([fityears, np.ones_like(fityears)]).T - y = dfo[fityears].values - coefs, _, _, _ = np.linalg.lstsq(x, y.T, rcond=None) - # Extract slope and intercept data from result of least squares fit - slope = dict(zip(dfo.index, coefs[0])) - intercept = dict(zip(dfo.index, coefs[1])) - ### Apply the row-specific fits - for addyear in addyears: - ### Clip if desired, otherwise just project the fit - if (clip_min is not None) or (clip_max is not None): - dfo[addyear] = ( - dfo.index.map(intercept) + dfo.index.map(slope) * addyear - ).values.clip(clip_min,clip_max) - else: - dfo[addyear] = dfo.index.map(intercept) + dfo.index.map(slope) * addyear - elif (forecast_fit.startswith('cagr') - or forecast_fit.startswith('yoy') - or forecast_fit.startswith('log')): - ### Get the number of years to fit from the futurefiles entry - fitlength = int(forecast_fit.split('_')[1]) - fityears = list(range(lastdatayear-fitlength, lastdatayear+1)) - ### Fit each row individually. By taking the exp() of the fit to log(y) - ### we get the compound annual growth rate (cagr) + 1. - cagr = {} - for row in dfo.index: - cagr[row] = np.exp(np.polyfit( - x=fityears, y=np.log(dfo.loc[row, fityears].values), deg=1)[0]) - ### Apply the row-specific fits - for addyear in addyears: - ### Clip if desired, otherwise just project the fit - if (clip_min is not None) or (clip_max is not None): - dfo[addyear] = ( - dfo[lastdatayear] * (dfo.index.map(cagr) ** (addyear - lastdatayear)) - ).values.clip(clip_min,clip_max) - else: - dfo[addyear] = ( - dfo[lastdatayear] * (dfo.index.map(cagr) ** (addyear - lastdatayear))) - else: - raise Exception( - 'forecast_fit == {} is not implemented; try constant, linear, or cagr'.format( - forecast_fit)) - - return dfo - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== -if __name__ == '__main__': - - ### Parse arguments - parser = argparse.ArgumentParser(description='Extend inputs to arbitrary future year') - parser.add_argument('reeds_path', help='path to ReEDS directory') - parser.add_argument('inputs_case', help='path to inputs_case directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = os.path.join(args.inputs_case, '') - - # #%% Settings for testing - # reeds_path = os.path.expanduser('~/github/ReEDS-2.0') - # inputs_case = os.path.join(reeds_path,'runs','v20220411_prmM0_USA2060','inputs_case') - - #%% Settings for debugging - ### Set debug == True to write to a new folder (inputs_case/future/), leaving original files - ### intact. If debug == False (default), the original files are overwritten. - debug = False - ### missing: 'raise' or 'warn' - missing = 'raise' - ### verbose: 0, 1, 2 - verbose = 2 - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - endyear = int(sw.endyear) - distpvscen = sw.distpvscen - - #%%#################################### - ### If endyear <= 2050, exit the script - if endyear <= 2050: - print('endyear = {} <= 2050, so skip forecast.py'.format(endyear)) - quit() - else: - print('Starting forecast.py', flush=True) - - #%%################### - ### Derived inputs ### - - ### Get the case name (ReEDS-2.0/runs/{casename}/inputscase/) - casename = inputs_case.split(os.sep)[-3] - - ### DEBUG: Make the outputs directory - if debug: - outpath = os.path.join(inputs_case,'future','') - os.makedirs(outpath, exist_ok=True) - else: - outpath = inputs_case - ### Get the modeled years - tmodel_new = pd.read_csv( - os.path.join(inputs_case,'modeledyears.csv')).columns.astype(int).values - - ### Get the settings file - futurefiles = pd.read_csv( - os.path.join(inputs_case,'futurefiles.csv'), - dtype={ - 'header':'category', 'ignore':int, 'wide':int, - 'year_col':str, 'fix_cols':str, 'header':str, 'clip_min':str, 'clip_max':str, - } - ) - ### Fill in the missing parts of filenames - futurefiles.filename = futurefiles.filename.map( - lambda x: x.format(casename=casename, distpvscen=distpvscen) - ) - ### Fix issue where columns with "None" entries are read in as NaN - for col in ['key','fix_cols','header','clip_min','clip_max']: - futurefiles[col] = futurefiles[col].fillna('None') - ### If any files are missing, stop and alert the user - inputfiles = [os.path.basename(f) for f in glob(os.path.join(inputs_case,'*'))] - missingfiles = [ - f for f in inputfiles if ((f not in futurefiles.filename.values) and ('.' in f))] - if any(missingfiles): - if missing == 'raise': - raise Exception( - 'Missing future projection method for:\n{}\n' - '>>> Need to add entries for these files to futurefiles.csv' - .format('\n'.join(missingfiles)) - ) - else: - from warnings import warn - warn( - 'Missing future directions for:\n{}\n' - '>>> For this run, these files are copied without modification' - .format('\n'.join(missingfiles)) - ) - for filename in missingfiles: - shutil.copy(os.path.join(inputs_case, filename), os.path.join(outpath, filename)) - print('copied {}, which is missing from futurefiles.csv'.format(filename), - flush=True) - - #%% Loop it - for i in futurefiles.index: - filename = futurefiles.loc[i,'filename'] - print(f'{filename}', end='') - - ### If the file isn't in inputs_case, skip it and continue to the next file - if filename not in inputfiles: - if verbose > 1: - print(' -> skipped since not in inputs_case') - continue - - ### if ignore == 0, continue with forecasting procedures - if futurefiles.loc[i,'ignore'] == 0: - pass - ### if ignore == 1, just copy the file to outpath and skip the rest - elif futurefiles.loc[i,'ignore'] == 1: - if debug: - shutil.copy(os.path.join(inputs_case,filename), os.path.join(outpath,filename)) - if verbose > 1: - print(' -> ignored: "ignore" == 1 in futurefiles.csv', flush=True) - continue - ### if ignore == 2, need to project for EFS or copy otherwise - elif futurefiles.loc[i,'ignore'] == 2: - ### Read the file to determine if it's formatted for default or EFS load - dftest = pd.read_csv(os.path.join(inputs_case,filename), header=0, nrows=20) - ### If it has more than 10 columns (indicating EFS), follow the directions - if dftest.shape[1] > 10: - pass - ### Otherwise (indicating default), just copy it - else: - if debug: - shutil.copy(os.path.join(inputs_case,filename), os.path.join(outpath,filename)) - if verbose > 1: - print(' -> EFS special case: {}'.format(filename), flush=True) - continue - - - - #%% Settings - ### header: 0 if file has column labels, otherwise 'None' - header = (None if futurefiles.loc[i,'header'] == 'None' - else 'keepindex' if futurefiles.loc[i,'header'] == 'keepindex' - else int(futurefiles.loc[i,'header'])) - ### year_col: usually 't' or 'year', or 'wide' if file uses years as columns - year_col = futurefiles.loc[i, 'year_col'] - ### forecast_fit: '{method}_{fityears}' or 'constant', with method in - ### ['linear','cagr'] and fityears indicating the number of historical years - ### (counting backwards from the last data year) to use for the projection. - ### If set to 'constant', will use the value from the last data year for - ### all future years. - forecast_fit = futurefiles.loc[i,'forecast_fit'] - ### fix_cols: indicate columns to use as for fields that should be projected - ### independently to future years (e.g. r, szn, tech) - fix_cols = futurefiles.loc[i,'fix_cols'] - fix_cols = (list() if fix_cols == 'None' else fix_cols.split(',')) - ### wide: 1 if any parameters are in wide format, otherwise 0 - wide = futurefiles.loc[i, 'wide'] - ### clip_min, clip_max: Indicate min and max values for projected data. - ### In general, costs should have clip_min = 0 (so they don't go negative) - clip_min, clip_max = futurefiles.loc[i,['clip_min','clip_max']] - clip_min = (None if clip_min.lower()=='none' else int(clip_min)) - clip_max = (None if clip_max.lower()=='none' else int(clip_max)) - filetype = futurefiles.loc[i, 'filetype'] - ### key: only used for gdx files, indicating the parameter name. - ### gdx files need a separate line in futurefiles.csv for each parameter. - key = futurefiles.loc[i,'key'] - efs = False - - ### Load it - if filetype in ['.csv','.csv.gz']: - dfin = pd.read_csv(os.path.join(inputs_case,filename), header=header,) - elif filetype == '.h5': - ### Currently load.h5 and dr_shed_hourly.h5 are the only h5 files we need to - ### project forward, so the procedure is currently specific to these files - dfin = reeds.io.read_file( - os.path.join(inputs_case,filename), - parse_timestamps=True, - ) - # dfin = pd.read_hdf(os.path.join(inputs_case,filename)) - if header == 'keepindex': - indexnames = list(dfin.index.names) - dfin = dfin.reset_index() - # Make a copy of dfin to prevent "PerformanceWarning: DataFrame is highly fragmented." error - dfin = dfin.copy() - ### We only need to do the projection for load.h5 if we're using EFS load, - ### which has a (year,hour) multiindex (which we reset above to columns). - ### If dfin doesn't have 'year' and 'datetime' columns, we can skip this file. - if (('year' in dfin.columns) and ('datetime' in dfin.columns)): - efs = True - else: - if debug: - shutil.copy(os.path.join(inputs_case,filename), os.path.join(outpath,filename)) - if verbose > 1: - print(f' -> ignored: {filename}', flush=True) - continue - elif filetype == '.txt': - dfin = pd.read_csv(os.path.join(inputs_case, filename), header=header, sep=' ') - ### Remove parentheses and commas - for col in [0,1]: - dfin[col] = dfin[col].map( - lambda x: x.replace('(','').replace(')','').replace(',','')) - ### Split the index column on periods - num_indices = len(dfin.loc[0,0].split('.')) - indexcols = ['i{}'.format(index) for index in range(num_indices)] - for index in range(num_indices): - dfin['i{}'.format(index)] = dfin[0].map(lambda x: x.split('.')[index]) - ### Make the data column numeric - dfin[1] = dfin[1].astype(float) - ### Reorder and rename the columns - dfin = dfin[indexcols + [1]].copy() - dfin.columns = list(range(num_indices+1)) - elif filetype == '.gdx': - ### Read in the full gdx file, but only change the 'key' parameter - ### given in futurefiles. That's wasteful, but there are currently no - ### active gdx files. - dfall = gdxpds.to_dataframes(os.path.join(inputs_case,filename)) - dfin = dfall[key] - else: - raise Exception('Unsupported filetype: {}'.format(filename)) - - dfin.rename(columns={c:str(c) for c in dfin.columns}, inplace=True) - columns = dfin.columns.tolist() - - if dfin.empty: - if verbose > 1: - print(' -> Empty dataframe: Skipping...') - continue - if (('year' in dfin.columns) and ('datetime' in dfin.columns)): - dfcheck = dfin.set_index(['datetime','year']) - if dfcheck.empty: - if verbose > 1: - print(' -> Empty dataframe: Skipping...') - continue - - #%% Reshape to wide format with year as column - if (len(fix_cols) == 0) and (wide == 0): - ### File is simply (year,data) - ### So just set year as index and transpose - df = dfin.set_index(year_col).T - elif (len(fix_cols) > 0) and (year_col == 'wide'): - ### File has some fixed columns and then years in wide format - ### Easy - just set the fix_cols as indices and keep years as columns - df = dfin.set_index(fix_cols) - elif (wide) and (year_col != 'wide') and (len(fix_cols) == 0): - ### Some value other than year is in wide format - ### So set years as index and transpose - df = dfin.set_index(year_col).T - elif (wide) and (year_col != 'wide') and (len(fix_cols) > 0): - ### Some value other than year is in wide format - ### So set years (and other fix_cols) as index and transpose - df = (dfin.melt(id_vars=[year_col]+fix_cols, ignore_index=False) - .set_index([year_col]+fix_cols+['variable']) - .unstack(year_col)) - ### Get the value name (in this case for the non-year wide column), then drop it - valuename = df.columns.get_level_values(0).unique().tolist() - if len(valuename) > 1: - raise Exception('Too many data columns: {}'.format(valuename)) - valuename = valuename[0] - df = df[valuename].copy() - elif (len(fix_cols) > 0) and (year_col != 'wide') and (not wide): - ### Tidy format - fix the fix_cols and unstack the year_col - ### same as `df = dfin.pivot(index=fix_cols, columns=year_col)`, but - ### pivot modifies fix_cols for some reason - df = dfin.set_index(fix_cols+[year_col]).unstack(year_col)#.droplevel(0, axis=1) - ### Get the value name, then drop it - valuename = df.columns.get_level_values(0).unique().tolist() - if len(valuename) > 1: - raise Exception('Too many data columns: {}'.format(valuename)) - valuename = valuename[0] - df = df[valuename].copy() - else: - raise Exception('Unknown data type for {}'.format(filename)) - - #%% All columns should now be years - df.rename(columns={c: int(c) for c in df.columns}, inplace=True) - lastdatayear = max([int(c) for c in df.columns]) - ### Create list of non-interpolated years (for filtering out interpolated year data later) - years_orig = df.columns.tolist() - ### Interpolate only required years for linear forecasting. Skip if forecast_fit - ### is 'constant' - if 'linear' in forecast_fit: - df = interpolate_missing_years(df, forecast_fit) - ### Get indices for projection - addyears = list(range(lastdatayear+1, endyear+1)) - ### If file is for EFS hourlyload, only project for years in tmodel_new to save time - if efs: - addyears = [y for y in addyears if y in tmodel_new] - - #%% Do the projection - df = forecast( - dfi=df, lastdatayear=lastdatayear, addyears=addyears, - forecast_fit=forecast_fit, clip_min=clip_min, clip_max=clip_max) - ### Remove years used for interpolation - keep only original and forecasted years - df = df[years_orig + addyears] - - #%% Put back in original format - if (len(fix_cols) == 0) and (wide == 0): - dfout = df.T.reset_index() - elif (len(fix_cols) > 0) and (year_col == 'wide'): - dfout = df.reset_index() - elif (wide) and (year_col != 'wide') and (len(fix_cols) == 0): - dfout = df.T.reset_index() - elif (wide) and (year_col != 'wide') and (len(fix_cols) > 0): - dfout = df.stack().unstack('variable').reset_index()[columns] - elif (len(fix_cols) > 0) and (year_col != 'wide') and (not wide): - dfout = df.stack().rename(valuename).dropna().reset_index()[columns] - - ### Unname any unnamed columns - dfout.rename(columns=the_unnamer, inplace=True) - - #%% Write it - if filetype in ['.csv','.csv.gz']: - dfout.round(decimals).to_csv( - os.path.join(outpath, filename), - header=(False if header is None else True), - index=False, - ) - elif filetype == '.h5': - if header == 'keepindex': - dfwrite = dfout.sort_values(indexnames).set_index(indexnames) - dfwrite.columns.name = None - else: - dfwrite = dfout - ### Special Case: ensure year col for dr_shed_hourly.h5 is same dtype as data - ### to prevent errors in write_profile_to_h5 - if filename == 'dr_shed_hourly.h5': - dfwrite[year_col] = dfwrite[year_col].astype(np.float32) - reeds.io.write_profile_to_h5(dfwrite.round(decimals),filename,outpath) - elif filetype == '.txt': - dfwrite = dfout.sort_index(axis=1) - ### Make the GAMS-readable index - dfwrite.index = dfwrite.apply( - lambda row: '(' + '.'.join([str(row[str(c)]) for c in range(num_indices)]) + ')', - axis=1 - ) - ### Downselect to data column - dfwrite = dfwrite[str(num_indices)].round(decimals) - ### Add commas to data column, remove from last entry - dfwrite = dfwrite.astype(str) + ',' - dfwrite.iloc[-1] = dfwrite.iloc[-1].replace(',','') - ### Write it - dfwrite.to_csv( - os.path.join(outpath, filename), - header=(False if header is None else True), - index=True, sep=' ', - ) - elif filetype == '.gdx': - ### Overwrite the projected parameter - dfall[key] = dfout.round(decimals) - ### Write the whole file - gdxpds.to_gdx(dfall, outpath+filename) - - if verbose > 1: - print( - f' -> Projected from {lastdatayear} to {endyear}', - flush=True) - - reeds.log.toc(tic=tic, year=0, process='input_processing/forecast.py', - path=os.path.join(inputs_case,'..')) - - print('Finished forecast.py') - -## ############################## -## ### Initial one-time setup ### -## infiles = pd.DataFrame( -## {'filename': [os.path.basename(f) for f in glob(os.path.join(inputs_case,'*'))]}) -## infiles['filetype'] = infiles.filename.map(lambda x: os.path.splitext(x)[1]) -## infiles.sort_values(['filetype','filename'], inplace=True) -## infiles.to_csv( -## os.path.join(reeds_path, 'inputs', 'userinput', 'futurefiles.csv'), -## index=False -## ) \ No newline at end of file diff --git a/input_processing/fuelcostprep.py b/input_processing/fuelcostprep.py deleted file mode 100644 index d697464c..00000000 --- a/input_processing/fuelcostprep.py +++ /dev/null @@ -1,175 +0,0 @@ -''' -The purpose of this script is to write out fuel costs for the following fuels at -census division level: - - coal - - uranium - - H2 (for H2-CT/CC tech) - - natural gas -Additionally, this script also writes out natural gas demand (total NG demand as -well as NG demand for electricity generation) and natural gas alphas -''' -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import pandas as pd -import os -import sys -import argparse -import datetime -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -# Time the operation of this script -tic = datetime.datetime.now() - -#%% Parse arguments -parser = argparse.ArgumentParser(description="""This file organizes fuel cost data by techonology""") - -parser.add_argument("reeds_path", help='ReEDS-2.0 directory') -parser.add_argument("inputs_case", help='ReEDS-2.0/runs/{case}/inputs_case directory') - -args = parser.parse_args() -reeds_path = args.reeds_path -inputs_case = args.inputs_case - -# #%% Settings for testing -# reeds_path = 'd:\\Danny_ReEDS\\ReEDS-2.0' -# reeds_path = os.getcwd() -# inputs_case = os.path.join('runs','nd5_ND','inputs_case') - -#%% Set up logger -log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), -) -print("Starting fuelcostprep.py") - -#%% Inputs from switches -sw = reeds.io.get_switches(inputs_case) - -# Load valid regions -val_r = pd.read_csv( - os.path.join(inputs_case, 'val_r.csv'), header=None).squeeze(1).tolist() -val_cendiv = pd.read_csv( - os.path.join(inputs_case, 'val_cendiv.csv'), header=None).squeeze(1).tolist() - -r_cendiv = pd.read_csv(os.path.join(inputs_case,"r_cendiv.csv")) - -dollaryear = pd.read_csv(os.path.join(inputs_case, "dollaryear_fuel.csv")) -deflator = pd.read_csv(os.path.join(inputs_case,'deflator.csv')) -deflator.columns = ["Dollar.Year","Deflator"] -dollaryear = dollaryear.merge(deflator,on="Dollar.Year",how="left") - -#%% =========================================================================== -### --- PROCEDURE: FUEL PRICE CALCULATIONS --- -### =========================================================================== - -#################### -# -- Coal -- # -#################### -coal = pd.read_csv(os.path.join(inputs_case, 'coal_price.csv')) -coal = coal.melt(id_vars = ['year']).rename(columns={'variable':'cendiv'}) -coal = coal.loc[coal['cendiv'].isin(val_cendiv)] - -# Adjust prices to 2004$ -deflate = dollaryear.loc[dollaryear['Scenario'] == sw.coalscen,'Deflator'].values[0] -coal.loc[:,'value'] = coal['value'] * deflate - -coal = coal.merge(r_cendiv,on='cendiv',how='left') -coal = coal.drop('cendiv', axis=1) -coal = coal[['year','r','value']].rename(columns={'year':'t','value':'coal'}) -coal.coal = coal.coal.round(6) - -####################### -# -- Uranium -- # -####################### -uranium = pd.read_csv(os.path.join(inputs_case, 'uranium_price.csv')) - -# Adjust prices to 2004$ -deflate = dollaryear.loc[dollaryear['Scenario'] == sw.uraniumscen,'Deflator'].values[0] -uranium.loc[:,'cost'] = uranium['cost'] * deflate -uranium = pd.concat([uranium.assign(r=i) for i in val_r], ignore_index=True) -uranium = uranium[['year','r','cost']].rename(columns={'year':'t','cost':'uranium'}) -uranium.uranium = uranium.uranium.round(6) - -############################# -# -- H2-Combustion -- # -############################# -# note that these fuel inputs are not used when H2 production is run endogenously in ReEDS (GSw_H2 > 0) -h2fuel = pd.read_csv(os.path.join(inputs_case, 'hydrogen_price.csv'), index_col='year') - -#Adjust prices to 2004$ -deflate = dollaryear.loc[dollaryear['Scenario'] == sw.h2combustionfuelscen,'Deflator'].squeeze() -h2fuel['cost'] = h2fuel['cost'] * deflate -# Reshape from [:,[t,cost]] to [:,[t,r,cost]] -h2fuel = ( - pd.concat({r:h2fuel for r in val_r}, axis=0, names=['r']) - .reset_index().rename(columns={'year':'t','cost':'h2fuel'}) - [['t','r','h2fuel']] - .round(6) -) - -########################### -# -- Natural Gas -- # -########################### - -ngprice = pd.read_csv(os.path.join(inputs_case,'natgas_price_cendiv.csv')) -ngprice = ngprice.melt(id_vars=['year']).rename(columns={'variable':'cendiv'}) -ngprice = ngprice.loc[ngprice['cendiv'].isin(val_cendiv)] - -# Adjust prices to 2004$ -deflate = dollaryear.loc[dollaryear['Scenario'] == sw.ngscen,'Deflator'].values[0] -ngprice.loc[:,'value'] = ngprice['value'] * deflate - -# Save Natural Gas prices by census region -ngprice_cendiv = ngprice.copy() -ngprice_cendiv = ngprice_cendiv.pivot_table(index='cendiv',columns='year',values='value') -ngprice_cendiv = ngprice_cendiv.round(6) - -# Map census regions to model regions -ngprice = ngprice.merge(r_cendiv,on='cendiv',how='left') -ngprice = ngprice.drop('cendiv', axis=1) -ngprice = ngprice[['year','r','value']].rename(columns={'year':'t','value':'naturalgas'}) -ngprice.naturalgas = ngprice.naturalgas.round(6) - -# Combine all fuel data -fuel = coal.merge(uranium,on=['t','r'],how='left') -fuel = fuel.merge(ngprice,on=['t','r'],how='left') -fuel = fuel.merge(h2fuel,on=['t','r'],how='left') -fuel = fuel.sort_values(['t','r']) - -#%%#################################### -### Natural Gas Demand Calculations ### - -# Natural Gas demand -ngdemand = pd.read_csv(os.path.join(inputs_case,'ng_demand_elec.csv'), index_col='year') -ngdemand = ngdemand[ngdemand.columns[ngdemand.columns.isin(val_cendiv)]] -ngdemand = ngdemand.transpose() -ngdemand = ngdemand.round(6) - -# Total Natural Gas demand -ngtotdemand = pd.read_csv(os.path.join(inputs_case, 'ng_demand_tot.csv'), index_col='year') -ngtotdemand = ngtotdemand[ngtotdemand.columns[ngtotdemand.columns.isin(val_cendiv)]] -ngtotdemand = ngtotdemand.transpose() -ngtotdemand = ngtotdemand.round(6) - -### Natural Gas Alphas (already in 2004$) -alpha = pd.read_csv(os.path.join(inputs_case, 'alpha.csv'), index_col='t') -alpha = alpha[alpha.columns[alpha.columns.isin(val_cendiv)]] -alpha = alpha.round(6) - -#%%################### -### Data Write-Out ### -###################### - -fuel.to_csv(os.path.join(inputs_case,'fprice.csv'),index=False) -ngprice_cendiv.to_csv(os.path.join(inputs_case,'gasprice_ref.csv')) - -ngdemand.to_csv(os.path.join(inputs_case,'ng_demand_elec.csv')) -ngtotdemand.to_csv(os.path.join(inputs_case,'ng_demand_tot.csv')) -alpha.to_csv(os.path.join(inputs_case,'alpha.csv')) - -reeds.log.toc(tic=tic, year=0, process='input_processing/fuelcostprep.py', - path=os.path.join(inputs_case,'..')) - -print('Finished fuelcostprep.py') diff --git a/input_processing/h2_storage.py b/input_processing/h2_storage.py deleted file mode 100644 index 69e29df5..00000000 --- a/input_processing/h2_storage.py +++ /dev/null @@ -1,141 +0,0 @@ -''' -This script calculates the H2 storage type for each model region. -Specifically, the script identifies the storage sites that exist in each -zone and associates the zone with its cheapest available storage site type. -''' - -import argparse -import pandas as pd -import os -import sys -import datetime -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== -def main(reeds_path, inputs_case): - print('Starting h2_storage.py') - - # Get model regions - dfzones = reeds.io.get_dfmap( - os.path.dirname(inputs_case), - levels=['r'], - exclude_water_areas=True - )['r'] - dfzones['geometry'] = dfzones['geometry'].buffer(0.) - dfzones['km2'] = dfzones.geometry.area / 1e6 - - for h2_storage_type in ['hardrock', 'salt']: - # Get storage sites of the given type and combine them into one region - h2_storage_sites = reeds.io.get_h2_storage_sites( - h2_storage_type=h2_storage_type - ) - h2_storage_region = ( - h2_storage_sites.dissolve() - .loc[0,'geometry'] - .buffer(0.) - ) - - # Calculate the areas of intersection between model regions and the - # collection of storage sites as percentages of total model region area - dfzones[h2_storage_type] = dfzones.intersection(h2_storage_region) - dfzones[h2_storage_type+'_km2'] = ( - dfzones[h2_storage_type].area / 1e6 - ) - dfzones[h2_storage_type+'_frac'] = ( - dfzones[h2_storage_type+'_km2'] / dfzones['km2'] - ) - - # Determine the H2 storage types available in each model region - # and reformat dataframe - scalars = reeds.io.get_scalars() - dfout = ( - pd.concat( - { - col: pd.Series( - dfzones.loc[( - dfzones[col+'_frac'] - > scalars["h2_storage_area_threshold"] - )] - .index - .values - ) - for col in ['hardrock','salt'] - } - ) - .reset_index(level=1, drop=True) - .rename('rb') - .reset_index() - .rename(columns={'index':'*h2stortype'}) - .assign(exists=1) - .pivot(index='rb',columns='*h2stortype',values='exists') - .reindex(dfzones.index.rename('rb')) - .fillna(0) - .astype(int) - ) - - # Downselect to one row per model region, selecting the cheapeast storage - # type available in the region, assuming salt is cheaper than hardrock. - outname = { - 'hardrock':'h2_storage_hardrock', - 'salt':'h2_storage_saltcavern', - 'underground':'h2_storage_undergroundpipe', - } - dfout['keep'] = ( - dfout.apply( - lambda row: ( - 'salt' if row.get('salt', False) - else 'hardrock' if row.get('hardrock', False) - else 'underground' - ), - axis=1 - ) - .replace(outname) - ) - dfout = ( - dfout.reset_index() - .rename(columns={'keep':'*h2_stor'}) - [['*h2_stor','rb']] - ) - - dfout.to_csv( - os.path.join(inputs_case, 'h2_storage_rb.csv'), - index=False - ) - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if __name__ == '__main__': - # Time the operation of this script - tic = datetime.datetime.now() - - ### Parse arguments - parser = argparse.ArgumentParser( - description='Process H2 storage inputs', - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument('reeds_path', help='ReEDS-2.0 directory') - parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - #%% Run it - main(reeds_path=reeds_path, inputs_case=inputs_case) - - reeds.log.toc(tic=tic, year=0, process='input_processing/h2_storage.py', - path=os.path.join(inputs_case,'..')) - - print('Finished h2_storage.py') \ No newline at end of file diff --git a/input_processing/hourly_load.py b/input_processing/hourly_load.py deleted file mode 100644 index 8b2114fb..00000000 --- a/input_processing/hourly_load.py +++ /dev/null @@ -1,783 +0,0 @@ -''' -This script handles the modification of load data. Specifically, it converts -state-level hourly end-use load to model region-level busbar load by doing -the following: - -- Allocate state load to model regions according to the method specified - in GSw_LoadAllocationMethod -- Apply scenario-specific modifications: - EER scenarios: - - Append historical load for pre-2021 model years - - Interpolate projected load for missing model years - - Apply calibration factors to projected load based on the difference - between historical and projected load in the latest year for which - historical and projected load data exist - Historical: - - Apply annual load growth factors - Other: - - If needed, replicate the dataset to match the number of weather years - specified for this run -- Apply a distribution loss factor - -The script also calculates peak load for each region level. -''' - -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import datetime -import numpy as np -import os -import pandas as pd -from pathlib import Path -import sys -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -def get_historical_state_load_for_model_year( - historical_state_load_annual: pd.DataFrame, - model_year: int -) -> pd.Series: - """ - Get historical annual state loads in MWh for the model year. - - Args: - historical_state_load_annual: Annual historical state loads in MWh. - model_year: Year to retrieve load values for. - - Returns: - pd.Series - """ - return ( - historical_state_load_annual - .loc[historical_state_load_annual.year == model_year] - .set_index('st') - ['MWh'] - ) - -def scale_historical_hourly_state_load_to_model_year( - historical_state_load_hourly: pd.DataFrame, - historical_state_load_annual: pd.DataFrame, - model_year: int -) -> pd.DataFrame: - """ - Scale historical hourly state load profiles to match historical - annual totals for the specified model year. - - Args: - historical_state_load_hourly: Hourly historical state load profiles - in MWh. - historical_state_load_annual: Annual historical state loads in MWh. - model_year: Year of annual load to scale hourly load by. - - Returns: - pd.DataFrame - """ - historical_model_year_state_load = get_historical_state_load_for_model_year( - historical_state_load_annual, - model_year - ) - # Calculate total state loads for each weather year - # of the historical hourly state load profiles - historical_weather_year_state_loads = ( - historical_state_load_hourly.groupby( - historical_state_load_hourly.index.get_level_values('datetime').year - ) - .transform('sum') - ) - # Scale the historical hourly state load profiles so that total state - # loads for each weather year match state loads for the model year - historical_state_load_hourly_scaled = ( - historical_state_load_hourly - / historical_weather_year_state_loads - * historical_model_year_state_load - ) - - return historical_state_load_hourly_scaled - -def interpolate_missing_model_years( - load_hourly: pd.DataFrame, - endyear: int -) -> pd.DataFrame: - """ - Linearly interpolate hourly load values for missing model years between - the first year of the load profiles and the specified end year. - - Args: - load_hourly: Hourly load profiles. - endyear: Final model year of resulting load profiles. - - Returns: - pd.DataFrame - """ - model_years = [ - year for year in - range(load_hourly.index.get_level_values('year').min(), endyear + 1) - ] - known_model_years = [ - year for year in - model_years if year in load_hourly.index.get_level_values('year') - ] - - dictload = {} - for model_year in model_years: - #find known years that bound this year - for i, known_model_year in enumerate(known_model_years): - if(known_model_year > model_year): - section_end_model_year = known_model_year - section_start_model_year = known_model_years[i-1] - break - - #grab dataframes for linear interpolation - df_load_beg = load_hourly.loc[section_start_model_year] - df_load_end = load_hourly.loc[section_end_model_year] - - #linear interpolation: - # y = y1 + (y2-y1)/(x2-x1)*(x-x1). x is year; y is value - df_load = ( - df_load_beg + - (df_load_end - df_load_beg) - / (section_end_model_year - section_start_model_year) - * (model_year-section_start_model_year) - ) - - dictload[model_year] = df_load - - load_hourly = pd.concat(dictload, names=('year',)) - - return load_hourly - -def calibrate_hourly_state_load_to_historical_annuals( - state_load_hourly: pd.DataFrame, - historical_state_load_annual: pd.DataFrame -) -> pd.DataFrame: - """ - For historical model years, scale hourly state load profiles to match - historical annual totals. For post-historical model years, scale hourly - state load profiles to increase the projected annual totals by the - difference between historical and projected annual totals for the - latest historical model year. - - Args: - state_load_hourly: Hourly state load profiles in MWh. - historical_state_load_annual: Annual historical state loads in MWh. - - Returns: - pd.DataFrame - """ - df_list = [] - - # For the model years for which we have historical annual loads, scale - # state_load_hourly so that its annual totals match each model year's - # historical annual loads - min_projected_model_year = ( - state_load_hourly.index.get_level_values('year').min() - ) - max_historical_model_year = historical_state_load_annual['year'].max() - for model_year in range( - min_projected_model_year, - max_historical_model_year + 1 - ): - model_year_historical_load = get_historical_state_load_for_model_year( - historical_state_load_annual, - model_year - ) - state_load_hourly_model_year = ( - state_load_hourly - .loc[( - state_load_hourly.index.get_level_values('year') == model_year - )] - .copy() - ) - calibration_factors = model_year_historical_load.div( - state_load_hourly_model_year - .groupby( - state_load_hourly_model_year.index - .get_level_values('datetime') - .year - ) - .transform('sum') - ) - state_load_hourly_model_year_scaled = ( - state_load_hourly_model_year.mul(calibration_factors) - ) - df_list.append(state_load_hourly_model_year_scaled) - - # For the latest model year for which we have historical annual loads - # (the calibration year), calculate the differences between - # the historical annual loads and projected annual loads - calibration_year_historical_load = ( - get_historical_state_load_for_model_year( - historical_state_load_annual, - max_historical_model_year - ) - ) - state_load_hourly_calibration_year = ( - state_load_hourly.loc[max_historical_model_year] - ) - calibration_diffs = calibration_year_historical_load.sub( - state_load_hourly_calibration_year - .groupby( - state_load_hourly_calibration_year.index - .get_level_values('datetime') - .year - ) - .transform('sum') - ) - - # For post-historical model years, scale state_load_hourly so that its - # annual totals match the sum of each model year's projected annual loads - # and the historical/projected load differences in the calibration year - max_projected_model_year = ( - state_load_hourly.index.get_level_values('year').max() - ) - for model_year in range( - max_historical_model_year + 1, - max_projected_model_year + 1 - ): - state_load_hourly_model_year = state_load_hourly.loc[model_year] - model_year_projected_load = ( - state_load_hourly_model_year - .groupby( - state_load_hourly_model_year.index - .get_level_values('datetime') - .year - ) - .transform('sum') - ) - calibration_factors = ( - model_year_projected_load.add(calibration_diffs) - .div(model_year_projected_load) - ) - state_load_hourly_model_year_scaled = ( - state_load_hourly_model_year - .mul(calibration_factors) - .assign(year=model_year) - .set_index('year', append=True) - .reorder_levels(['year', 'datetime']) - ) - df_list.append(state_load_hourly_model_year_scaled) - - state_load_hourly = pd.concat(df_list) - - return state_load_hourly - -def prepend_historical_hourly_state_load( - state_load_hourly: pd.DataFrame, - historical_state_load_hourly: pd.DataFrame, - historical_state_load_annual: pd.DataFrame -) -> pd.DataFrame: - """ - Create hourly state load profiles for historical model years and - prepend them to state_load_hourly. - - Args: - state_load_hourly: Hourly state load profiles in MWh. - historical_state_load_hourly: Hourly historical state load profiles - in MWh. - historical_state_load_annual: Annual historical state loads in MWh. - - Returns: - pd.DataFrame - """ - historical_load_dict = {} - - # For historical model years with no projected load profiles, create load - # profiles for each model year by scaling the historical load profiles to - # match annual totals for the model year - min_historical_model_year = historical_state_load_annual['year'].min() - min_projected_model_year = ( - state_load_hourly.index.get_level_values('year').min() - ) - for model_year in range( - min_historical_model_year, min_projected_model_year - ): - historical_state_load_hourly_scaled = ( - scale_historical_hourly_state_load_to_model_year( - historical_state_load_hourly, - historical_state_load_annual, - model_year - ) - ) - historical_load_dict[model_year] = historical_state_load_hourly_scaled - - historical_state_load_hourly = pd.concat( - historical_load_dict, - names=('year',) - ) - state_load_hourly = pd.concat([ - historical_state_load_hourly, - state_load_hourly - ]) - - return state_load_hourly - -def apply_load_growth_factors_to_historical_state_load( - historical_state_load_hourly: pd.DataFrame, - historical_state_load_annual: pd.DataFrame, - inputs_case: str, - solveyears: list[int] | None = None -) -> pd.DataFrame: - """ - Multiply hourly historical load profiles (scaled to match historical - annual totals for a baseline year) by annual load growth factors to - create projected load profiles for each model year. - - Args: - historical_state_load_hourly: Hourly historical state load - profiles in MWh. - historical_state_load_annual: Annual state loads in MWh - for historical years. - inputs_case: Path to the inputs case directory. - solveyears: Optional list of model years to filter load - multipliers down to. - - Returns: - pd.DataFrame - """ - # Read annual state multipliers representing projected load growth - # from a baseline year - load_multiplier = pd.read_csv( - os.path.join(inputs_case, 'load_multiplier.csv') - ) - # Scale the historical load profiles to match annual totals - # for the baseline year - load_multiplier_baseline_year = load_multiplier['year'].min() - historical_state_load_hourly = ( - scale_historical_hourly_state_load_to_model_year( - historical_state_load_hourly, - historical_state_load_annual, - load_multiplier_baseline_year - ) - ) - # Subset load multipliers for solve years only - if solveyears is not None: - load_multiplier = ( - load_multiplier[load_multiplier['year'].isin(solveyears)] - [['year', 'r', 'multiplier']] - ) - # Reformat hourly load profiles to merge with load multipliers - historical_state_load_hourly.reset_index(drop=False, inplace=True) - historical_state_load_hourly = pd.melt( - historical_state_load_hourly, - id_vars=['datetime'], - var_name='r', - value_name='load' - ) - # Merge load multipliers into hourly load profiles - state_load_hourly = historical_state_load_hourly.merge( - load_multiplier, - on=['r'], - how='outer' - ) - state_load_hourly.sort_values( - by=['r', 'year'], - ascending=True, - inplace=True - ) - state_load_hourly['load'] *= state_load_hourly['multiplier'] - state_load_hourly = state_load_hourly[['year', 'datetime', 'r', 'load']] - # Reformat hourly load profiles for GAMS - state_load_hourly = state_load_hourly.pivot_table( - index=['year', 'datetime'], columns='r', values='load') - # Convert 'year' index to integers - state_load_hourly.index = ( - state_load_hourly.index - .set_levels( - [ - state_load_hourly.index.levels[0].astype(int), - state_load_hourly.index.levels[1] - ], - level=['year', 'datetime'] - ) - ) - - return state_load_hourly - -def downselect_to_model_years( - load_hourly: pd.DataFrame, - model_years: list[int] -) -> pd.DataFrame: - """ - Retrieve the subset of hourly load profiles corresponding - to the given model years. - - Args: - load_hourly: Hourly load profiles. - model_years: List of model years used to filter load_hourly. - These years should correspond to load_hourly's "year" - index level. - - Returns: - pd.DataFrame - """ - return ( - load_hourly.loc[( - load_hourly.index - .get_level_values('year') - .isin(model_years) - )] - ) - -def downselect_to_weather_years( - load_hourly: pd.DataFrame, - weather_years: list[int] -) -> pd.DataFrame: - """ - Retrieve the subset of hourly load profiles corresponding - to the given weather years. - - Args: - load_hourly: Hourly load profiles. - weather_years: List of weather years used to filter load_hourly. - These years should correspond to the years of load_hourly's - "datetime" index level. - - Returns: - pd.DataFrame - """ - return ( - load_hourly.loc[( - load_hourly.index - .get_level_values('datetime') - .year - .isin(weather_years) - )] - ) - -def duplicate_weather_years(load_hourly, weather_years): - """ - Replicate hourly load profiles to match the number of weather years. - - Args: - load_hourly: Hourly load profiles with only one weather year of data. - weather_years: List of weather years to replicate load profiles for. - - Returns: - pd.DataFrame - """ - # Copy the load profiles n times for the number of weather years and - # concatenate them - num_years = len(weather_years) - load_hourly_wide = load_hourly.unstack('year') - - if len(load_hourly_wide) != 8760: - raise ValueError( - "The provided dataframe has more than one weather year of data." - ) - - load_hourly = ( - pd.concat([load_hourly_wide] * num_years, axis=0, ignore_index=True) - .rename_axis('hour').stack('year') - .reorder_levels(['year','hour']).sort_index(axis=0, level=['year','hour']) - ) - # Update the time index of the concatenated load profile to contain - # the hours of each weather year - fulltimeindex = pd.Series(reeds.timeseries.get_timeindex(weather_years)) - load_hourly['datetime'] = ( - load_hourly.index.get_level_values('hour').map(fulltimeindex) - ) - load_hourly = load_hourly.set_index('datetime', append=True).droplevel('hour') - - return load_hourly - -def apply_distribution_loss_factor( - load_hourly: pd.DataFrame, - distloss: float = 0.05 -) -> pd.DataFrame: - """ - Adjust hourly end-use load profiles to account for energy - lost during transmission and distribution. - - Args: - load_hourly: Hourly load profiles. - distloss: Percentage of busbar load lost during - transmission and distribution. - - Returns: - pd.DataFrame - """ - return load_hourly / (1 - distloss) - -def calculate_peak_load( - load_hourly: pd.DataFrame, - hierarchy: pd.DataFrame -) -> pd.DataFrame: - """ - Calculate coincident peak demand at all region hierarchy levels. - - Args: - load_hourly: Hourly load profiles. - hierarchy: Model region hierarchy levels. - - Returns: - pd.DataFrame - """ - _peakload = {} - for _level in hierarchy.columns: - _peakload[_level] = ( - ## Aggregate to level - load_hourly.rename(columns=hierarchy[_level]) - .groupby(axis=1, level=0).sum() - ## Calculate peak - .groupby(axis=0, level='year').max() - .T - ) - - ## Also calculate it at r level - _peakload['r'] = load_hourly.groupby(axis=0, level='year').max().T - peakload = pd.concat(_peakload, names=['level','region']).round(3) - - return peakload - -def reaggregate_to_model_regions( - state_load_hourly: pd.DataFrame, - inputs_case: str, - GSw_LoadAllocationMethod: str, - dr_data: bool = False -) -> pd.DataFrame: - """ - Allocate hourly state load to model regions according to the provided - load allocation method (e.g., according to each region's share of - state population). - - Args: - state_load_hourly: Hourly state load profiles. - inputs_case: Path to the inputs case directory. - GSw_LoadAllocationMethod: Method by which to allocate state - load to model regions. - - Returns: - pd.DataFrame - """ - # Get state/region-to-county disaggregation factors - disagg_data = reeds.io.get_disagg_data( - os.path.dirname(inputs_case), - disagg_variable=GSw_LoadAllocationMethod - ) - # Calculate state-to-region aggregation/disaggregation factors - state_region_factors = ( - disagg_data.groupby(['state', 'r'], as_index=False) - ['state_frac'] - .sum() - .pivot(index='state', columns='r', values='state_frac') - .rename_axis(None, axis=1) - .fillna(0) - ) - # Identify regions with aggregation/disaggregation factors of 0 - # and raise an error if any exist - if state_region_factors.sum().min() == 0: - regional_factors = state_region_factors.sum() - no_load_regions = ( - regional_factors.loc[regional_factors == 0].index.tolist() - ) - raise ValueError( - f"Load allocation method {GSw_LoadAllocationMethod} produced the " - "following regions with 0 load. Update GSw_LoadAllocationMethod " - "in your cases file:\n{}\n" - .format('\n'.join(no_load_regions)) - ) - # Demand response data may not be populated for every state - if dr_data: - state_region_factors = state_region_factors.loc[state_region_factors.index.intersection(state_load_hourly.columns), :] - - # Multiply the hourly state load profiles by the state-to-region factors - regional_load_hourly = ( - state_load_hourly[state_region_factors.index] - .dot(state_region_factors) - ) - - return regional_load_hourly - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== -def main(reeds_path, inputs_case): - print('Starting hourly_load.py') - - #%%### Load inputs - ### Load the input parameters - sw = reeds.io.get_switches(inputs_case) - weather_years = sw.resource_adequacy_years_list - scalars = reeds.io.get_scalars(inputs_case) - solveyears = reeds.io.get_years(os.path.dirname(inputs_case)) - hierarchy = reeds.io.get_hierarchy(os.path.dirname(inputs_case)) - - #%%%######################################### - # -- Get load profiles -- # - ############################################# - - state_load_hourly = reeds.io.get_load_hourly(inputs_case) - state_load_hourly = downselect_to_weather_years( - state_load_hourly, - weather_years - ) - historical_state_load_annual = reeds.io.get_historical_state_load_annual() - - match sw.GSw_LoadProfiles: - case _ if ( - sw.GSw_LoadProfiles.startswith('EER') - or Path(sw.GSw_LoadProfiles).is_file() - ): - endyear = int(sw.endyear) - state_load_hourly = interpolate_missing_model_years( - state_load_hourly, - endyear - ) - state_load_hourly = ( - calibrate_hourly_state_load_to_historical_annuals( - state_load_hourly, - historical_state_load_annual - ) - ) - historical_state_load_hourly = reeds.io.get_load_hourly( - GSw_LoadProfiles='historic' - ) - historical_state_load_hourly = downselect_to_weather_years( - historical_state_load_hourly, - weather_years - ) - state_load_hourly = prepend_historical_hourly_state_load( - state_load_hourly, - historical_state_load_hourly, - historical_state_load_annual - ) - state_load_hourly = downselect_to_model_years( - state_load_hourly, - solveyears - ) - case 'historic': - state_load_hourly = ( - apply_load_growth_factors_to_historical_state_load( - state_load_hourly, - historical_state_load_annual, - inputs_case, - solveyears - ) - ) - case _: - state_load_hourly = downselect_to_model_years( - state_load_hourly, - solveyears - ) - if len(state_load_hourly.unstack('year')) == 8760: - state_load_hourly = duplicate_weather_years( - state_load_hourly, - weather_years - ) - - regional_load_hourly = reaggregate_to_model_regions( - state_load_hourly, - inputs_case, - sw.GSw_LoadAllocationMethod - ) - - #%%%######################################### - # -- Performing Load Modifications -- # - ############################################# - - regional_load_hourly = apply_distribution_loss_factor( - regional_load_hourly, - scalars['distloss'] - ) - regional_load_hourly = regional_load_hourly.astype(np.float32) - - #%%%######################################### - # -- Peak Load Calculation -- # - ############################################# - - peakload = calculate_peak_load(regional_load_hourly, hierarchy) - - #%%%######################################### - # -- DR Shed Load Modifications -- # - ############################################# - - if int(sw.GSw_DRShed): - state_dr_shed_hourly = reeds.io.read_file(os.path.join(inputs_case, 'dr_shed_hourly.h5')) - dr_types = list({x.split('|')[0] for x in state_dr_shed_hourly.columns[1:]}) - - # Reformat to match state load profiles - state_dr_shed_hourly = state_dr_shed_hourly.reset_index().set_index(['year','datetime']) - regional_dr_shed_hourly = {} - for dr_type in dr_types: - type_cols = [col for col in state_dr_shed_hourly.columns if col.startswith(dr_type)] - reg_shed = state_dr_shed_hourly[type_cols].copy() - reg_shed.columns = [col.split('|')[1] for col in reg_shed.columns] - reg_shed = reaggregate_to_model_regions( - reg_shed, - inputs_case, - 'state_lpf', - dr_data=True - ) - # Add back dr type to column header - reg_shed.columns = [f"{dr_type}|{col}" for col in reg_shed.columns] - reg_shed = reg_shed.reset_index() - if isinstance(reg_shed['datetime'].iloc[0], bytes): - reg_shed['datetime'] = reg_shed['datetime'].str.decode('utf-8') - reg_shed['datetime'] = pd.to_datetime(reg_shed['datetime']) - reg_shed = reg_shed.set_index(['year','datetime']) - regional_dr_shed_hourly[dr_type] = reg_shed - - # Combined dr shed types - regional_dr_shed_hourly = pd.concat(regional_dr_shed_hourly.values(), axis=1) - regional_dr_shed_hourly = regional_dr_shed_hourly.astype(np.float32) - regional_dr_shed_hourly = regional_dr_shed_hourly.reset_index().set_index(['datetime']) - - #%%########################### - # -- Data Write-Out -- # - ############################## - - reeds.io.write_profile_to_h5(regional_load_hourly, 'load.h5', inputs_case) - peakload.to_csv(os.path.join(inputs_case,'peakload.csv')) - ### Write peak demand by NERC region to use in firm net import constraint - ( - peakload.loc['nercr'] - .stack('year') - .rename_axis(['*nercr','t']) - .rename('MW') - .to_csv(os.path.join(inputs_case,'peakload_nercr.csv')) - ) - if int(sw.GSw_DRShed): - reeds.io.write_profile_to_h5(regional_dr_shed_hourly, 'dr_shed_hourly.h5', inputs_case) - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if __name__ == '__main__': - # Time the operation of this script - tic = datetime.datetime.now() - - ### Parse arguments - parser = argparse.ArgumentParser( - description='Create run-specific hourly profiles', - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument('reeds_path', help='ReEDS-2.0 directory') - parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - #%% Run it - main(reeds_path=reeds_path, inputs_case=inputs_case) - - reeds.log.toc(tic=tic, year=0, process='input_processing/hourly_load.py', - path=os.path.join(inputs_case,'..')) - - print('Finished hourly_load.py') diff --git a/input_processing/hourly_plots.py b/input_processing/hourly_plots.py deleted file mode 100644 index a11987aa..00000000 --- a/input_processing/hourly_plots.py +++ /dev/null @@ -1,719 +0,0 @@ -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== -import os -import sys -import logging -import pandas as pd -import numpy as np -import h5py -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib import patheffects as pe -import cmocean - -import hourly_repperiods -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -from reeds import plots -plots.plotparams() - -## Turn off logging for imported packages -for i in ['matplotlib']: - logging.getLogger(i).setLevel(logging.CRITICAL) - -#%%################# -### FIXED INPUTS ### -interactive = False - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -def plot_unclustered_periods(profiles, sw, reeds_path, figpath): - """ - """ - ### Overlapping days - for label, dfin in [ - # ('unscaled',profiles), - ('scaled',profiles), - ]: - properties = dfin.columns.get_level_values('property').unique() - nhours = len(dfin.columns.get_level_values('region').unique())*(24 if sw['GSw_HourlyType']=='day' else 120) - plt.close() - f,ax = plt.subplots(1,len(properties),sharex=True,figsize=(nhours/12, 3.75)) - for col, prop in enumerate(properties): - dfin[prop].T.reset_index(drop=True).plot( - ax=ax[col], lw=0.2, legend=False) - dfin[prop].mean(axis=0).reset_index(drop=True).plot( - ax=ax[col], lw=1.5, color='k', legend=False) - ax[col].set_title(prop) - for x in np.arange(0,nhours+1,(24 if sw['GSw_HourlyType']=='day' else 120)): - ax[col].axvline(x,c='k',ls=':',lw=0.3) - ax[col].tick_params(labelsize=9) - ax[col].set_ylim(0) - ### Formatting - title = ' | '.join( - profiles.columns.get_level_values('region').drop_duplicates().tolist()) - ax[0].annotate(title,(0,1.12), xycoords='axes fraction', fontsize='large',) - ax[0].xaxis.set_major_locator(mpl.ticker.MultipleLocator(12)) - ax[0].xaxis.set_minor_locator(mpl.ticker.MultipleLocator(6)) - ax[0].set_xlim(0, nhours) - plots.despine(ax) - plt.savefig(os.path.join(figpath,'inputs_profiles-day_hourly-{}.png'.format(label))) - if interactive: - plt.show() - plt.close() - - ### Sequential days - properties = profiles.columns.get_level_values('property').unique() - regions = profiles.columns.get_level_values('region').unique() - rows = [(p,r) for p in properties for r in regions] - colors = {'wind-ons':'C0', 'upv':'C1', 'load':'C2', 'wind-ofs':'C4'} - - for label in ['hourly', 'daily']: - plt.close() - f,ax = plt.subplots(len(rows),1,figsize=(12,len(rows)*0.5),sharex=True,sharey=True) - for row, (p,r) in enumerate(rows): - if label == 'hourly': - df = profiles[p][r].stack('h_of_period') - else: - df = profiles[p][r].mean(axis=1) - ax[row].fill_between(range(len(df)), df.values, lw=0, color=colors.get(p,'k')) - ax[row].set_ylabel(f'{p}\n{r}', ha='right', va='center', rotation=0,color=colors.get(p,'k')) - ax[0].set_ylim(0,1) - plots.despine(ax) - plt.savefig(os.path.join(figpath,f'inputs_profiles-year_{label}.png')) - if interactive: - plt.show() - plt.close() - - -def plot_feature_scatter(profiles_fitperiods, reeds_path, figpath): - """ - """ - ### Settings - colors = plots.rainbowmapper(profiles_fitperiods.columns.get_level_values('region').unique()) - props = ['load','upv','wind-ons'] - ### Plot it - plt.close() - f,ax = plt.subplots(3,3,figsize=(7,7),sharex='col',sharey='row') - for row, yax in enumerate(props): - for col, xax in enumerate(props): - for region, c in colors.items(): - ax[row,col].plot( - profiles_fitperiods[xax][region].values, - profiles_fitperiods[yax][region].values, - c=c, lw=0, markeredgewidth=0, ms=5, alpha=0.5, marker='o', - label=(region if (row,col)==(1,2) else '_nolabel'), - ) - ### Formatting - ax[1,-1].legend( - loc='center left', bbox_to_anchor=(1,0.5), frameon=False, fontsize='large', - handletextpad=0.3,handlelength=0.7, - ) - for i, prop in enumerate(props): - ax[-1,i].set_xlabel(prop) - ax[i,0].set_ylabel(prop) - - plots.despine(ax) - plt.savefig(os.path.join(figpath,'inputs_feature_scatter.png')) - if interactive: - plt.show() - plt.close() - - -def plot_ldc( - period_szn, profiles, rep_periods, - forceperiods_write, sw, reeds_path, figpath): - """ - """ - if isinstance(sw.GSw_HourlyWeatherYears, str): - GSw_HourlyWeatherYears = [int(y) for y in sw.GSw_HourlyWeatherYears.split('_')] - else: - GSw_HourlyWeatherYears = sw.GSw_HourlyWeatherYears - ### Get clustered load, repeating representative periods based on how many - ### periods they represent - numperiods = period_szn.value_counts().rename('numperiods').to_frame() - numperiods['yearperiod'] = numperiods.index.map(hourly_repperiods.szn2yearperiod).values - numperiods['year'] = numperiods.index.map(hourly_repperiods.szn2yearperiod).map(lambda x: x[0]) - numperiods['yperiod'] = numperiods.index.map(hourly_repperiods.szn2period) - periods = [[row.yearperiod] * row.numperiods for (i,row) in numperiods.iterrows()] - periods = [item for sublist in periods for item in sublist] - - #### Hourly - hourly_in = ( - profiles - .stack('h_of_period') - .loc[GSw_HourlyWeatherYears] - ).copy() - hourly_out = hourly_in.unstack('h_of_period').loc[periods].stack('h_of_period') - - #### Daily - periodly_in = hourly_in.groupby('yperiod').mean() - ## Index doesn't matter; replace it so we can take daily mean - periodly_out = hourly_out.copy() - hourly_out.index = hourly_in.index.copy() - periodly_out = hourly_out.groupby('yperiod').mean() - - ### Get axis coordinates: properties = rows, regions = columns - properties = periodly_out.columns.get_level_values('property').unique().values - regions = periodly_out.columns.get_level_values('region').unique().values - nrows, ncols = len(properties), len(regions) - coords = {} - if ncols == 1: - coords = dict(zip( - [(prop, regions[0]) for prop in properties], - range(nrows))) - elif nrows == 1: - coords = dict(zip( - [(properties[0], region) for region in regions], - range(ncols))) - else: - coords = dict(zip( - [(prop, reg) for prop in properties for reg in regions], - [(row, col) for row in range(nrows) for col in range(ncols)], - )) - - ###### Plot it - for plotlabel, xlabel, dfin, dfout in [ - ('hourly','Hour',hourly_in,hourly_out), - ('periodly','Period',periodly_in,periodly_out), - ]: - plt.close() - f,ax = plt.subplots( - nrows,ncols,figsize=(len(regions)*1.2,9),sharex=True,sharey='row', - gridspec_kw={'hspace':0.1,'wspace':0.1}, - ) - for region in regions: - for prop in properties: - if region not in dfout[prop]: - continue - df = dfout[prop][region].sort_values(ascending=False) - ax[coords[prop,region]].plot( - range(len(dfout)), df.values, - label='Clustered', c='C1') - # label='Clustered', c='C7', lw=0.25) - # ax[coords[prop,region]].scatter( - # range(len(dfout)), df.values, - # c=df.index.map(yperiod2color), s=10, lw=0, - # ) - ax[coords[prop,region]].plot( - range(len(dfin)), - dfin[prop][region].sort_values(ascending=False).values, - ls=':', label='Original', c='k') - ### Formatting - for region in regions: - ax[coords[properties[0], region]].set_title(region) - for prop in properties: - ax[coords[prop, regions[0]]].set_ylabel(prop) - ax[coords[prop, regions[0]]].set_ylim(0) - ax[coords[properties[0], regions[0]]].annotate( - '{} periods: {} forced, {} clustered'.format( - sw['GSw_HourlyNumClusters'], len(forceperiods_write), - int(sw['GSw_HourlyNumClusters']) - len(forceperiods_write)), - (0,1.15), xycoords='axes fraction', fontsize='x-large', - ) - ax[coords[properties[0], regions[-1]]].legend( - loc='lower right', bbox_to_anchor=(1,1.1), ncol=2) - ax[coords[properties[-1], regions[0]]].set_xlabel( - '{} of year'.format(xlabel), x=0, ha='left') - plots.despine(ax) - plt.savefig(os.path.join(figpath,f'inputs_ldc-{plotlabel}.png')) - if interactive: - plt.show() - plt.close() - - -def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG:5070'): - """ - """ - ### Settings - cmaps = { - 'cf_actual':plt.cm.turbo, 'cf_rep':plt.cm.turbo, 'cf_diff':plt.cm.RdBu_r, - 'GW_actual':cmocean.cm.rain, 'GW_rep':cmocean.cm.rain, - 'GW_diff':plt.cm.RdBu_r, 'GW_frac':plt.cm.RdBu_r, 'pct_diff':plt.cm.RdBu_r, - } - vm = { - 'upv':{'cf_actual':(0,0.3),'cf_rep':(0,0.3),'cf_diff':(-0.05,0.05)}, - 'wind-ons':{'cf_actual':(0,0.6),'cf_rep':(0,0.6),'cf_diff':(-0.05,0.05)}, - 'wind-ofs':{'cf_actual':(0,0.6),'cf_rep':(0,0.6),'cf_diff':(-0.05,0.05)}, - } - vlimload = {'GW_diff':1, 'pct_diff':5} - title = ( - '{}\n' - 'Algorithm={}, NumClusters={}, RegionLevel={}' - ).format( - os.path.abspath(os.path.join(inputs_case,'..')), - sw['GSw_HourlyClusterAlgorithm'], - sw['GSw_HourlyNumClusters'], sw['GSw_HourlyClusterRegionLevel'], - ) - techs = ['upv', 'wind-ons', 'wind-ofs'] - colors = {'cf_actual':'k', 'cf_rep':'C1'} - lss = {'cf_actual':':', 'cf_rep':'-'} - zorders = {'cf_actual':10, 'cf_rep':9} - if isinstance(sw.GSw_HourlyWeatherYears, str): - GSw_HourlyWeatherYears = [int(y) for y in sw.GSw_HourlyWeatherYears.split('_')] - else: - GSw_HourlyWeatherYears = sw.GSw_HourlyWeatherYears - - hierarchy = reeds.io.get_hierarchy(os.path.abspath(os.path.join(inputs_case,'..'))) - dfmap = reeds.io.get_dfmap(os.path.abspath(os.path.join(inputs_case,'..'))) - for key, df in dfmap.items(): - dfmap[key] = df.to_crs(crs) - dfmap[key]['centroid_x'] = dfmap[key].centroid.x - dfmap[key]['centroid_y'] = dfmap[key].centroid.y - - ### Get the CF data over all years, take the mean over weather years - recf = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) - recf = recf.loc[recf.index.year.isin(GSw_HourlyWeatherYears)].mean() - - ### Get the hourly data - hours = pd.read_csv( - os.path.join(inputs_case, periodtype, 'numhours.csv') - ).rename(columns={'*h':'h'}).set_index('h').numhours - dfcf = pd.read_csv(os.path.join(inputs_case, periodtype, 'cf_vre.csv')).rename(columns={'*i':'i'}) - - for tech in techs: - ### Get supply curve - dfsc = pd.read_csv( - os.path.join(inputs_case, f'supplycurve_{tech}.csv') - ).rename(columns={'region':'r'}) - dfsc['i'] = tech + '_' + dfsc['class'].astype(str) - ### Add geographic and CF information - sitemap = reeds.io.get_sitemap(offshore=(True if tech == 'wind-ofs' else False)) - - dfsc['latitude'] = dfsc.sc_point_gid.map(sitemap.latitude) - dfsc['longitude'] = dfsc.sc_point_gid.map(sitemap.longitude) - dfsc = plots.df2gdf(dfsc, crs=crs) - dfsc['resource'] = dfsc.i + '|' + dfsc.r - dfsc['cf_actual'] = dfsc.resource.map(recf) - - ### Get the annual average CF of the hourly-processed data - cf_hourly = dfcf.loc[dfcf.i.str.startswith(tech)].pivot( - index=['i','r'],columns='h',values='cf') - cf_hourly = ( - (cf_hourly * cf_hourly.columns.map(hours)).sum(axis=1) / hours.sum() - ).rename('cf_rep').reset_index() - cf_hourly['resource'] = cf_hourly.i + '|' + cf_hourly.r - - ### Merge with supply curve, take the difference - cfmap = dfsc.assign( - cf_rep=dfsc.resource.map(cf_hourly.set_index('resource').cf_rep)).copy() - cfmap['cf_diff'] = cfmap.cf_rep - cfmap.cf_actual - - ### Calculate the difference at different resolutions - levels = ['r', 'st', 'transgrp', 'transreg', 'interconnect', 'country'] - dfdiffs = {} - for col in levels: - if col != 'r': - cfmap[col] = cfmap.r.map(hierarchy[col]) - dfdiffs[col] = dfmap[col].copy() - df = cfmap.copy() - for i in ['cf_actual','cf_rep']: - df['weighted'] = cfmap[i] * cfmap.capacity - dfdiffs[col][i] = ( - df.groupby(col).weighted.sum() / df.groupby(col).capacity.sum() - ) - dfdiffs[col]['cf_diff'] = dfdiffs[col].cf_rep - dfdiffs[col].cf_actual - - ## Convert from point to polygons (raster is 11.52 km but include a little extra) - cfmap.geometry = cfmap.buffer(11530/2, cap_style='square') - - ### Plot the difference map - nrows, ncols, coords = plots.get_coordinates([ - 'cf_actual', 'cf_rep', 'cf_diff', - 'r', 'st', 'transgrp', - 'transreg', 'interconnect', 'country', - ], aspect=1) - - plt.close() - f,ax = plt.subplots( - nrows, ncols, figsize=(14,9), gridspec_kw={'wspace':-0.05, 'hspace':0}, - ) - ## Absolute and site difference - for col in ['cf_actual','cf_rep','cf_diff']: - cfmap.plot( - ax=ax[coords[col]], column=col, cmap=cmaps[col], - lw=0, - legend=False, - vmin=vm[tech][col][0], vmax=vm[tech][col][1], - ) - dfmap['st'].plot(ax=ax[coords[col]], facecolor='none', edgecolor='k', lw=0.1, zorder=1e6) - ## Colorbar - plots.addcolorbarhist( - f=f, ax0=ax[coords[col]], data=cfmap[col]*100, nbins=51, - cmap=cmaps[col], histratio=1.5, - vmin=vm[tech][col][0]*100, vmax=vm[tech][col][1]*100, - cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, - ) - ## Regional differences - for level in levels: - dfdiffs[level].plot( - ax=ax[coords[level]], column='cf_diff', cmap=cmaps['cf_diff'], - vmin=vm[tech]['cf_diff'][0], vmax=vm[tech]['cf_diff'][1], - lw=0, legend=False, - ) - dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) - ## Text differences - for r, row in (dfdiffs[level].assign(val=dfdiffs[level].cf_diff.abs()).sort_values('val')).iterrows(): - decimals = 0 if abs(row.cf_diff) >= 1 else 1 - ax[coords[level]].annotate( - f"{row.cf_diff*100:+.{decimals}f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], - ) - ## Colorbar - plots.addcolorbarhist( - f=f, ax0=ax[coords[level]], data=dfdiffs[level].cf_diff*100, nbins=51, - cmap=cmaps['cf_diff'], histratio=1.5, - vmin=vm[tech]['cf_diff'][0]*100, vmax=vm[tech]['cf_diff'][1]*100, - cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, - ) - ## Formatting - ax[0,0].annotate(title+f', tech={tech}', (0.05,1.05), xycoords='axes fraction', fontsize=10) - for level in coords: - ax[coords[level]].set_title({'cf_diff':'site'}.get(level,level), y=0.9, weight='bold') - ax[coords[level]].axis('off') - savename = f"inputs_cfmap-{tech.replace('-','')}.png" - print(savename) - plt.savefig(os.path.join(figpath,savename)) - if interactive: - plt.show() - plt.close() - - #%% Plot the distribution of capacity factors - plt.close() - f,ax = plt.subplots() - for col in ['cf_actual','cf_rep']: - ax.plot( - np.linspace(0,100,len(cfmap)), - cfmap.sort_values('cf_actual', ascending=False)[col].values, - label=col.split('_')[1], - color=colors[col], ls=lss[col], zorder=zorders[col], - ) - ax.set_ylim(0) - ax.set_xlim(-1,101) - ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(10)) - ax.legend(fontsize='large', frameon=False) - ax.set_ylabel('{} capacity factor [.]'.format(tech)) - ax.set_xlabel('Percent of sites [%]') - ax.set_title( - '\n'.join(title.split('\n')[1:]).replace(' ','\n').replace(',',''), - x=0, ha='left', fontsize=10) - plots.despine(ax) - plt.savefig(os.path.join(figpath, f"inputs_cfmapdist-{tech.replace('-','')}.png")) - if interactive: - plt.show() - plt.close() - - #%%### Do it again for load - ### Get the full hourly data, take the mean for the cluster year and weather year(s) - with h5py.File(os.path.join(inputs_case, 'load.h5'), 'r') as f: - index_year = pd.Series(f['index_0']) - index_datetime = pd.to_datetime(pd.Series(f['index_1']).str.decode('utf-8')) - index = pd.MultiIndex.from_arrays( - [index_year, index_datetime], names=['year','timeindex']) - load_raw = pd.DataFrame( - columns=pd.Series(f['columns']).str.decode('utf-8'), - data=f['data'], index=index, - ) - loadyears = load_raw.index.get_level_values('year').unique() - keepyear = ( - int(sw['GSw_HourlyClusterYear']) if int(sw['GSw_HourlyClusterYear']) in loadyears - else max(loadyears)) - load_raw = load_raw.loc[keepyear].copy() - load_mean = load_raw.loc[ - load_raw.index.map(lambda x: x.year in GSw_HourlyWeatherYears) - ].mean() / 1000 - ## load.h5 is busbar load, but b_inputs.gms ingests end-use load, so scale down by distloss - scalars = reeds.io.get_scalars(inputs_case) - load_mean *= (1 - scalars['distloss']) - ### Get the representative data, take the mean for the cluster year - load_allyear = ( - pd.read_csv(os.path.join(inputs_case, periodtype, 'load_allyear.csv')).rename(columns={'*r':'r'}) - .set_index(['t','r','h']).MW.loc[keepyear] - .multiply(hours).groupby('r').sum() - / hours.sum() - ) / 1000 - ### Map it - dfplot = dfmap['r'].copy() - for level in [i for i in levels if i != 'r']: - dfplot[level] = dfplot.index.map(hierarchy[level]) - dfplot['GW_actual'] = load_mean - dfplot['GW_rep'] = load_allyear - - #%% Calculate the difference at different resolutions - dfdiffs = {} - for level in levels: - dfdiffs[level] = dfplot.groupby(level)[['GW_actual','GW_rep']].sum() - dfdiffs[level] = dfmap[level].merge(dfdiffs[level], left_index=True, right_index=True) - dfdiffs[level]['GW_diff'] = dfdiffs[level].GW_rep - dfdiffs[level].GW_actual - dfdiffs[level]['pct_diff'] = (dfdiffs[level].GW_rep / dfdiffs[level].GW_actual - 1) * 100 - - ### Plot the difference map - nrows, ncols, coords = plots.get_coordinates([ - 'GW_actual', 'GW_rep', 'None', - 'r', 'st', 'transgrp', - 'transreg', 'interconnect', 'country', - ], aspect=1) - labels = {'GW_diff':'[GW]', 'pct_diff':'[%]'} - - for val, label in labels.items(): - plt.close() - f,ax = plt.subplots( - nrows, ncols, figsize=(14,9), gridspec_kw={'wspace':-0.05, 'hspace':0}, - ) - ## Absolute and site difference - for col in ['GW_actual','GW_rep']: - dfplot.plot( - ax=ax[coords[col]], column=col, cmap=cmaps[col], - lw=0, - legend=False, - vmin=0, vmax=dfplot[col].max(), - ) - dfmap['st'].plot(ax=ax[coords[col]], facecolor='none', edgecolor='k', lw=0.1, zorder=1e6) - ## Colorbar - plots.addcolorbarhist( - f=f, ax0=ax[coords[col]], data=dfplot[col], nbins=51, - cmap=cmaps[col], histratio=1.5, - vmin=0., vmax=dfplot[col].max(), - cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, - ) - ## Regional differences - for level in levels: - dfdiffs[level].plot( - ax=ax[coords[level]], column=val, cmap=cmaps[val], - vmin=-vlimload[val], vmax=vlimload[val], - lw=0, legend=False, - ) - dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) - ## Text differences - for r, row in (dfdiffs[level].assign(val=dfdiffs[level][val].abs()).sort_values('val')).iterrows(): - decimals = 0 if abs(row[val]) >= 1 else 1 - ax[coords[level]].annotate( - f"{row[val]:+.{decimals}f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], - ) - ## Colorbar - plots.addcolorbarhist( - f=f, ax0=ax[coords[level]], data=dfdiffs[level][val], nbins=51, - cmap=cmaps[val], histratio=1.5, - vmin=-vlimload[val], vmax=vlimload[val], - cbarleft=0.95, cbarbottom=0.1, ticklabel_fontsize=7, - ) - ax[coords[level]].annotate( - label, (0.96,0.08), xycoords='axes fraction', ha='center', va='top', - weight='bold', fontsize=8, - ) - ## Formatting - ax[0,0].annotate(title+f', {val}', (0.05,1.05), xycoords='axes fraction', fontsize=10) - for level in coords: - ax[coords[level]].axis('off') - if level != 'None': - ax[coords[level]].set_title(level, y=0.9, weight='bold') - savename = f"inputs_loadmap-{val}.png" - print(savename) - plt.savefig(os.path.join(figpath,savename)) - if interactive: - plt.show() - plt.close() - - #%% Plot the distribution of load by region - colors = {'GW_actual':'k', 'GW_rep':'C1'} - lss = {'GW_actual':':', 'GW_rep':'-'} - zorders = {'GW_actual':10, 'GW_rep':9} - plt.close() - f,ax = plt.subplots() - for col in ['GW_actual','GW_rep']: - ax.plot( - range(1,len(dfplot)+1), - dfplot.sort_values('GW_actual', ascending=False)[col].values, - label='{} ({:.1f} GW mean)'.format(col.split('_')[1], dfplot[col].sum()), - color=colors[col], ls=lss[col], zorder=zorders[col], - ) - ax.set_ylim(0) - ax.set_xlim(0,len(dfplot)+1) - ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(10)) - ax.legend(fontsize='large', frameon=False) - ax.set_ylabel('Average load [GW]') - ax.set_xlabel('Number of BAs') - ax.set_title(title.replace(' ','\n').replace(',',''), x=0, ha='left', fontsize=10) - plots.despine(ax) - plt.savefig(os.path.join(figpath,'inputs_loadmapdist.png')) - if interactive: - plt.show() - plt.close() - - -def plot_8760(profiles, period_szn, sw, reeds_path, figpath): - def get_profiles(regions, year): - """Assemble 8760 profiles from original and representative days""" - timeindex = pd.date_range(f'{year}-01-01',f'{year+1}-01-01',freq='H',inclusive='left')[:8760] - props = profiles.columns.get_level_values('property').unique() - ### Original profiles - dforig = {} - for prop in props: - df = profiles[prop].loc[year].stack('h_of_period')[regions].sum(axis=1) - dforig[prop] = df / df.max() - dforig[prop].index = timeindex - dforig = pd.concat(dforig, axis=1) - - ### Representative profiles - periodmap = period_szn.map(hourly_repperiods.szn2yearperiod).to_frame() - periodmap['year'] = periodmap.szn.map(lambda x: x[0]) - periodmap['yperiod'] = periodmap.szn.map(lambda x: x[1]) - periodmap = periodmap.loc[periodmap.year==year].yperiod - - dfrep = {} - for prop in props: - df = ( - profiles[prop].loc[year].loc[periodmap.values] - .stack('h_of_period')[regions].sum(axis=1)) - dfrep[prop] = df / df.max() - dfrep[prop].index = timeindex - dfrep = pd.concat(dfrep, axis=1) - - return dforig, dfrep - - ###### All regions - if isinstance(sw.GSw_HourlyWeatherYears, str): - GSw_HourlyWeatherYears = [int(y) for y in sw.GSw_HourlyWeatherYears.split('_')] - else: - GSw_HourlyWeatherYears = sw.GSw_HourlyWeatherYears - for year in GSw_HourlyWeatherYears: - props = profiles.columns.get_level_values('property').unique() - regions = profiles.columns.get_level_values('region').unique() - dforig, dfrep = get_profiles(regions, year) - - ### Original vs representative - plt.close() - f,ax = plt.subplots(38,1,figsize=(12,16),sharex=True,sharey=True) - for i, prop in enumerate(props): - plots.plotyearbymonth( - dfrep[prop].rename('Representative').to_frame(), - style='line', colors=['C1'], ls='-', f=f, ax=ax[i*12+i:(i+1)*12+i]) - plots.plotyearbymonth( - dforig[prop].rename('Original').to_frame(), - style='line', colors=['k'], ls=':', f=f, ax=ax[i*12+i:(i+1)*12+i]) - for i in [12,25]: - ax[i].axis('off') - for i, prop in list(zip(range(len(props)), props)): - ax[i*12+i].set_title( - '{}: {}'.format(prop,' | '.join(regions)),x=0,ha='left',fontsize=12) - ax[0].legend(loc='lower left', bbox_to_anchor=(0,1.5), ncol=2, frameon=False) - plt.savefig(os.path.join(figpath,f'inputs_8760-allregions-{year}.png')) - if interactive: - plt.show() - plt.close() - - ### Load, wind, solar together; original - plt.close() - f,ax = plots.plotyearbymonth( - dforig[['wind-ons','upv']], colors=['#0064ff','#ff0000'], alpha=0.5) - plots.plotyearbymonth(dforig['load'], f=f, ax=ax, style='line', colors='k') - plt.savefig(os.path.join(figpath,f'inputs_8760-allregions-original-{year}.png')) - if interactive: - plt.show() - plt.close() - - ### Load, wind, solar together; representative - plt.close() - f,ax = plots.plotyearbymonth( - dfrep[['wind-ons','upv']], colors=['#0064ff','#ff0000'], alpha=0.5) - plots.plotyearbymonth(dfrep['load'], f=f, ax=ax, style='line', colors='k') - plt.savefig(os.path.join(figpath,f'inputs_8760-allregions-representative-{year}.png')) - if interactive: - plt.show() - plt.close() - - ###### Individual regions, original vs representative - for region in profiles.columns.get_level_values('region').unique(): - dforig, dfrep = get_profiles([region], year) - - plt.close() - f,ax = plt.subplots(38,1,figsize=(12,16),sharex=True,sharey=True) - for i, prop in enumerate(props): - plots.plotyearbymonth( - dfrep[prop].rename('Representative').to_frame(), - style='line', colors=['C1'], ls='-', f=f, ax=ax[i*12+i:(i+1)*12+i]) - plots.plotyearbymonth( - dforig[prop].rename('Original').to_frame(), - style='line', colors=['k'], ls=':', f=f, ax=ax[i*12+i:(i+1)*12+i]) - for i in [12,25]: - ax[i].axis('off') - for i, prop in list(zip(range(len(props)), props)): - ax[i*12+i].set_title('{}: {}'.format(prop,region),x=0,ha='left',fontsize=12) - ax[0].legend(loc='lower left', bbox_to_anchor=(0,1.5), ncol=2, frameon=False) - plt.savefig(os.path.join(figpath,f'inputs_8760-{region}-{year}.png')) - if interactive: - plt.show() - - -def plot_load_days(profiles, rep_periods, period_szn, sw, reeds_path, figpath): - """ - """ - ### Input processing - idx_reedsyr = period_szn.map(hourly_repperiods.szn2yearperiod) - medoid_profiles = profiles.loc[rep_periods] - centroids = profiles.loc[rep_periods] - centroid_profiles = centroids * profiles.stack('h_of_period').max() - - colors = plots.rainbowmapper(list(set(idx_reedsyr)), plt.cm.turbo) - - ### Plot all days on same x axis - ## Map days to axes - ncols = len(colors) - nrows = 1 - coords = dict(zip( - colors.keys(), - [col for col in range(ncols)] - )) - plt.close() - f,ax = plt.subplots( - nrows, ncols, figsize=(1.5*ncols,2.5*nrows), sharex=True, sharey=True) - for day in range(len(idx_reedsyr)): - szn = idx_reedsyr[day] - this_profile = profiles.load.iloc[day].groupby('h_of_period').sum() - ax[coords[szn]].plot( - range(len(this_profile)), this_profile.values/1e3, color=colors[szn], alpha=0.5) - ## add centroids and medoids to the plot: - for szn in colors: - ## centroids - only for clustered days, not force-included days - try: - ax[coords[szn]].plot( - range(len(this_profile)), - centroid_profiles['load'].loc[szn].groupby('h_of_period').sum().values/1e3, - color='k', alpha=1, linewidth=2, label='centroid', - ) - except IndexError: - pass - ## medoids - ax[coords[szn]].plot( - range(len(this_profile)), - medoid_profiles['load'].loc[szn].groupby('h_of_period').sum().values/1e3, - ls='--', color='0.7', alpha=1, linewidth=2, label='medoid', - ) - ## title - ax[coords[szn]].set_title( - '{}, {} days'.format(szn, idx_reedsyr.value_counts()[szn]), size=9) - - ax[0].legend(loc='upper left', frameon=False, fontsize='small') - ax[0].set_xlabel('Hour') - ax[0].set_ylabel('Conterminous\nUS Load [GW]', y=0, ha='left') - ax[0].xaxis.set_major_locator( - mpl.ticker.MultipleLocator(6 if sw['GSw_HourlyType']=='day' else 24)) - ax[0].xaxis.set_minor_locator( - mpl.ticker.MultipleLocator(3 if sw['GSw_HourlyType']=='day' else 12)) - ax[0].annotate( - 'Cluster Comparison (All Days of All Weather Years Shown)', - xy=(0,1.2), xycoords='axes fraction', ha='left', - ) - plots.despine(ax) - plt.savefig(os.path.join(figpath,'inputs_day_comparison_all.png')) - if interactive: - plt.show() - plt.close() diff --git a/input_processing/hourly_repperiods.py b/input_processing/hourly_repperiods.py deleted file mode 100644 index 2c57bcc8..00000000 --- a/input_processing/hourly_repperiods.py +++ /dev/null @@ -1,976 +0,0 @@ -""" -The purpose of this script is to collect 8760 data as it is output by -hourlize and perform a temporal aggregation to produce load and capacity -factor parameters for the representative days that will be read by ReEDS. -The other outputs are the hours/seasons to be modeled in ReEDS and linking -sets used in the model. - -General notes: -* h: a timeslice with an h prefix, starting at h1 -* hour: an hour of the full period, starting at 1 ([1-8760] for 1 year or [1-61320] for 7 years) -* dayhour: a clock hour starting at 1 [1-24] -* period: a day (if GSw_HourlyType=='day') or a wek (if GSw_HourlyType=='wek') -* wek: A consecutive 5-day period (365 is only divisible by 1, 5, 73, and 365) - -This script is currently not compatible with: -* Climate impacts (climateprep.py) -* Beyond-2050 modeling (forecast.py) -* Flexible demand -""" - -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== -import argparse -import json -import numpy as np -import os -import sys -import datetime -import pandas as pd -import scipy -import sklearn.cluster -import sklearn.neighbors -import traceback -import hourly_writetimeseries -import hourly_plots -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -## Time the operation of this script -tic = datetime.datetime.now() - - -#%%################# -### FIXED INPUTS ### - -decimals = 3 -### Whether to show plots interactively [default False] -interactive = False -### VRE techs considered for GSw_PRM_StressSeedMinRElevel and GSw_HourlyMinRElevel -techs_min_vre = ['upv', 'wind-ons'] - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -def szn2yearperiod(szn): - """ - szn's are formatted as 'y{20xx}{d or w}{day of year or wek of year}' - where a 'wek' is a 5-day period (5*73 = 365) - """ - year, period = szn.split('d') if 'd' in szn else szn.split('w') - return int(year.strip('y')), int(period) - - -def szn2period(szn): - """ - szn's are formatted as 'y{20xx}{d or w}{day of year or wek of year}' - where a 'wek' is a 5-day period (5*73 = 365) - """ - year, period = szn.split('d') if 'd' in szn else szn.split('w') - return int(period) - - -############################### -# -- Load Processing -- # -############################### - -def get_load(inputs_case, keep_modelyear=None, keep_weatheryears=[2012]): - """ - """ - ### Subset to modeled regions - load = reeds.io.read_file(os.path.join(inputs_case,'load.h5'), parse_timestamps=True) - ### Subset to keep_modelyear if provided - if keep_modelyear: - load = load.loc[keep_modelyear].copy() - ### load.h5 is busbar load, but b_inputs.gms ingests end-use load, so scale down by distloss - scalars = reeds.io.get_scalars(inputs_case) - load *= (1 - scalars['distloss']) - - ### Downselect to weather years if provided - if isinstance(keep_weatheryears, list): - load = load.loc[load.index.year.isin(keep_weatheryears)] - - return load - - -def optimize_period_weights(profiles_fitperiods, numclusters=100): - """ - The optimization approach (minimizing sum of absolute errors) is described at - https://optimization.mccormick.northwestern.edu/index.php/Optimization_with_absolute_values - The general idea of optimizing period weights to reproduce regional variability is similar - to the method used in the EPRI US-REGEN model, described at - https://www.epri.com/research/products/000000003002016601 - """ - ### Imports - import pulp - - ### Input processing - profiles_day = ( - profiles_fitperiods.groupby(['property','region'], axis=1).mean()) - profiles_mean = profiles_day.mean() - numdays = len(profiles_day) - days = profiles_day.index.values - - ### Optimization: minimize sum of absolute errors - m = pulp.LpProblem('LinearDaySelection', pulp.LpMinimize) - ###### Variables - ### day weights - WEIGHT = pulp.LpVariable.dicts('WEIGHT', (d for d in days), lowBound=0, cat='Continuous') - ### errors - ERROR_POS = pulp.LpVariable.dicts( - 'ERROR_POS', (c for c in profiles_day.columns), lowBound=0, cat='Continuous') - ERROR_NEG = pulp.LpVariable.dicts( - 'ERROR_NEG', (c for c in profiles_day.columns), lowBound=0, cat='Continuous') - ###### Constraints - ### weights must sum to 1 - m += pulp.lpSum([WEIGHT[d] for d in days]) == 1 - ### definition of errors - for c in profiles_day.columns: - m += ( - ### Full error for column (given by positive component minus negative component)... - ERROR_POS[c] - ERROR_NEG[c] - ### ...plus sum of values for weighted representative days... - + pulp.lpSum([WEIGHT[d] * profiles_day[c][d] for d in days]) - ### ...equals the mean for that column - == profiles_mean[c]) - ###### Objective: minimize the sum of absolute values of errors across all columns - m += pulp.lpSum([ - ERROR_POS[c] + ERROR_NEG[c] - for c in profiles_day.columns - ]) - - ### Solve it - m.solve(solver=pulp.PULP_CBC_CMD(msg=True)) - - ### Collect weights, scaled by total number of days - weights = pd.Series({d:WEIGHT[d].varValue for d in days}) * numdays - - ### Truncate based on numclusters, scale appropriately, and convert to integers - ### Keep the the 'numclusters' highest-weighted days - rweights = (weights.sort_values(ascending=False)[:numclusters]) - ### Scale so that the weights sum to numdays (have to do if numclusters is small) - rweights *= numdays / rweights.sum() - ### Convert to integers - iweights = rweights.round(0).astype(int) - ### Scale all weights little by little until they sum to number of actual days - sumweights = iweights.sum() - diffweights = sumweights - numdays - increment = 0.00001 * (1 if diffweights < 0 else -1) - for i in range(1000000): - iweights = (rweights * (1 + increment*i)).round(0).astype(int) - if iweights.sum() == numdays: - break - - iweights = iweights.replace(0,np.nan).dropna().astype(int) - ### Make sure it worked - if iweights.sum() != numdays: - raise ValueError(f'Sum of rounded weights = {iweights.sum()} != {numdays}') - - return profiles_day, iweights, weights - - -def assign_representative_days(profiles_day, rweights): - """ - """ - ### Imports - import pulp - - ### Input processing - actualdays = profiles_day.index.values - repdays = list(rweights.index) - - ### Optimization: minimize sum of absolute errors - m = pulp.LpProblem('RepDayAssignment', pulp.LpMinimize) - ###### Variables - ### Weighting of rep days (r) for each actual day (a). - ### Can only use whole days, so it's a binary variable. - WEIGHT = pulp.LpVariable.dicts( - 'WEIGHT', ((a,r) for a in actualdays for r in repdays), - lowBound=0, upBound=1, cat=pulp.LpInteger) - ### Errors. These are defined for features (c) and for actual days (a). - ERROR_POS = pulp.LpVariable.dicts( - 'ERROR_POS', ((a,c) for a in actualdays for c in profiles_day.columns), - lowBound=0, cat='Continuous') - ERROR_NEG = pulp.LpVariable.dicts( - 'ERROR_NEG', ((a,c) for a in actualdays for c in profiles_day.columns), - lowBound=0, cat='Continuous') - ###### Constraints - ### Each actual day can only be assigned to one representative day - for a in actualdays: - m += pulp.lpSum([WEIGHT[a,r] for r in repdays]) == 1 - ### Each representative day must be used a number of times equal to its weight - for r in repdays: - m += pulp.lpSum([WEIGHT[a,r] for a in actualdays]) == rweights[r] - ### Define the error variables - for a in actualdays: - for c in profiles_day.columns: - m += ( - ### Full error for column on actual day (given by positive - ### component minus negative component)... - ERROR_POS[a,c] - ERROR_NEG[a,c] - ### ...plus value for its representative day (since WEIGHT is binary)... - + pulp.lpSum([WEIGHT[a,r] * profiles_day[c][r] for r in repdays]) - ### ...equals the actual value for that column and day - == profiles_day[c][a]) - ###### Objective: minimize the sum of absolute values of errors - m += pulp.lpSum([ - ERROR_POS[a,c] + ERROR_NEG[a,c] - for a in actualdays for c in profiles_day.columns - ]) - - ### Solve it - m.solve(solver=pulp.PULP_CBC_CMD(msg=True)) - - ### Collect assignments - assignments = pd.Series( - {(a,r):WEIGHT[a,r].varValue for a in actualdays for r in repdays}).astype(int) - assignments.index = assignments.index.rename(['act','rep']) - a2r = assignments.replace(0,np.nan).dropna().reset_index(level='rep').rep - - return a2r - - -def identify_peak_containing_periods(df, hierarchy, level): - """ - Identify the period containing the peak value. - Set of (region,reason,year,yperiod), with yperiod starting from 1. - """ - ### Map columns to level, then sum - if level == 'r': - rmap = pd.Series(hierarchy.index, index=hierarchy.index) - else: - rmap = hierarchy[level] - dfmod = df.copy() - dfmod.columns = dfmod.columns.map(lambda x: x.split('|')[-1]).map(rmap) - dfmod = dfmod.groupby(axis=1, level=0).sum() - ### Get the max value by (year,yperiod) - dfmax = dfmod.groupby(['year','yperiod']).max() - ### Get the max (year,yperiod) for each column - forceperiods = set([(c, 'peak-containing', *dfmax[c].nlargest(1).index[0]) for c in dfmax]) - - return forceperiods - - -def identify_min_periods(df, hierarchy, level, prefix=''): - """ - Identify the period with the minimum average value. - Set of (region,reason,year,yperiod), with yperiod starting from 1. - """ - ### Map columns to level, then sum - if level == 'r': - rmap = pd.Series(hierarchy.index, index=hierarchy.index) - else: - rmap = hierarchy[level] - dfmod = df[[c for c in df if c.startswith(prefix)]].copy() - dfmod.columns = dfmod.columns.map(lambda x: x.split('|')[-1]).map(rmap) - dfmod = dfmod.groupby(axis=1, level=0).sum() - ### Get the mean value by (year,yperiod) - dfmean = dfmod.groupby(['year','yperiod']).mean() - ### Get the min (year,yperiod) for each column - forceperiods = set([(c, 'min average', *dfmean[c].nsmallest(1).index[0]) for c in dfmean]) - - return forceperiods - - - -########################### -# -- Clustering -- # -########################### - -def cluster_profiles(profiles_fitperiods, sw, forceperiods_yearperiod): - """ - Cluster the load and (optionally) RE profiles to find representative days for dispatch in ReEDS. - - Args: - GSw_HourlyClusterRegionLevel: Level of inputs/hierarchy.csv at which to aggregate - profiles for clustering. VRE profiles are converted to available-capacity-weighted - averages. That's not the best - it would be better to weight sites that are more likely - to be developed more strongly - but it's better than not weighting at all. - - Returns: - cf_representative - hourly profile of centroid or medoid capacity factor values - for all regions and technologies - load_representative - hourly profile of centroid or medoid load values for all regions - period_szn - day indices of each cluster center - """ - ###### Run the clustering - print(f"Performing {sw.GSw_HourlyClusterAlgorithm} clustering") - if ( - sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical') - or sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kme') - ): - if sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical'): - args = sw['GSw_HourlyClusterAlgorithm'].split('_') - if len(args) > 1: - metric = args[1] - linkage = args[2] - else: - metric = 'euclidean' - linkage = 'ward' - clusters = sklearn.cluster.AgglomerativeClustering( - n_clusters=int(sw['GSw_HourlyNumClusters']), - metric=metric, linkage=linkage, - ) - elif sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmeans'): - clusters = sklearn.cluster.KMeans( - n_clusters=int(sw['GSw_HourlyNumClusters']), - random_state=0, n_init='auto', max_iter=1000, - ) - elif sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): - import sklearn_extra.cluster - args = sw['GSw_HourlyClusterAlgorithm'].split('_') - if len(args) > 1: - metric = args[1] - init = args[2] - else: - metric = 'euclidean' - init = 'heuristic' - clusters = sklearn_extra.cluster.KMedoids( - n_clusters=int(sw['GSw_HourlyNumClusters']), - metric=metric, init=init, method='pam', - max_iter=1000, random_state=0, - ) - ### Generate the fits - idx = clusters.fit_predict(profiles_fitperiods) - ### Get nearest period to each centroid - centroids = pd.DataFrame( - sklearn.neighbors.NearestCentroid().fit(profiles_fitperiods, idx).centroids_, - columns=profiles_fitperiods.columns, - ) - nearest_period = { - i: - profiles_fitperiods.loc[:,idx==i,:].apply( - lambda row: scipy.spatial.distance.euclidean(row, centroids.loc[i]), - axis=1 - ).nsmallest(1).index[0] - for i in range(int(sw['GSw_HourlyNumClusters'])) - } - - period_szn = pd.DataFrame({ - 'period': profiles_fitperiods.index.values, - 'szn': [f"y{i[0]}{sw['GSw_HourlyType'][0]}{i[1]:>03}" - for i in pd.Series(idx).map(nearest_period)] - ### Add the force-include periods to the end of the list of seasons - }) - period_szn = pd.concat([ - period_szn, - pd.DataFrame({ - 'period': list(forceperiods_yearperiod), - 'szn': [f"y{i[0]}{sw['GSw_HourlyType'][0]}{i[1]:>03}" - for i in forceperiods_yearperiod] - }) - ]).sort_values('period').set_index('period').szn - - elif sw['GSw_HourlyClusterAlgorithm'] in ['opt','optimized','optimize']: - ### Optimize the weights of representative days - profiles_day, rweights, weights = optimize_period_weights( - profiles_fitperiods=profiles_fitperiods, numclusters=int(sw['GSw_HourlyNumClusters'])) - ### Optimize the assignment of actual days to representative days - a2r = assign_representative_days(profiles_day=profiles_day.round(4), rweights=rweights) - - if len(rweights) < int(sw['GSw_HourlyNumClusters']): - print( - 'Asked for {} representative periods but only needed {}'.format( - sw['GSw_HourlyNumClusters'], len(rweights))) - - period_szn = pd.concat([ - a2r.reset_index().rename(columns={'act':'period','rep':'szn'}), - pd.DataFrame({'period':list(forceperiods_yearperiod), - 'szn':list(forceperiods_yearperiod)}) - if len(forceperiods_yearperiod) else None - ]).sort_values('period').set_index('period').szn - period_szn = period_szn.map(lambda x: f'y{x[0]}{sw.GSw_HourlyType[0]}{x[1]:>03}') - - elif 'user' in sw['GSw_HourlyClusterAlgorithm'].lower(): - print('Using user-defined representative period weights') - period_szn = pd.read_csv( - os.path.join(inputs_case,'period_szn_user.csv') - ).set_index('actual_period').rep_period.rename('szn') - period_szn.index = period_szn.index.map(szn2yearperiod).values - period_szn.index = period_szn.index.rename('period') - - - ### Get the list of representative periods for convenience - rep_periods = sorted(period_szn.map(szn2yearperiod).unique()) - - return rep_periods, period_szn - - -def make_timestamps(sw): - ### Get some useful constants - hoursperperiod = {'day':24, 'wek':120, 'year':24} - periodsperyear = {'day':365, 'wek':73, 'year':365} - weather_years = sw.resource_adequacy_years_list - - ### Get map from yperiod, hour, and h_of_period to timestamp - timestamps = pd.DataFrame({ - 'year': np.ravel([[y]*8760 for y in weather_years]), - 'h_of_year': np.ravel([list(range(1,8761)) * len(weather_years)]), - 'h_of_period': np.ravel( - [f'{h+1:>03}' for h in range(hoursperperiod[sw['GSw_HourlyType']])] - * periodsperyear[sw['GSw_HourlyType']] * len(weather_years)), - 'yperiod': np.ravel( - [p+1 for p in range(periodsperyear[sw['GSw_HourlyType']]) - for h in range(hoursperperiod[sw['GSw_HourlyType']])] - * len(weather_years)), - 'h_of_day': np.ravel( - [f'{h+1:>03}' for h in range(hoursperperiod['day'])] - * periodsperyear['day'] * len(weather_years)), - 'yday': np.ravel( - [p+1 for p in range(periodsperyear['day']) - for h in range(hoursperperiod['day'])] - * len(weather_years)), - 'h_of_wek': np.ravel( - [f'{h+1:>03}' for h in range(hoursperperiod['wek'])] - * periodsperyear['wek'] * len(weather_years)), - 'ywek': np.ravel( - [p+1 for p in range(periodsperyear['wek']) - for h in range(hoursperperiod['wek'])] - * len(weather_years)), - }) - timestamps['timestamp'] = ( - 'y' + timestamps.year.astype(str) - ## d for day and w for wek - + ('w' if sw.GSw_HourlyType == 'wek' else 'd') - + timestamps.yperiod.astype(str).map('{:>03}'.format) - + 'h' + timestamps.h_of_period - ) - timestamps['period'] = timestamps['timestamp'].map(lambda x: x.split('h')[0]) - timestamps['day'] = ( - 'y' + timestamps.year.astype(str) - + 'd' + timestamps.yday.astype(str).map('{:>03}'.format) - ) - timestamps['wek'] = ( - 'y' + timestamps.year.astype(str) - + 'w' + timestamps.ywek.astype(str).map('{:>03}'.format) - ) - timestamps.index = np.ravel([ - pd.date_range( - f'{y}-01-01', f'{y+1}-01-01', - freq='H', inclusive='left', tz='Etc/GMT+6', - )[:8760] - for y in weather_years - ]) - - return timestamps - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== - -def main( - sw, - reeds_path, - inputs_case, - periodtype='rep', - minimal=0, -): - """ - """ - #%% Parse inputs if necessary - if not isinstance(sw['GSw_HourlyClusterWeights'], pd.Series): - sw['GSw_HourlyClusterWeights'] = pd.Series(json.loads( - '{"' - + (':'.join(','.join(sw['GSw_HourlyClusterWeights'].split('/')).split('_')) - .replace(':','":').replace(',',',"')) - +'}' - )) - sw['GSw_HourlyClusterWeights'].index = sw['GSw_HourlyClusterWeights'].index.rename('property') - sw['GSw_HourlyClusterWeights'] = ( - sw['GSw_HourlyClusterWeights'].loc[sw['GSw_HourlyClusterWeights'] != 0] - ).copy() - if not isinstance(sw['GSw_HourlyWeatherYears'], list): - sw['GSw_HourlyWeatherYears'] = [int(y) for y in sw['GSw_HourlyWeatherYears'].split('_')] - if not isinstance(sw['GSw_CSP_Types'], list): - sw['GSw_CSP_Types'] = [int(i) for i in sw['GSw_CSP_Types'].split('_')] - ## VRE techs that can be used for profiles - techs_vre = ['upv', 'wind-ons', 'wind-ofs'] if int(sw.GSw_OfsWind) else ['upv', 'wind-ons'] - - #%% Direct plots to outputs folder - figpath = os.path.join(inputs_case,'..', 'outputs', 'figures') - os.makedirs(figpath, exist_ok=True) - os.makedirs(os.path.join(inputs_case, periodtype), exist_ok=True) - - val_r_all = pd.read_csv( - os.path.join(inputs_case, 'val_r_all.csv'), header=None).squeeze(1).tolist() - modelyears = pd.read_csv( - os.path.join(inputs_case, 'modeledyears.csv')).columns.astype(int) - # Use agglevel_variables function to obtain spatial resolution variables - agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) - - #%% Get map from yperiod, hour, and h_of_period to timestamp - timestamps = make_timestamps(sw) - timestamps_myr = timestamps.loc[timestamps.year.isin(sw['GSw_HourlyWeatherYears'])].copy() - - ### Get region hierarchy for use with GSw_HourlyClusterRegionLevel - hierarchy = pd.read_csv( - os.path.join(inputs_case,'hierarchy.csv')).rename(columns={'*r':'r'}).set_index('r') - hierarchy_orig = pd.read_csv( - os.path.join(inputs_case,'hierarchy_original.csv')) - - if sw.GSw_HourlyClusterRegionLevel == 'r': - rmap = pd.Series(hierarchy_orig.index, index=hierarchy_orig.index) - elif agglevel_variables['agglevel'] == 'county' or 'county' in agglevel_variables['agglevel']: - rmap = hierarchy[sw['GSw_HourlyClusterRegionLevel']] - elif agglevel_variables['agglevel'] in ['ba','aggreg']: - rmap = (hierarchy_orig.loc[hierarchy_orig['ba'].isin(val_r_all)] - [['aggreg',sw['GSw_HourlyClusterRegionLevel']]] - .drop_duplicates().set_index('aggreg')).squeeze(1) - - #%% Load supply curves to use for available capacity weighting - sc = { - tech: pd.read_csv( - os.path.join(inputs_case, f'supplycurve_{tech}.csv') - ).groupby(['region','class'], as_index=False).capacity.sum() - for tech in techs_vre - } - sc = ( - pd.concat(sc, names=['tech','drop'], axis=0) - .reset_index(level='drop', drop=True).reset_index()) - ### Downselect to modeled regions - sc = sc.loc[sc.region.isin(val_r_all)].copy() - sc['i'] = sc.tech+'_'+sc['class'].astype(str) - sc['resource'] = sc.i + '|' + sc.region - sc['aggreg'] = sc.region.map(rmap) - - #%%### Load RE CF data, then take available-capacity-weighted average by (tech,region) - print("Collecting 8760 capacity factor data") - recf_ra = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) - ### Downselect to techs used for rep-period selection - recf_ra = recf_ra[[c for c in recf_ra if any([c.startswith(p) for p in techs_vre])]].copy() - ### Multiply by available capacity for weighted average - recf_ra *= sc.set_index('resource')['capacity'] - ### Downselect to modeled years, add descriptive time index - recf = recf_ra.loc[recf_ra.index.year.isin(sw['GSw_HourlyWeatherYears'])] - recf.index = timestamps_myr.set_index(['year','yperiod','h_of_period']).index - recf_ra.index = timestamps.set_index(['year','yperiod','h_of_period']).index - - ### Identify outlying periods if using capacity credit instead of stress periods - if (int(sw.GSw_PRM_CapCredit) - and (sw['GSw_HourlyMinRElevel'].lower() not in ['false','none'])): - forceperiods_minre = { - tech: identify_min_periods( - df=recf, hierarchy=hierarchy, - level=sw['GSw_HourlyMinRElevel'], prefix=tech) - for tech in techs_min_vre - } - else: - forceperiods_minre = {tech: set() for tech in techs_min_vre} - - ### Aggregate to (tech,GSw_HourlyClusterRegionLevel) - recf_agg = recf.copy() - tmp = ( - pd.DataFrame({'resource':recf.columns}).set_index('resource') - .merge(sc.set_index('resource')[['tech','region']], left_index=True, right_index=True) - ) - columns = tmp.loc[tmp.index.isin(recf.columns)] - recf_agg = recf_agg[tmp.index] - columns['region'] = columns.region.map(rmap) - recf_agg.columns = pd.MultiIndex.from_frame(columns[['tech','region']]) - recf_agg = recf_agg.groupby(axis=1, level=['tech','region']).sum() - - ### Divide by aggregated capacity to get back to CF - recf_agg /= sc.groupby(['tech','aggreg']).capacity.sum().rename_axis(['tech','region']) - - ### Load load data (Eastern time) - print("Collecting 8760 load data") - load = get_load( - inputs_case=inputs_case, - keep_modelyear=(int(sw['GSw_HourlyClusterYear']) - if int(sw['GSw_HourlyClusterYear']) in modelyears - else max(modelyears)), - keep_weatheryears=sw.GSw_HourlyWeatherYears, - ) - ## Add descriptive index - load.index = timestamps_myr.set_index(['year','yperiod','h_of_period']).index - - ### Identify outlying periods if using capacity credit instead of stress periods - if (int(sw.GSw_PRM_CapCredit) - and (sw['GSw_HourlyPeakLevel'].lower() not in ['false','none']) - ): - forceperiods_load = identify_peak_containing_periods( - df=load, hierarchy=hierarchy, level=sw['GSw_HourlyPeakLevel']) - else: - forceperiods_load = set() - - ### Aggregate to GSw_HourlyClusterRegionLevel - load_agg = load.copy() - load_agg.columns = load_agg.columns.map(rmap) - load_agg = load_agg.groupby(axis=1, level=0).sum() - match sw.GSw_HourlyClusterLoadNorm: - case 'none': - ## Don't normalize load - pass - case 'regionmax': - ## Normalize each region to [0,1] - load_agg /= load_agg.max() - case 'maxmax': - ## Divide each region by largest regional max across all regions - load_agg /= load_agg.max().max() - case 'maxmin': - ## Divide each region by smallest regional max across all regions - load_agg /= load_agg.max().min() - case _: - ## Like 'maxmin' but scaled by the provided numeric value - load_agg /= load_agg.max().min() * float(sw.GSw_HourlyClusterLoadNorm) - - ### Get the full list of forced periods - forceperiods = forceperiods_load.copy() - for tech in forceperiods_minre: - forceperiods.update(forceperiods_minre[tech]) - ## Make a simpler list without the metadata to use for indexing below - ## (use list(set()) to drop duplicate force-periods) - forceperiods_yearperiod = list(set([(i[2], i[3]) for i in forceperiods])) - ### Add number of force-include periods to GSw_HourlyNumClusters for total number of periods - num_rep_periods = int(sw['GSw_HourlyNumClusters']) + len(forceperiods) - ### Record the force-included periods - print('representative periods: {}'.format(sw['GSw_HourlyNumClusters'])) - print('force-include periods: {}'.format(len(forceperiods))) - print(' peak-load periods: {}'.format(len(forceperiods_load))) - for tech in forceperiods_minre: - print(' min-{} periods: {}'.format(tech, len(forceperiods_minre[tech]))) - print('total periods: {}'.format(num_rep_periods)) - - - forceperiods_write = pd.DataFrame( - [['load'] + list(i) for i in forceperiods_load] - + [[k]+list(i) for k,v in forceperiods_minre.items() for i in v], - columns=['property','region','reason','year','yperiod'], - ) - forceperiods_write['szn'] = ( - 'y' + forceperiods_write.year.astype(str) - + ('d' if sw.GSw_HourlyType=='year' else sw.GSw_HourlyType[0]) - + forceperiods_write.yperiod.map('{:>03}'.format) - ) - forceperiods_write.drop_duplicates('szn', inplace=True) - - ### Package profiles into one dataframe - profiles = pd.concat({ - **{'load': load_agg}, - **{tech: recf_agg[tech] for tech in techs_vre if tech in recf_agg} - }, - axis=1, - names=('property', 'region'), - ).unstack('h_of_period') - - ### Drop forceperiods for clustering - profiles_fitperiods_hourly = profiles.loc[~profiles.index.isin(forceperiods_yearperiod)].copy() - ## Normalize the profiles if desired - if int(sw.GSw_HourlyNormProfiles): - profiles_fitperiods_hourly /= profiles_fitperiods_hourly.stack('h_of_period').max() - - ### Aggregate from hours to periods if necessary - if sw.GSw_HourlyClusterTimestep in ['period','day','wek','week']: - profiles_fitperiods = ( - profiles_fitperiods_hourly.groupby(axis=1, level=['property','region']).mean()) - else: - profiles_fitperiods = profiles_fitperiods_hourly.copy() - - #%% Plots - if int(sw.debug): - try: - hourly_plots.plot_unclustered_periods(profiles, sw, reeds_path, figpath) - except Exception as err: - print('plot_unclustered_periods failed with the following error:\n{}'.format(err)) - - try: - hourly_plots.plot_feature_scatter(profiles_fitperiods, reeds_path, figpath) - except Exception as err: - print('plot_feature_scatter failed with the following error:\n{}'.format(err)) - - - #%%### Determine representative periods - print("Identify and weight representative periods") - ## First weight the profiles - profiles_fitperiods_weighted = ( - profiles_fitperiods - .multiply(sw.GSw_HourlyClusterWeights, axis=1, level='property') - .dropna(axis=1, how='all') - ) - - ## Representative days or weeks - if sw['GSw_HourlyType'] in ['day','wek']: - rep_periods, period_szn = cluster_profiles( - profiles_fitperiods=profiles_fitperiods_weighted, - sw=sw, - forceperiods_yearperiod=forceperiods_yearperiod, - ) - print("Clustering complete") - - ## 8760 - elif sw['GSw_HourlyType']=='year': - ### For 8760 we use the original seasons - month2quarter = pd.read_csv( - os.path.join(inputs_case, 'month2quarter.csv'), - index_col='month', - ).squeeze(1).map(lambda x: x[:4]) - - period_szn = pd.Series( - index=timestamps_myr.drop_duplicates('yperiod').yperiod.values, - data=timestamps_myr.drop_duplicates('yperiod').index.month.map(month2quarter), - name='szn', - ).rename_axis('period') - - rep_periods = period_szn.index.tolist() - forceperiods_write = pd.DataFrame(columns=['property','region','reason','year','yperiod']) - - - #%%### Identify a (potentially different) collection of periods to use as initial stress periods - if ((not int(sw.GSw_PRM_CapCredit)) - and (sw['GSw_PRM_StressSeedMinRElevel'].lower() not in ['false','none']) - ): - stressperiods_minre = { - tech: identify_min_periods( - df=recf_ra, - hierarchy=hierarchy, - level=sw['GSw_PRM_StressSeedMinRElevel'], - prefix=tech, - ) - for tech in techs_min_vre} - else: - stressperiods_minre = {tech: set() for tech in techs_min_vre} - - if ((not int(sw.GSw_PRM_CapCredit)) - and (sw['GSw_PRM_StressSeedLoadLevel'].lower() not in ['false','none']) - ): - ## Get load for all model and weather years - load_allyears = get_load(inputs_case, keep_weatheryears='all').loc[modelyears] - ## Add descriptive index - load_allyears = load_allyears.merge( - timestamps[['year', 'yperiod', 'h_of_period']], left_on='datetime', right_index=True) - load_allyears = load_allyears.droplevel('datetime') - load_allyears.index.names = ['modelyear'] - load_allyears = load_allyears.set_index(['year', 'yperiod', 'h_of_period'], append=True) - stressperiods_load = { - y: identify_peak_containing_periods( - df=load_allyears.loc[y], hierarchy=hierarchy, - level=sw['GSw_PRM_StressSeedLoadLevel']) - for y in modelyears - } - else: - stressperiods_load = {y: set() for y in modelyears} - - ## Combine dicts of load and min-wind/solar stress periods into a dataframe with - ## (modelyear, property, region, reason) index and (weatheryear, period of year, szn) - ## values. - stressperiods_write = pd.concat( - {y: pd.DataFrame( - [['load'] + list(i) for i in stressperiods_load[y]] - + [[k]+list(i) for k,v in stressperiods_minre.items() for i in v], - columns=['property','region','reason','year','yperiod'] - ).drop_duplicates(subset=['year','yperiod']) - for y in modelyears}, - axis=0, names=['modelyear','index'], - ).reset_index(level='index', drop=True) - stressperiods_write['szn'] = ( - 'y' + stressperiods_write.year.astype(str) - + ('d' if sw.GSw_HourlyType=='year' else sw.GSw_HourlyType[0]) - + stressperiods_write.yperiod.map('{:>03}'.format) - ) - - - #%%### Get the representative and force periods - period_szn_write = period_szn.rename('season').reset_index() - if sw['GSw_HourlyType'] == 'year': - period_szn_write['year'] = sorted(sw['GSw_HourlyWeatherYears']*365) - period_szn_write['yperiod'] = period_szn_write.period - else: - period_szn_write['rep_period'] = period_szn_write['season'].copy() - period_szn_write['year'] = period_szn_write.period.map(lambda x: x[0]) - period_szn_write['yperiod'] = period_szn_write.period.map(lambda x: x[1]) - period_szn_write['actual_period'] = ( - 'y' + period_szn_write.year.astype(str) - + ('w' if sw.GSw_HourlyType == 'wek' else 'd') - + period_szn_write.yperiod.astype(str).map('{:>03}'.format) - ) - if sw['GSw_HourlyType'] == 'year': - period_szn_write['rep_period'] = period_szn_write['actual_period'].copy() - - - #%% Get some other convenience sets - timestamps_day = make_timestamps(sw=pd.Series({**sw, **{'GSw_HourlyType':'day'}})) - timestamps_wek = make_timestamps(sw=pd.Series({**sw, **{'GSw_HourlyType':'wek'}})) - ## Include all possible seasons so dispatch mode can be rerun with any of them - quarters = pd.read_csv( - os.path.join(inputs_case, 'sets', 'quarter.csv'), - header=None, - ).squeeze(1).tolist() - set_allszn = pd.Series( - list(timestamps_day.period.unique()) - + list(timestamps_wek.period.unique()) - + quarters - ) - ## Include stress periods - set_allszn = pd.concat([set_allszn, 's'+set_allszn]) - - set_allh = pd.concat([ - timestamps_day['timestamp'], - timestamps_wek['timestamp'], - 's'+timestamps_day['timestamp'], - 's'+timestamps_wek['timestamp'], - ]) - - set_actualszn = ( - period_szn_write['season'].drop_duplicates() if sw['GSw_HourlyType'] == 'year' - else period_szn_write['actual_period']) - - stress_period_szn = ( - stressperiods_write.assign(rep_period=stressperiods_write.szn) - [['rep_period','year','yperiod','szn']].rename(columns={'szn':'actual_period'}) - ) - - stressperiods_seed = ( - stressperiods_write - .assign(szn='s'+stressperiods_write.szn) - .reset_index().rename(columns={'modelyear':'t'}) - [['t','szn']] - ) - - - #%%### Plot some stuff - try: - hourly_plots.plot_ldc( - period_szn, profiles, rep_periods, - forceperiods_write, sw, reeds_path, figpath) - except Exception: - print('plot_ldc failed:') - print(traceback.format_exc()) - - if int(sw.debug): - try: - hourly_plots.plot_load_days(profiles, rep_periods, period_szn, sw, reeds_path, figpath) - except Exception: - print('plot_load_days failed:') - print(traceback.format_exc()) - - try: - hourly_plots.plot_8760(profiles, period_szn, sw, reeds_path, figpath) - except Exception: - print('plot_8760 failed:') - print(traceback.format_exc()) - - - #%%### Write the outputs - period_szn_write.drop('period', axis=1).to_csv( - os.path.join(inputs_case, periodtype, 'period_szn.csv'), index=False) - - if 'user' not in sw['GSw_HourlyClusterAlgorithm']: - forceperiods_write.to_csv( - os.path.join(inputs_case, periodtype, 'forceperiods.csv'), index=False) - - timestamps.to_csv( - os.path.join(inputs_case, periodtype, 'timestamps.csv'), index=False) - - set_actualszn.to_csv( - os.path.join(inputs_case, periodtype, 'set_actualszn.csv'), header=False, index=False) - - if minimal: - return period_szn_write - - #%% Write the sets over all possible periods (representative and stress) - set_allszn.to_csv( - os.path.join(inputs_case, 'set_allszn.csv'), header=False, index=False) - - set_allh.to_csv( - os.path.join(inputs_case, 'set_allh.csv'), header=False, index=False) - - #%% Write the seed stress periods to use for the PRM constraint - if 'user' in sw.GSw_PRM_StressModel: - stressperiods_seed = pd.read_csv(os.path.join(inputs_case, 'stressperiods_user.csv')) - stressperiods_seed.to_csv(os.path.join(inputs_case, 'stressperiods_seed.csv'), index=False) - _missing = [t for t in modelyears if t not in stressperiods_seed.t.unique()] - if len(_missing): - raise Exception(f"Missing user-defined stress periods for {','.join(map(str, _missing))}") - for t in modelyears: - ## Write the period_szn file - szns = stressperiods_seed.loc[stressperiods_seed.t==t, 'szn'].values - dfwrite = pd.DataFrame({ - 'rep_period': [i.strip('s') for i in szns], - 'year': [int(i.strip('sy')[:4]) for i in szns], - 'yperiod': [int(i[-3:]) for i in szns], - 'actual_period': [i.strip('s') for i in szns], - }) - os.makedirs(os.path.join(inputs_case, f'stress{t}i0'), exist_ok=True) - dfwrite.to_csv(os.path.join(inputs_case, f'stress{t}i0', 'period_szn.csv'), index=False) - else: - stressperiods_seed.to_csv(os.path.join(inputs_case, 'stressperiods_seed.csv'), index=False) - for t in modelyears: - os.makedirs(os.path.join(inputs_case, f'stress{t}i0'), exist_ok=True) - if stressperiods_write.empty: - pd.DataFrame(columns=['property','region','reason','year','yperiod','szn']).to_csv( - os.path.join(inputs_case, f'stress{t}i0', 'forceperiods.csv'), index=False) - else: - stressperiods_write.loc[[t]].to_csv( - os.path.join(inputs_case, f'stress{t}i0', 'forceperiods.csv'), index=False) - if stress_period_szn.empty: - pd.DataFrame(columns=['rep_period','year','yperiod','actual_period']).to_csv( - os.path.join(inputs_case, f'stress{t}i0', 'period_szn.csv'), index=False) - else: - stress_period_szn.loc[[t]].to_csv( - os.path.join(inputs_case, f'stress{t}i0', 'period_szn.csv'), index=False) - - return period_szn_write - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if __name__ == '__main__': - - #%% Parse arguments - parser = argparse.ArgumentParser( - description='Create the necessary 8760 and capacity factor data for hourly resolution') - parser.add_argument('reeds_path', help='ReEDS-2.0 directory') - parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - # #%% Settings for testing - # reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # inputs_case = os.path.join( - # reeds_path,'runs', - # 'v20260411_itlM0_USA_faster','inputs_case','') - # interactive = True - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - print('Starting hourly_repperiods.py') - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - - ####################################### - #%% Identify the representative periods - main(sw=sw, reeds_path=reeds_path, inputs_case=inputs_case) - - #################################################### - #%% Write timeseries data for representative periods - hourly_writetimeseries.main( - sw=sw, reeds_path=reeds_path, inputs_case=inputs_case, - periodtype='rep', - make_plots=1, - ) - - ############################################ - #%% Write timeseries data for stress periods - modelyears = pd.read_csv( - os.path.join(inputs_case, 'modeledyears.csv')).columns.astype(int) - for t in modelyears: - print(f'Writing seed stress periods for {t}') - hourly_writetimeseries.main( - sw=sw, reeds_path=reeds_path, inputs_case=inputs_case, - periodtype=f'stress{t}i0', - make_plots=0, - ) - - #%% All done - reeds.log.toc(tic=tic, year=0, process='input_processing/hourly_repperiods.py', - path=os.path.join(inputs_case,'..')) - print('Finished hourly_repperiods.py') diff --git a/input_processing/hourly_writetimeseries.py b/input_processing/hourly_writetimeseries.py deleted file mode 100644 index 5fa32608..00000000 --- a/input_processing/hourly_writetimeseries.py +++ /dev/null @@ -1,1583 +0,0 @@ -# %% =========================================================================== -### --- IMPORTS --- -### =========================================================================== -import os -import sys -import logging -import shutil -import datetime -import pandas as pd -import numpy as np -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -##% Time the operation of this script -tic = datetime.datetime.now() -## Turn off logging for imported packages -for i in ["matplotlib"]: - logging.getLogger(i).setLevel(logging.CRITICAL) - - -# %%################# -### FIXED INPUTS ### -decimals = 3 -### Indicate whether to show plots interactively [default False] -interactive = False -### Indicate whether to save the old h17 inputs for comparison -debug = True - - -# %% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== -def make_8760_map(period_szn, sw): - """ - """ - hoursperperiod = {'day':24, 'wek':120, 'year':24}[sw['GSw_HourlyType']] - periodsperyear = {'day':365, 'wek':73, 'year':365}[sw['GSw_HourlyType']] - fulltimeindex = reeds.timeseries.get_timeindex(sw.resource_adequacy_years_list) - ### Start with all weather years - hmap_allyrs = pd.DataFrame({ - 'timestamp': fulltimeindex, - 'year': np.ravel([[y]*8760 for y in sw.resource_adequacy_years_list]), - 'yearperiod': np.ravel([ - [h+1 for d in range(365) for h in (d,)*24] if sw['GSw_HourlyType'] == 'year' - else [h+1 for d in range(periodsperyear) - for h in (d,)*hoursperperiod] - for y in sw.resource_adequacy_years_list]), - 'hour': range(1, 8760*len(sw.resource_adequacy_years_list) + 1), - 'hour0': range(8760*len(sw.resource_adequacy_years_list)), - 'yearhour': np.ravel(list(range(1,8761))*len(sw.resource_adequacy_years_list)), - 'periodhour': ( - list(range(1,25))*365*len(sw.resource_adequacy_years_list) - if sw['GSw_HourlyType'] == 'year' - else ( - list(range(1, hoursperperiod+1)) - * periodsperyear - * len(sw.resource_adequacy_years_list)) - ), - }) - hmap_allyrs['actual_period'] = ( - 'y' + hmap_allyrs.year.astype(str) - + ('w' if sw.GSw_HourlyType == 'wek' else 'd') - + hmap_allyrs.yearperiod.astype(str).map('{:>03}'.format) - ) - hmap_allyrs['actual_h'] = ( - hmap_allyrs.actual_period - + 'h' + hmap_allyrs.periodhour.astype(str).map('{:>03}'.format) - ) - hmap_allyrs['season'] = hmap_allyrs.actual_period.map( - period_szn.set_index('actual_period').season) - hmap_allyrs['month'] = hmap_allyrs.timestamp.dt.strftime('%b').str.upper() - ### create the timestamp index: y{20xx}d{xxx}h{xx} (left-padded with 0) - if sw["GSw_HourlyType"] == "year": - ### If using a chronological year (i.e. 8760) the day index uses actual days - hmap_allyrs['h'] = ( - 'y' + hmap_allyrs.year.astype(str) - + 'd' + hmap_allyrs.yearperiod.astype(str).map('{:>03}'.format) - + 'h' + hmap_allyrs.periodhour.astype(str).map('{:>03}'.format) - ) - else: - ### If using representative periods (days/weks) the period index uses - ### representative periods, which are in the 'season' column - hmap_allyrs['h'] = ( - hmap_allyrs.season - + 'h' + hmap_allyrs.periodhour.astype(str).map('{:>03}'.format) - ) - ### hmap_myr (for "model years") only contains the actually-modeled periods - hmap_myr = hmap_allyrs.dropna(subset=['season']).copy() - - return hmap_allyrs, hmap_myr - - -def get_ccseason_peaks_hourly(load, sw, inputs_case, hierarchy, h2ccseason, val_r_all): - ### Aggregate demand by GSw_PRM_hierarchy_level - if sw["GSw_PRM_hierarchy_level"] == "r": - rmap = pd.Series(hierarchy.index, index=hierarchy.index) - else: - rmap = hierarchy[sw['GSw_PRM_hierarchy_level']] - load_agg = ( - load.assign(region=load.r.map(rmap)) - .groupby(["h", "region"]) - .MW.sum() - .unstack("region") - .reset_index() - ) - ### Get the peak hour by ccseason for aggregated load - load_agg["ccseason"] = load_agg.h.map(h2ccseason) - # Get the peak hours for the aggregated region associated with GSw_PRM_hierarchy_level - peakhour_agg_byccseason = load_agg.set_index("h").groupby("ccseason").idxmax() - ### Get the BA/region resolution demand during the peak hour of the associated GSw_PRM_hierarchy_level - - peak_out = {} - - # Determination of season peaks: We merge the peak_agg_byccseason dataframe to rmap and load. - # By changing the peak_agg_byccseason to long format we are able to merge based on the aggregated GSw_PRM_hierarchy_level region - # Then by merging the resultant dataframe to load based on 'r' and peak hour 'h', - # we get the peak hours for each region of interest at the desired region resolution by ccseason. - peak_out = ( - peakhour_agg_byccseason.unstack() - .rename("h") - .reset_index() - .merge(rmap.rename("region").reset_index(), on="region") - .merge(load, on=["r", "h"], how="left")[["r", "ccseason", "MW"]] - ) - - return peak_out - - -def append_csp_profiles(cf_rep, sw): - ### Parse switch data (hourly_repperiods.py does this already but stress_periods.py does not) - if isinstance(sw["GSw_CSP_Types"], str): - sw["GSw_CSP_Types"] = [int(i) for i in sw["GSw_CSP_Types"].split("_")] - ### Get the CSP profiles - cfcsp = cf_rep[[c for c in cf_rep if c.startswith("csp")]].copy() - ### As in cfgather.py, we duplicate the csp1 profiles for each CSP tech - cfcsp_out = pd.concat( - ( - [ - cfcsp.rename( - columns={c: c.replace("csp", f"csp{i}") for c in cfcsp.columns} - ) - for i in sw["GSw_CSP_Types"] - ] - ), - axis=1, - ) - ### Drop the original 'csp' profiles and append the duplicated CSP profiles to rest of cf's - cf_combined = pd.concat( - [cf_rep[[c for c in cf_rep if not c.startswith("csp")]], cfcsp_out], axis=1 - ) - - return cf_combined - - -def get_minloading_windows(sw, h_szn, chunkmap): - """ - Create combinations of h's within GSw_HourlyWindow of each other, - beginning every GSw_HourlyWindowOverlap - """ - ### Inputs for testing - # sw['GSw_HourlyWindow'] = 2 - # sw['GSw_HourlyWindowOverlap'] = 1 - h_szn_chunked = h_szn.assign(h=h_szn.h.map(chunkmap)).drop_duplicates() - seasons = h_szn_chunked.season.unique() - hour_szn_group = set() - for season in seasons: - ## 2 copies so we can loop around the end - timeslices = h_szn_chunked.loc[h_szn_chunked.season == season, "h"].tolist() * 2 - numslices = len(set(timeslices)) - all_combos = [ - (t1, t2) - for (i, t1) in enumerate(timeslices) - for (j, t2) in enumerate(timeslices) - if ( - ## Drop duplicates - (i != j) - ## Must be within GSw_HourlyWindow steps of each other - and (abs(i - j) < int(sw["GSw_HourlyWindow"])) - ## First index must be in the first pass - and (i <= numslices) - ## Only keep the windows that start from overlaps that are kept - and not (i % (int(sw["GSw_HourlyWindowOverlap"]) + 1)) - ) - ] - ### Add both polarities - hour_szn_group.update(all_combos) - hour_szn_group.update([(j, i) for (i, j) in all_combos]) - - ### Format as dataframe and return - hour_szn_group = pd.DataFrame(hour_szn_group, columns=["h", "hh"]).sort_values( - ["h", "hh"] - ) - - return hour_szn_group - - -def get_yearly_demand(sw, hmap_myr, hmap_allyrs, inputs_case, periodtype='rep'): - """ - After clustering based on GSw_HourlyClusterYear and identifying the modeled days, - reload the raw demand and extract the demand on the modeled days for each year. - """ - ### Get original demand data, subset to cluster year - load_in = reeds.io.read_file( - os.path.join(inputs_case,'load.h5'), parse_timestamps=True).unstack(level=0) - load_in.columns = load_in.columns.rename(['r','t']) - ### load.h5 is busbar load, but b_inputs.gms ingests end-use load, so scale down by distloss - scalars = reeds.io.get_scalars(inputs_case) - load_in *= (1 - scalars['distloss']) - - ### Add time index - load_in.index = load_in.index.map(hmap_allyrs.set_index('timestamp')['actual_h']).rename('h') - - load_out = load_in.copy() - ### For full year, keep all periods in the modeled years - if (sw.GSw_HourlyType == 'year') and (periodtype == 'rep'): - load_out = load_out.loc[ - load_out.index.map(hmap_allyrs.set_index('actual_h').year) - .isin(sw['GSw_HourlyWeatherYears']) - ].copy() - ### Otherwise, pull out the specified periods - else: - load_out = load_out.loc[hmap_myr.h.unique()].copy() - - ### Reshape for ReEDS - load_out = load_out.stack("r").reorder_levels(["r", "h"], axis=0).sort_index() - - return load_in, load_out - -def format_climate_inputs(filename, inputs_case, szn_month_weights): - """ - This function converts climate data from monthly to repperiod resolution using the - szn_month_weights - """ - climate_index = { - 'temp_hydadjsea': ['r','season','t'], - 'temp_UnappWaterMult': ['wst','r','season','t'], - 'temp_UnappWaterSeaAnnDistr': ['wst','r','season','t'] - } - - df = pd.read_csv(os.path.join(inputs_case,filename+'.csv')) - df_out = szn_month_weights.merge(df, on='month', how='outer') - df_out['value'] = df_out['weight'] * df_out['Value'] - df_out = ( - df_out - .groupby(climate_index[filename]).agg({'value':'sum'}) - .value - ## For rep periods, sum of season weights is 1, so the next line has no effect. - ## For full chronological year (GSw_HourlyType=year), we use four seasons, - ## so the sum of season weights is the number of months in that season and - ## we need to divide sum{cf*weight} by sum{weight}. - / szn_month_weights.groupby('season').weight.sum() - ).rename('value').reset_index().rename(columns={'season':'szn'}) - # Convert to GAMS-readable wide format - climate_index = [x if x != 'season' else 'szn' for x in climate_index[filename]] - climate_index = [x for x in climate_index if x != 't'] - df_out = df_out.pivot_table(index=climate_index, columns='t', values='value') - - return df_out - -def get_yearly_flexibility( - sw, - period_szn, - rep_periods, - hmap_1yr, - set_szn, - inputs_case, - drcat, -): - """ - After clustering based on GSw_HourlyClusterYear and identifying the modeled days, - reload the raw flexible DR or EV profiles and extract for the modeled days of each year - """ - hoursperperiod = {"day": 24, "wek": 120, "year": np.nan}[sw["GSw_HourlyType"]] - ### Get the set of szn's and h's - szn_h = ( - hmap_1yr.drop_duplicates(["h", "season"]) - .sort_values(["season", "hour"]) - .reset_index(drop=True)[["season", "h"]] - .assign( - periodhour=np.ravel( - ( - [range(1, 25)] * 365 - if sw["GSw_HourlyType"] == "year" - else [range(1, hoursperperiod + 1)] * len(set_szn) - ) - ) - ) - .set_index(["season", "periodhour"]) - .h - ).copy() - - idx_vals = [i + 1 for i in period_szn.index.values] - period_szn_dict = period_szn.set_index("yperiod").to_dict()["season"] - - ### Original flexibility data - shape = {} - shape_out = {} - - for stype in ["increase", "decrease", "energy"]: - if stype == "energy": - if drcat.lower() == "evmc_storage": - shape[stype] = pd.read_csv( - os.path.join(inputs_case, f"evmc_storage_{stype}.csv") - ) - else: - continue - elif drcat.lower() == "evmc_shape": - shape[stype] = pd.read_csv( - os.path.join(inputs_case, f"evmc_shape_profile_{stype}.csv") - ) - elif drcat.lower() == "evmc_storage": - shape[stype] = pd.read_csv( - os.path.join(inputs_case, f"evmc_storage_profile_{stype}.csv") - ) - else: - raise ValueError( - f"drcat must be in ['dr','evmc_shape','evmc_storage'] but is '{drcat}'" - ) - - unique_techs = len(shape[stype].i.unique()) - unique_years = len(shape[stype].year.unique()) - ### Add time indices ("season" is the identifier for modeled periods) - shape[stype]["yperiod"] = ( - np.ravel([[d] * 24 for d in range(1, 366)] * unique_techs * unique_years) - if sw["GSw_HourlyType"] == "year" - else np.ravel( - [[d] * hoursperperiod for d in idx_vals] * unique_techs * unique_years - ) - ) - shape[stype]["periodhour"] = ( - np.ravel([range(1, 25) for d in range(365)] * unique_techs * unique_years) - if sw["GSw_HourlyType"] == "year" - else np.ravel( - [range(1, hoursperperiod + 1) for d in idx_vals] - * unique_techs - * unique_years - ) - ) - shape[stype]["season"] = shape[stype].yperiod.map(period_szn_dict) - - ### If modeling a full year, keep everything - if sw["GSw_HourlyType"] == "year": - shape_out[stype] = shape[stype].drop( - ["yperiod", "periodhour", "season"], axis=1 - ) - shape_out[stype].index = hmap_1yr.h - ### If using representative periods, pull out the representative periods - elif unique_years == 1: - shape_out[stype] = ( - shape[stype] - .loc[shape[stype].season.isin(rep_periods)] - .drop(["yperiod", "hour", "year"], axis=1) - .set_index(["season", "periodhour"]) - .sort_index() - ) - shape_out[stype].index = shape_out[stype].index.map(szn_h).rename("h") - shape_out[stype] = ( - shape_out[stype] - .reset_index() - .set_index(["h", "i"]) - .stack() - .reset_index() - .rename(columns={"i": "*i", "level_2": "r", 0: "Values"})[ - ["*i", "r", "h", "Values"] - ] - ) - else: - shape_out[stype] = ( - shape[stype] - .loc[shape[stype].season.isin(rep_periods)] - .drop(["yperiod", "hour"], axis=1) - .set_index(["season", "periodhour"]) - .sort_index() - ) - - shape_out[stype].index = shape_out[stype].index.map(szn_h).rename("h") - shape_out[stype] = ( - shape_out[stype] - .reset_index() - .set_index(["h", "i", "year"]) - .stack() - .reset_index() - .rename(columns={"i": "*i", "level_3": "r", "year": "t", 0: "Values"})[ - ["*i", "r", "h", "t", "Values"] - ] - ) - - if "energy" in shape.keys(): - return shape_out["decrease"], shape_out["increase"], shape_out["energy"] - else: - return shape_out["decrease"], shape_out["increase"] - - -# %% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== -def main(sw, reeds_path, inputs_case, periodtype='rep', make_plots=1, logging=True): - """ """ - # #%% Settings for testing - # reeds_path = os.path.realpath(os.path.join(os.path.dirname(__file__),'..')) - # inputs_case = os.path.join(reeds_path, 'runs', 'v20250313_chunkM0_Pacific_r4mean_s4max', 'inputs_case') - # sw = reeds.io.get_switches(inputs_case) - # periodtype = 'stress2010i0' - # periodtype = 'rep' - # make_plots = 0 - - #%% Set up logger - if logging: - _log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - # %% Parse some switches - if not isinstance(sw["GSw_HourlyWeatherYears"], list): - sw["GSw_HourlyWeatherYears"] = [ - int(y) for y in sw["GSw_HourlyWeatherYears"].split("_") - ] - - # Ensure the GSw_CSP_Types is a list, as hourly_writetimeseries is called in F_stress_periods.py as well - if not isinstance(sw['GSw_CSP_Types'],list): - sw['GSw_CSP_Types'] = [int(i) for i in sw['GSw_CSP_Types'].split('_')] - ## Make outputs path - outpath = os.path.join(inputs_case, periodtype) - os.makedirs(outpath, exist_ok=True) - ## Designate prefix for timestamps - tprefix = 's' if periodtype.startswith('stress') else '' - - # %%### Load shared files - val_r_all = ( - pd.read_csv(os.path.join(inputs_case, "val_r_all.csv"), header=None) - .squeeze(1) - .tolist() - ) - hierarchy = ( - pd.read_csv(os.path.join(inputs_case, "hierarchy.csv")) - .rename(columns={"*r": "r"}) - .set_index("r") - ) - - #%%### Load period-szn map, get representative and stress periods - period_szn = pd.read_csv(os.path.join(outpath, 'period_szn.csv')) - try: - forceperiods = pd.read_csv(os.path.join(outpath, 'forceperiods.csv')) - except FileNotFoundError: - forceperiods = pd.DataFrame( - columns=["property", "region", "reason", "year", "yperiod", "szn"] - ) - ### Strip off prefix to start with a fresh slate - for col in ["rep_period", "actual_period"]: - period_szn[col] = period_szn[col].str.strip(tprefix) - if len(forceperiods): - for col in ["szn"]: - forceperiods[col] = forceperiods[col].str.strip(tprefix) - if "season" not in period_szn: - period_szn["season"] = period_szn["rep_period"].copy() - - # %%### If there are no periods, write empty dataframes and stop here - if not len(period_szn): - write = { - 'set_h': ['*h'], - 'set_szn': ['*szn'], - 'h_preh': ['*h','preh'], - 'szn_actualszn': ['*season', 'actual_period'], - 'numpartitions': ['*actual_period', 'next_actual_period'], - 'nextpartition': ['*actual_period', 'next_actual_period'], - 'h_szn': ['*h','season'], - 'h_dt_szn': ['h','season','ccseason','year','hour'], - 'numhours': ['*h','numhours'], - 'nexth': ['*h','h'], - 'frac_h_ccseason_weights': ['*h','ccseason','weight'], - 'frac_h_quarter_weights': ['*h','quarter','weight'], - 'h_szn_start': ['*season','h'], - 'h_szn_end': ['*season','h'], - 'hour_szn_group': ['*h','hh'], - 'opres_periods': ['*szn'], - 'h_ccseason_prm': ['*h','ccseason'], - 'load_allyear': ['*r','h','t','MW'], - 'peak_ccseason': ['*r','ccseason','t','MW'], - 'cf_vre': ['*i','r','h','cf'], - 'cf_hyd': ['*i','szn','r','t','cf'], - 'cap_hyd_szn_adj': ['*i','szn','r','value'], - 'can_exports_h_frac': ['*h','frac_weighted'], - 'can_imports_szn_frac': ['*szn','frac_weighted'], - 'period_weights': ['*szn','rep_period'], - 'hmap_myr': ['*timestamp', 'year', 'yearperiod', 'hour', 'hour0', 'yearhour', - 'periodhour', 'actual_period', 'actual_h', 'season', 'month', 'h'], - 'periodmap_1yr': ['*actual_period','season'], - 'canmexload': ['*r','h'], - 'outage_forced_h': ['*i','r','h'], - 'outage_scheduled_h': ['*i','h'], - 'dr_shed_out': ['*i','r','h'], - 'evmc_baseline_load': ['r','h','t'], - 'evmc_shape_generation': ['*i','r','h'], - 'evmc_shape_load': ['*i','r','h'], - 'evmc_storage_discharge': ['*i','r','h','t'], - 'evmc_storage_charge': ['*i','r','h','t'], - 'evmc_storage_energy': ['*i','r','h','t'], - 'flex_frac_all': ['*flex_type','r','h','t'], - 'peak_h': ['*r','h','t','MW'], - } - for f, columns in write.items(): - pd.DataFrame(columns=columns).to_csv( - os.path.join(outpath, f+'.csv'), index=False) - - return write - - - #%%### Process the representative period weights - #%% Generate map from actual to representative periods - hmap_allyrs, hmap_myr = make_8760_map(period_szn=period_szn, sw=sw) - ### Add prefix if necessary - if tprefix: - for col in ["actual_period", "actual_h", "season", "h"]: - hmap_myr[col] = tprefix + hmap_myr[col] - hmap_allyrs[col] = tprefix + hmap_allyrs[col] - for col in ['rep_period','actual_period']: - period_szn[col] = tprefix + period_szn[col] - if not ( - (sw['GSw_HourlyType'] == 'year') - and ((periodtype == 'rep') or periodtype.startswith('pcm')) - ): - period_szn['season'] = tprefix + period_szn['season'] - if len(forceperiods): - for col in ["szn"]: - forceperiods[col] = tprefix + forceperiods[col] - - ### Add ccseasons - ccseason_dates = pd.read_csv( - os.path.join(inputs_case, 'ccseason_dates.csv'), - index_col=['month','day'], - ).squeeze(1) - hmap_allyrs['ccseason'] = hmap_allyrs.timestamp.map(lambda x: ccseason_dates[x.month, x.day]) - - #%%### Load full hourly RE CF, for downselection below - #%% VRE - recf = reeds.io.read_file(os.path.join(inputs_case, 'recf.h5'), parse_timestamps=True) - ### Overwrite CSP CF (which in recf.h5 is post-storage) with solar field CF - cspcf = reeds.io.read_file(os.path.join(inputs_case, 'csp.h5'), parse_timestamps=True) - recf = ( - recf.drop([c for c in recf if c.startswith('csp')], axis=1) - .merge(cspcf, left_index=True, right_index=True) - ).loc[hmap_allyrs.timestamp] - recf.index = hmap_allyrs.actual_h - - # %% Get data for representative periods - rep_periods = sorted(period_szn.rep_period.unique()) - - ### Broadcast CSP values for all techs - cf_rep = recf.loc[ - recf.index.map(lambda x: any([x.startswith(i) for i in rep_periods])) - ] - - if int(sw["GSw_CSP"]) != 0: - cf_rep = append_csp_profiles(cf_rep=cf_rep, sw=sw) - - cf_out = cf_rep.rename_axis("h").copy() - i = cf_rep.columns.map(lambda x: x.split("|")[0]) - r = cf_rep.columns.map(lambda x: x.split("|")[1]) - cf_out.columns = pd.MultiIndex.from_arrays([i, r], names=["i", "r"]) - cf_out = ( - cf_out.stack(["i", "r"]) - .reorder_levels(["i", "r", "h"]) - .rename("cf") - .reset_index() - ) - - # %%### Create the temporal sets used by ReEDS - ### Calculate number of hours represented by each timeslice - hours = ( - hmap_myr.groupby('h').season.count().rename('numhours') - / (len(sw['GSw_HourlyWeatherYears']) if not periodtype.startswith('stress') else 1)) - ## Stress period hours are scaled to sum to 6 hours, making 8766 hours (365.25 days) per year - if periodtype.startswith('stress'): - hours = hours / hours.sum() * 6 - ### Make sure it lines up - if not periodtype.startswith('stress'): - assert int(np.around(hours.sum(), 0)) % 8760 == 0 - else: - assert np.around(hours.sum(), 0) == 6 - - # create the timeslice-to-season and timeslice-to-ccseason mappings - h_szn = hmap_myr[['h','season']].drop_duplicates().reset_index(drop=True) - h_ccseason = hmap_allyrs[['h','ccseason']].drop_duplicates().reset_index(drop=True) - - ### create the set of szn's modeled in ReEDS - set_szn = pd.DataFrame({"szn": period_szn.season.sort_values().unique()}) - - ### create the set of timeslicess modeled in ReEDS - hset = h_szn.h.sort_values().reset_index(drop=True) - - ### List of periods in which to apply operating reserve constraints - if (not periodtype.startswith('stress')) and ('user' in sw['GSw_HourlyClusterAlgorithm']): - period_szn_user = pd.read_csv(os.path.join(inputs_case, 'period_szn_user.csv')) - opres_periods = period_szn_user.loc[ - ~period_szn_user.opres.isnull() - ].rep_period.drop_duplicates().rename('szn').to_frame() - elif (sw["GSw_OpResPeriods"] == "all") or (sw["GSw_HourlyType"] == "year"): - opres_periods = set_szn - elif sw["GSw_OpResPeriods"] == "representative": - opres_periods = set_szn.loc[~set_szn.szn.isin(forceperiods.szn)] - elif sw["GSw_OpResPeriods"] == "stress": - opres_periods = set_szn.loc[set_szn.szn.isin(forceperiods.szn)] - elif sw["GSw_OpResPeriods"] in ["peakload", "peak_load", "peak", "load", "demand"]: - opres_periods = set_szn.loc[ - set_szn.szn.isin(forceperiods.loc[forceperiods.property == "load"].szn) - ] - elif sw["GSw_OpResPeriods"] in ["minre", "re", "vre", "min_re"]: - opres_periods = set_szn.loc[ - set_szn.szn.isin(forceperiods.loc[forceperiods.property != "load"].szn) - ] - - ### Calculate the fraction of each h associated with each ReEDS quarter, for compatibility - ### with model inputs that are defined by quarter - ## Get a map from hour-of-year to ReEDS quarter - month2quarter = pd.read_csv( - os.path.join(inputs_case, 'month2quarter.csv'), - index_col='month', - ).squeeze(1) - - quarters = ( - hmap_allyrs.iloc[:8760] - .set_index('hour') - .timestamp.dt.month.map(month2quarter) - .rename('quarter') - .map(lambda x: x[:4]) - ) - - ccseasons = ( - hmap_allyrs.iloc[:8760] - .set_index('hour').ccseason - ) - - # %% Calculate the fraction of hours of each timeslice associated with each quarter - frac_h_weights = {} - if not periodtype.startswith('stress'): - for season in ['quarter','ccseason']: - frac_h_weights[season] = hmap_myr.copy() - frac_h_weights[season][season] = hmap_myr.yearhour.map( - {"quarter": quarters, "ccseason": ccseasons}[season] - ) - frac_h_weights[season] = ( - ## Count the number of days in each szn that are part of each quarter - frac_h_weights[season].groupby(["h", season])["season"].count() - ## Normalize by the total number of hours per timeslice - / hours - / len(sw["GSw_HourlyWeatherYears"]) - ).rename("weight") - frac_h_weights[season] = frac_h_weights[season].reset_index() - else: - for season in ['quarter','ccseason']: - frac_h_weights[season] = hmap_allyrs.copy() - frac_h_weights[season][season] = hmap_allyrs.yearhour.map( - {'quarter':quarters, 'ccseason':ccseasons}[season]) - frac_h_weights[season] = ( - ( - frac_h_weights[season] - .groupby(["actual_h", season]) - .actual_period.count() - ) - .rename_axis(["h", season]) - .rename("weight") - .reset_index() - ) - - ### Make sure it lines up - for season in ["quarter", "ccseason"]: - assert (frac_h_weights[season].groupby("h").weight.sum().round(5) == 1).all() - - ### Calculate the fraction of hours of each h associated with each calendar month, - ### for compatibility with model inputs that are defined by quarter - # Get a map from hour-of-year to ReEDS month - months = hmap_allyrs.iloc[:8760].set_index('hour').month - - if not periodtype.startswith('stress'): - frac_h_month_weights = hmap_myr.copy() - frac_h_month_weights["month"] = hmap_myr.yearhour.map(months) - frac_h_month_weights = ( - ## Count the number of days in each szn that are part of each month - frac_h_month_weights.groupby(["h", "month"]).season.count() - ## Normalize by the total number of hours per timeslice - / hours - / len(sw["GSw_HourlyWeatherYears"]) - ).rename("weight") - frac_h_month_weights = frac_h_month_weights.reset_index() - else: - frac_h_month_weights = hmap_allyrs.copy() - frac_h_month_weights['month'] = hmap_allyrs.yearhour.map(months) - frac_h_month_weights = ( - (frac_h_month_weights.groupby(["actual_h", "month"]).actual_period.count()) - .rename_axis(["h", "month"]) - .rename("weight") - .reset_index() - ) - - ### Make sure it lines up - assert (frac_h_month_weights.groupby("h").weight.sum().round(5) == 1).all() - - # %%### Seasonal Canadian imports/exports for GSw_Canada=1 - # %% Exports: Spread equally over hours by quarter. - can_exports_szn_frac = pd.read_csv( - os.path.join(inputs_case, "can_exports_szn_frac.csv"), - header=0, - names=["season", "frac"], - index_col="season", - ).squeeze(1) - df = ( - frac_h_weights["quarter"] - .astype({"h": "str", "quarter": "str"}) - .replace({"weight": {0: np.nan}}) - .dropna(subset=["weight"]) - .copy() - ) - df = ( - df.assign(frac_exports=df.quarter.map(can_exports_szn_frac)) - .assign(season=df.h.map(h_szn.set_index("h").season)) - .assign(hours=df.h.map(hours)) - ) - df["quarter_hours"] = df.hours * df.weight - df["hours_per_quarter"] = df.quarter.map(quarters.value_counts()) - df["frac_weighted"] = df.frac_exports * df.quarter_hours / df.hours_per_quarter - can_exports_h_frac = df.groupby("h", as_index=False).frac_weighted.sum() - ### Make sure it sums to 1 - if not periodtype.startswith('stress'): - assert can_exports_h_frac.frac_weighted.sum().round(5) == 1 - - # %% Imports: Spread over seasons by quarter. - can_imports_quarter_frac = pd.read_csv( - os.path.join(inputs_case, "can_imports_quarter_frac.csv"), - header=0, - names=["season", "frac"], - index_col="season", - ).squeeze(1) - df = hmap_myr.assign(quarter=hmap_myr.yearhour.map(quarters)) - hours_per_quarter = df["quarter"].value_counts() - ## Fraction of quarter made up by each season (typically rep period) - quarter_season_weights = ( - df.groupby(["quarter", "season"]) - .year.count() - .divide(hours_per_quarter, axis=0, level="quarter") - ) - can_imports_szn_frac = ( - quarter_season_weights.multiply(can_imports_quarter_frac, level="quarter") - .groupby("season") - .sum() - .rename("frac_weighted") - .reset_index() - .rename(columns={"season": "szn"}) - ) - ### Make sure it sums to 1 - if not periodtype.startswith('stress'): - assert can_imports_szn_frac.frac_weighted.sum().round(5) == 1 - - ################################################## - # -- Hour, Region, and Timezone Mapping -- # - ################################################## - - period_weights = ( - (period_szn.rep_period.value_counts() / len(sw["GSw_HourlyWeatherYears"])) - .reset_index() - .rename(columns={"index": "szn", "szn": "weight"}) - ) - - ###### Mapping from hourly resolution to GSw_HourlyChunkLength resolution - ### Aggregation is performed as an average over the hours to be aggregated - ### For simplicity, midnight is always a boundary between chunks - - ### First make sure the number of hours is divisible by the chunk length - GSw_HourlyChunkLength = int( - sw[f"GSw_HourlyChunkLength{'Stress' if periodtype.startswith('stress') else 'Rep'}"]) - assert not len(hset) % GSw_HourlyChunkLength, ( - "Hours are not divisible by chunk length:" - "\nlen(hset) = {}\nGSw_HourlyChunkLength = {}" - ).format(len(hset), GSw_HourlyChunkLength) - - ### Map hours to chunks. Chunks are formatted as hour-ending. - ## If GSw_HourlyChunkLength == 2, - ## h1-h2-h3-h4-h5-h6 is mapped to h2-h2-h4-h4-h6-h6. - outchunks = hmap_myr.actual_h[GSw_HourlyChunkLength - 1 :: GSw_HourlyChunkLength] - chunkmap = dict( - zip( - hmap_myr.actual_h.values, - np.ravel([[c] * GSw_HourlyChunkLength for c in outchunks]), - ) - ) - - outchunks_allyrs = hmap_allyrs.actual_h[GSw_HourlyChunkLength-1::GSw_HourlyChunkLength] - chunkmap_allyrs = dict(zip( - hmap_allyrs.actual_h.values, - np.ravel([[c]*GSw_HourlyChunkLength for c in outchunks_allyrs]) - )) - - # %%### h_dt_szn for Augur - if not len(hmap_myr) % 8760: - ## Important: When modeling a single weather year, rep periods in the - ## h_dt_szn table are just the single-year periods concatenated n times. - ## In that case electrolyzer demand (optimized for GSw_HourlyWeatherYears, usually - ## 2012) won't line up with optimal operation times (low demand / high wind/solar) - ## outside of the single reprsentative year. - h_dt_szn = pd.concat( - {y: hmap_myr.drop_duplicates(['yearhour']).drop('year', axis=1) - for y in sw.resource_adequacy_years_list}, names=('year',), - axis=0, - ).reset_index(level='year').reset_index(drop=True) - h_dt_szn['ccseason'] = h_dt_szn.timestamp.map(lambda x: ccseason_dates[x.month, x.day]) - h_dt_szn['hour0'] = h_dt_szn.index - h_dt_szn['hour'] = h_dt_szn['hour0'] + 1 - for col in ['actual_period', 'actual_h']: - h_dt_szn[col] = 'y' + h_dt_szn.year.astype(str) + h_dt_szn[col].str[5:] - ## If hmap_myr contains less than a full year (e.g. for stress periods), - ## just use hmap_myr as-is. - else: - h_dt_szn = hmap_myr.copy() - h_dt_szn['ccseason'] = h_dt_szn.actual_h.map(hmap_allyrs.set_index('actual_h').ccseason) - - ################################################ - # -- Season starting and ending hours -- # - ################################################ - - ### Start hour is the lowest-numbered h in each season - ## End up with a series mapping season to start hour: {'szn1':'h1', ...} - szn2starth = hmap_myr.drop_duplicates("season", keep="first").set_index("season").h - - ### End hour is the highest-numbered h in each season - szn2endh = hmap_myr.drop_duplicates("season", keep="last").set_index("season").h - - ### next timeslice - nexth_actualszn = ( - hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) - [[ - ( - 'season' - if ((sw.GSw_HourlyType == 'year') and (not periodtype.startswith('stress'))) - else 'actual_period' - ), - 'h', - ]] - .drop_duplicates() - .rename(columns={"actual_period": "allszn", "season": "allszn"}) - ).copy() - ## Roll to make a lookup table for GAMS - nexth_actualszn["allsznn"] = np.roll(nexth_actualszn["allszn"], -1) - nexth_actualszn["hh"] = np.roll(nexth_actualszn["h"], -1) - - ### h-to-actual-period mapping for inter-period storage - h_actualszn = ( - hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) - [[ - 'h', - ( - 'season' - if ((sw.GSw_HourlyType == 'year') and (not periodtype.startswith('stress'))) - else 'actual_period' - ) - ]] - .drop_duplicates()) - - ### The following four sets are used for the inter-day linkage constraints for energy storage - ### Inter-day linkage only applicable to rep day and wek scenarios, so we only need to calculate these sets for - ### rep day and wek scenarios, otherwise they are empty - if sw.GSw_HourlyType in ['day', 'wek']: - ### Write rep period to actual period mapping set for inter-day storage linkage - szn_actualszn = ( - hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) - [['season', 'actual_period']] - .drop_duplicates()) - - ### Group actual period to partitions and count number of partitions of for each actual period - numpartitions = ( - hmap_myr.assign(h=hmap_myr.h.map(chunkmap)) - [['season', 'actual_period']] - .drop_duplicates()).copy() - - if 'actual_period' not in numpartitions.columns: - numpartitions['actual_period'] = numpartitions['season'] - - numpartitions['partition'] = ( - numpartitions['season'] != numpartitions['season'].shift() - ).cumsum() - - count_partition = numpartitions.groupby('partition').size() - numpartitions['partition_count'] = numpartitions['partition'].map(count_partition) - - numpartitions = ( - numpartitions.drop_duplicates('partition') - [['actual_period', 'partition_count']] - .reset_index(drop=True) - ) - - ### Write next partition mapping set - nextpartition = numpartitions[['actual_period']].copy() - nextpartition['next_actual_period'] = nextpartition['actual_period'].shift(-1) - nextpartition.iloc[-1, nextpartition.columns.get_loc('next_actual_period') - ] = nextpartition['actual_period'].iloc[0] - - ### Write mapping set between current hour and previous hours before current hour - ### of the period to assist the inter-day linkage constraints. For example: - ### y2012d001h004 -> [y2012d001h004] - ### y2012d001h008 -> [y2012d001h004, y2012d001h008] - ### y2012d001h012 -> [y2012d001h004, y2012d001h008, y2012d001h012] - unique_timeslices = ( - hmap_myr - .assign(h=hmap_myr.h.map(chunkmap)) - .drop_duplicates('h') - .sort_values('h') - ) - h_preh = pd.Series( - index=unique_timeslices.h, - data=( - unique_timeslices - .groupby('season') - .h.apply(lambda x: (x+' ').cumsum().str.strip().str.split()) - .values - ), - name='preh', - ).explode().reset_index() - - else: - szn_actualszn = pd.DataFrame(columns=['season', 'actual_period']) - numpartitions = pd.DataFrame(columns=['actual_period', 'partition_count']) - nextpartition = pd.DataFrame(columns=['actual_period', 'next_actual_period']) - h_preh = pd.DataFrame(columns=['h', 'preh']) - - ### Number of times one h follows another h (for startup/ramping costs) - numhours_nexth = ( - hmap_myr.assign(h=hmap_myr.h.map(chunkmap))[ - ["actual_period", "h"] - ].drop_duplicates() - ).copy() - ## Roll to make a lookup table for GAMS - numhours_nexth = ( - numhours_nexth.assign(nexth=np.roll(numhours_nexth["h"], -1)) - .groupby(["h", "nexth"]) - .count() - .reset_index() - .rename(columns={"nexth": "hh", "actual_period": "hours"}) - ) - - #%%### Adjacent hour linkages (including links across adjacent stress periods) - if not periodtype.startswith('stress'): - if (sw.GSw_HourlyType == 'year') and (sw.GSw_HourlyWrapLevel == 'year'): - nexth_unchunked = dict(zip( - hmap_myr.actual_h.values, - np.roll(hmap_myr.actual_h.values, -1) - )) - else: - nexth_unchunked = {} - for period in hmap_myr.season.unique(): - hs = hmap_myr.loc[hmap_myr.season == period, "h"].values - nexth_unchunked = {**nexth_unchunked, **dict(zip(hs, np.roll(hs, -1)))} - else: - ### Get runs of periods - ## Two copies in case it loops from end of timeseries to beginning - unique_periods = list(hmap_myr.actual_period.unique())*2 - ## Map from each actual period to the next actual period - next_actual_period = dict(zip( - hmap_allyrs.actual_period.drop_duplicates().values, - np.roll(hmap_allyrs.actual_period.drop_duplicates().values, -1), - )) - _runs = [] - for period in unique_periods: - ## Start a run for each period - this_run = [period] - for nextperiod in unique_periods: - ## If the next period is a stress period, add it to the run, then - ## do the same for the period after that (and so forth) - if next_actual_period[period] == nextperiod: - this_run += [nextperiod] - period = nextperiod - _runs.append(this_run) - - runs = pd.Series(_runs).drop_duplicates() - - ### For each period, get the longest run containing it - _longest_run = {} - for i, period in enumerate(unique_periods): - _longest_run[period] = [] - for j, row in runs.items(): - if (period in row) and (len(row) > len(_longest_run[period])): - _longest_run[period] = row - - longest_run = pd.Series(_longest_run.values()).drop_duplicates() - - ### Cyclic boundary conditions within each run of periods - nexth_unchunked = {} - for i, row in longest_run.items(): - hs = hmap_allyrs.set_index('actual_period').loc[row,'h'] - nexth_unchunked = { - **nexth_unchunked, - **dict(zip(hs, np.roll(hs, -1))) - } - - nexth = pd.Series({ - chunkmap_allyrs[k]: chunkmap_allyrs[v] for k,v in nexth_unchunked.items() - }).rename_axis('*h').rename('h') - - # %%########################################## - # -- Hour groups for eq_minloading -- # - ############################################# - - hour_szn_group = get_minloading_windows(sw=sw, h_szn=h_szn, chunkmap=chunkmap) - - ############################# - # -- Yearly demand -- # - ############################# - - load_in, load_h = get_yearly_demand( - sw=sw, hmap_myr=hmap_myr, hmap_allyrs=hmap_allyrs, inputs_case=inputs_case, - periodtype=periodtype, - ) - - ###### Get the peak demand in each (r,szn,modelyear) for GSw_HourlyWeatherYears - load_full_yearly = load_in.loc[ - load_in.index.map(hmap_allyrs.set_index('actual_h').year.isin(sw['GSw_HourlyWeatherYears'])) - ].stack('r').reset_index() - - h2ccseason = hmap_allyrs.set_index('actual_h').ccseason - years = pd.read_csv(os.path.join(inputs_case,'modeledyears.csv')).columns.astype(int).values - - peak_all = {} - for year in years: - peak_all[year] = get_ccseason_peaks_hourly( - load=load_full_yearly[["r", "h", year]].rename(columns={year: "MW"}), - sw=sw, - inputs_case=inputs_case, - hierarchy=hierarchy, - h2ccseason=h2ccseason, - val_r_all=val_r_all, - ) - peak_all = ( - pd.concat(peak_all, names=["t", "drop"]) - .reset_index() - .drop("drop", axis=1)[["r", "ccseason", "t", "MW"]] - ).copy() - - ############################################## - # %% -- Hydro Month-to-Szn Adjustments -- # - ############################################## - - ### Import and format hydro capacity factors - h_szn_chunked = h_szn.assign(h=h_szn.h.map(chunkmap)).drop_duplicates() - - ## Calculate fraction of each month associated with each season. - szn_month_weights = ( - frac_h_month_weights.merge(h_szn_chunked, on="h", how="inner") - .drop("h", axis=1) - .drop_duplicates()[["season", "month", "weight"]] - ) - - cf_hyd = pd.read_csv( - os.path.join(inputs_case, "hydcf.csv"), - header=0, - ).rename(columns={"value": "cf_month"}) - ## Filter for modeled years - ## Get last data year (year used to forward-fill data) by removing all duplicated data - lastdatayr = ( - cf_hyd.pivot_table(index='t', columns=['*i','month','r'], values='cf_month') - ## remove all duplicated data, leaving the last data year - .drop_duplicates() - .index.max() - ) - buildyears = np.arange(2010, lastdatayr+1).tolist() + [y for y in years if y > lastdatayr] - cf_hyd = cf_hyd.loc[cf_hyd["t"].isin(buildyears)] - ## Calculate the month-weighted-average capacity factor by season - cf_hyd_out = szn_month_weights.merge(cf_hyd, on="month", how="outer") - cf_hyd_out["cf"] = cf_hyd_out["weight"] * cf_hyd_out["cf_month"] - cf_hyd_out = ( - ( - cf_hyd_out.groupby(["*i", "season", "r", "t"]) - .sum() - .drop(["month", "weight", "cf_month"], axis=1) - .cf - ## For rep periods, sum of season weights is 1, so the next line has no effect. - ## For full chronological year (GSw_HourlyType=year), we use four seasons, - ## so the sum of season weights is the number of months in that season and - ## we need to divide sum{cf*weight} by sum{weight}. - / szn_month_weights.groupby("season").weight.sum() - ) - .rename("cf") - .reset_index() - .rename(columns={"season": "szn"}) - ) - - ### Import and format monthly hydro capacity adjustment factors - hydcapadj = pd.read_csv( - os.path.join(inputs_case, "hydcapadj.csv"), header=0 - ).rename(columns={"value": "cap_month"}) - ## Calculate the month-weighted-average capacity factor by season - hydcapadj_out = szn_month_weights.merge(hydcapadj, on="month", how="outer") - hydcapadj_out["cap"] = hydcapadj_out["weight"] * hydcapadj_out["cap_month"] - hydcapadj_out = ( - ( - hydcapadj_out.groupby(["*i", "season", "r"]) - .sum() - .drop(["month", "weight", "cap_month"], axis=1) - .cap - ## For rep periods, sum of season weights is 1, so the next line has no effect. - ## For full chronological year (GSw_HourlyType=year), we use four seasons, - ## so the sum of season weights is the number of months in that season and - ## we need to divide sum{cf*weight} by sum{weight}. - / szn_month_weights.groupby("season").weight.sum() - ) - .rename("value") - .reset_index() - .rename(columns={"season": "szn"}) - ) - - ### Import and format monthly climate_{hydadjsea/UnappWaterMult/UnappWaterSeaAnnDistr}.csv - climate_files = {} - if int(sw.GSw_ClimateHydro): - climate_files['temp_hydadjsea'] = format_climate_inputs('temp_hydadjsea', inputs_case, szn_month_weights) - if int(sw.GSw_ClimateWater): - for file in ['temp_UnappWaterMult', 'temp_UnappWaterSeaAnnDistr']: - climate_files[file] = format_climate_inputs(file, inputs_case, szn_month_weights) - - ### Calculate the peak demand timeslice of each ccseason. - ## Used for hydro_nd PRM constraint. - h_ccseason_prm = ( - pd.merge(load_h[max(years)].groupby("h").sum().rename("MW"), h_ccseason, on="h") - .sort_values("MW") - .drop_duplicates("ccseason", keep="last") - .drop("MW", axis=1) - .sort_values("ccseason") - ) - - - #%%### Outage rates ###### - aggmethod = 'mean' if (not periodtype.startswith('stress')) else 'max' - - outage_h = {} - for outage_type in ['forced', 'scheduled']: - outage_hourly = reeds.io.get_outage_hourly(inputs_case, outage_type) - column_levels = list(outage_hourly.columns.names) - ## Aggregate to model resolution - outage_h[outage_type] = outage_hourly.loc[hmap_myr.timestamp].copy() - outage_h[outage_type].index = hmap_myr.h.map(chunkmap) - outage_h[outage_type] = ( - outage_h[outage_type] - .groupby(outage_h[outage_type].index) - .agg(aggmethod) - .stack(column_levels) - .reorder_levels(column_levels+['h']) - .rename('outage_rate') - .reset_index() - ) - - - #%% - ############################# - # -- DR shed -- # - ############################# - if int(sw.GSw_DRShed) and periodtype.startswith('stress'): - # Only available in stress periods - - # identify year - t = int(periodtype[6:10]) - - # each year (2030-2050) has a different dr shed profile - # prior years assume 2030 data - t_set = max(t, 2030) - - dr_shed_avail_allyears = reeds.io.read_file(os.path.join(inputs_case, 'dr_shed_hourly.h5'), parse_timestamps=True) - dr_shed_avail_allyears['year'] = round(dr_shed_avail_allyears['year'],0).astype(int) - dr_shed_avail = dr_shed_avail_allyears.loc[dr_shed_avail_allyears['year']==t_set].copy().drop('year', axis=1) - - # dr_shed only has 2018 weather year data, need to populate for other RA years - dr_shed_avail_all_weatheryears = pd.DataFrame() - # copy 2018 data to other weather years - for y in sw.resource_adequacy_years_list: - #set datetime column to match hmap_allyrs.timestamp for y - dr_shed_avail_new_index = dr_shed_avail.copy() - dr_shed_avail_new_index.index = pd.to_datetime(hmap_allyrs[hmap_allyrs['year']==y].timestamp) - - dr_shed_avail_all_weatheryears = pd.concat([dr_shed_avail_all_weatheryears, dr_shed_avail_new_index]) - - # downselect dr_shed_avail to timestamps in all weather years - dr_shed_avail_all_weatheryears.loc[hmap_allyrs.timestamp] - # map dr_shed_avail index to actual period - dr_shed_avail_all_weatheryears.index = hmap_allyrs.loc[hmap_allyrs.timestamp.isin(dr_shed_avail_all_weatheryears.index)].actual_h - # Map actual periods to rep periods - dr_shed_avail_all_weatheryears = dr_shed_avail_all_weatheryears.loc[ - dr_shed_avail_all_weatheryears.index.map(lambda x: any([x.startswith(i) for i in rep_periods]))] - #Need to convert avail to a fraction - use max in each column as base - # Normalize dr_shed_avail by values specified in inputs/demand_response/dr_shed_avail_scalar.csv - dr_shed_avail_scalar = pd.read_csv(os.path.join(inputs_case,'dr_shed_avail_scalar.csv')) - dr_shed_avail_scalar = dr_shed_avail_scalar[dr_shed_avail_scalar['t']==t_set]['Value'].item() - dr_shed_avail_all_weatheryears = (dr_shed_avail_all_weatheryears - .div(dr_shed_avail_all_weatheryears .max()))*dr_shed_avail_scalar - - - # Reformat to be indexed by i,r,h - dr_shed_avail_out = dr_shed_avail_all_weatheryears.rename_axis('h').copy() - i = dr_shed_avail_all_weatheryears.columns.map(lambda x: x.split('|')[0]) - r = dr_shed_avail_all_weatheryears.columns.map(lambda x: x.split('|')[1]) - dr_shed_avail_out.columns = pd.MultiIndex.from_arrays([i,r], names=['i','r']) - dr_shed_avail_out = dr_shed_avail_out.stack(['i','r']).reorder_levels(['i','r','h']).rename('cap').reset_index() - - else: - # populate empty dataframe - dr_shed_avail_out = pd.DataFrame(columns=['i','r','h','cap']) - - ############################# - # -- EV Managed Charging -- # - ############################# - - if int(sw.GSw_EVMC): - evmc_baseline_load = ( - pd.read_hdf(os.path.join(inputs_case, "ev_baseline_load.h5")) - .rename(columns={"h": "hour"}) - .astype({"r": str}) - ) - ## Drop the h - evmc_baseline_load.hour = evmc_baseline_load.hour.str.strip("h").astype("int") - ## Concat for each weather year - evmc_baseline_load_weatheryears = evmc_baseline_load.pivot( - index="hour", columns=["t", "r"], values="net" - ) - evmc_baseline_load_weatheryears = pd.concat( - {y: evmc_baseline_load_weatheryears for y in sw.resource_adequacy_years_list}, - axis=0, ignore_index=True).loc[hmap_myr.hour0] - ## Map 8760 hours to modeled hours - evmc_baseline_load_weatheryears.index = hmap_myr.h - ### Sum by (r,h,t) to get net trade in MWh during modeled hours - evmc_baseline_load_out = ( - evmc_baseline_load_weatheryears.stack(["r", "t"]) - .groupby(["r", "h", "t"]) - .sum() - .rename("MWh") - ## Divide by number of weather years since we concatted that number of weather years - / (len(sw['GSw_HourlyWeatherYears']) if (not periodtype.startswith('stress')) else 1) - ).reset_index() - ## Only keep modeled regions - evmc_baseline_load_out = evmc_baseline_load_out.loc[ - evmc_baseline_load_out.r.isin(val_r_all) - ].copy() - - evmc_shape_dec, evmc_shape_inc = get_yearly_flexibility( - sw=sw, - period_szn=period_szn, - rep_periods=rep_periods, - hmap_1yr=hmap_myr, - set_szn=set_szn, - inputs_case=inputs_case, - drcat="evmc_shape", - ) - - evmc_storage_dec, evmc_storage_inc, evmc_storage_energy = ( - get_yearly_flexibility( - sw=sw, - period_szn=period_szn, - rep_periods=rep_periods, - hmap_1yr=hmap_myr, - set_szn=set_szn, - inputs_case=inputs_case, - drcat="evmc_storage", - ) - ) - else: - evmc_shape_dec = pd.DataFrame(columns=["*i", "r", "h"]) - evmc_shape_inc = pd.DataFrame(columns=["*i", "r", "h"]) - evmc_storage_dec = pd.DataFrame(columns=["*i", "r", "h", "t"]) - evmc_storage_inc = pd.DataFrame(columns=["*i", "r", "h", "t"]) - evmc_storage_energy = pd.DataFrame(columns=["*i", "r", "h", "t"]) - evmc_baseline_load_out = pd.DataFrame(columns=["r", "h", "t", "MWh"]) - - - #%% Chunk the profiles - if sw.GSw_HourlyChunkAggMethod == 'mean': - aggmethod = 'mean' - args = [] - else: - if sw.GSw_HourlyChunkAggMethod == 'mid': - ## Round up: - ## For 2 hours, keep hour 2; - ## for 3 hours, keep hour 2; - ## for 4 hours, keep hour 3; etc. - keephour = int(np.ceil((GSw_HourlyChunkLength + 0.1) / 2)) - else: - keephour = int(sw.GSw_HourlyChunkAggMethod) - assert 0 < keephour <= GSw_HourlyChunkLength - aggmethod = 'nth' - ## Change from start-at-1 index to Python's start-at-0 index - args = [keephour - 1] - - if ('stress' in periodtype) and (aggmethod == 'mean'): - aggmethod_load = sw.GSw_PRM_StressLoadAggMethod - else: - aggmethod_load = aggmethod - print(f'{periodtype} load aggregation method: {aggmethod_load} {args}') - - cf_vre = ( - cf_out - .sort_values(['i','r','h']) - .assign(h=cf_out.h.map(chunkmap)) - .groupby(['i','r','h'], as_index=False) - .agg(aggmethod, *args) - ) - - load_long = ( - load_h - .stack('t') - .rename('MW') - .reorder_levels(['t','r','h']) - .sort_index() - .reset_index() - ) - load_allyear = ( - load_long - .assign(h=load_long.h.map(chunkmap)) - .groupby(['t','r','h'], as_index=False) - .agg(aggmethod_load, *args) - .set_index(['r','h','t']) - .reset_index() - ) - - - # %%################################################################################### - # -- Write outputs, aggregating hours to GSw_HourlyChunkLength if necessary -- # - ###################################################################################### - write = { - ### Contents are [dataframe, header, index] - ## h set for representative timeslices - "set_h": [hset.map(chunkmap).drop_duplicates().to_frame(), False, False], - ## szn set for representative periods - 'set_szn': [set_szn, False, False], - ## Previous hour for each h of the period - 'h_preh': [h_preh, False, False], - ## Hours to season mapping (h,szn) - "h_szn": [ - h_szn.assign(h=h_szn.h.map(chunkmap)).drop_duplicates(), - False, - False, - ], - ## 8760 hour linkage set for Augur (h,szn,year,hour) - "h_dt_szn": [ - h_dt_szn[["h", "season", "ccseason", "year", "hour"]].assign( - h=h_dt_szn.h.map(chunkmap) - ), - True, - False, - ], - ## Number of hours represented by each timeslice (h) - "numhours": [ - ( - hours.reset_index() - .assign(h=hours.index.map(chunkmap)) - .groupby("h") - .numhours.sum() - .reset_index() - .round(decimals + 3) - ), - False, - False, - ], - ## Number of times in actual year that one timeslice follows another (h,hh) - "numhours_nexth": [numhours_nexth, False, False], - ## Quarterly season weights for assigning quarter-dependent parameters (h,quarter) - "frac_h_quarter_weights": [ - ( - frac_h_weights["quarter"] - .assign(h=frac_h_weights["quarter"].h.map(chunkmap)) - .groupby(["h", "quarter"], as_index=False) - .weight.mean() - .round(decimals + 3) - ), - False, - False, - ], - ## ccseason weights for assigning ccseason-dependent parameters (h,ccseason) - "frac_h_ccseason_weights": [ - ( - frac_h_weights["ccseason"] - .assign(h=frac_h_weights["ccseason"].h.map(chunkmap)) - .groupby(["h", "ccseason"], as_index=False) - .weight.mean() - .round(decimals + 3) - ), - False, - False, - ], - ## Hydro capacity factors by szn - "cf_hyd": [cf_hyd_out.round(decimals), True, False], - ## Hydro capacity adjustment factors by szn - "cap_hyd_szn_adj": [hydcapadj_out.round(decimals + 2), True, False], - ## mapping from one timeslice to the next - "nexth": [nexth, True, True], - ## Hours to actual season mapping (h,allszn) - 'h_actualszn': [h_actualszn, False, False], - ## season to actual season mapping (szn,allszn) - 'szn_actualszn': [szn_actualszn, False, False], - ## actual season partition - 'numpartitions': [numpartitions, False, False], - ## next partition - 'nextpartition': [nextpartition, False, False], - ## mapping from one timeslice to the next for actual periods - "nexth_actualszn": [nexth_actualszn, False, False], - ## first timeslice in season (szn,h) - "h_szn_start": [szn2starth.map(chunkmap).reset_index(), False, False], - ## last timeslice in season (szn,h) - "h_szn_end": [szn2endh.map(chunkmap).reset_index(), False, False], - ## minload hour windows with overlap (h,h) - "hour_szn_group": [hour_szn_group, False, False], - ## periods in which to apply operating reserve constraints (szn) - "opres_periods": [opres_periods, False, False], - ## Season-peak demand hour for each szn's representative day (h,szn) - "h_ccseason_prm": [ - h_ccseason_prm.assign(h=h_ccseason_prm.h.map(chunkmap)), - False, - False, - ], - ## Annual timeslice demand - 'load_allyear': [load_allyear.round(decimals), False, False], - ## Seasonal peak demand - "peak_ccseason": [peak_all.round(decimals), False, False], - ## Capacity factors (i,r,h) - 'cf_vre': [cf_vre.round(5), False, False], - ## Exports to Canada [fraction] (h) - "can_exports_h_frac": [ - ( - can_exports_h_frac.assign(h=can_exports_h_frac.h.map(chunkmap)) - .groupby("h", as_index=False) - .sum() - .round(6) - ), - False, - False, - ], - ## Imports from Canada [fraction] (szn) - 'can_imports_szn_frac': [can_imports_szn_frac.round(6), False, False], - ## Outage rates - 'outage_forced_h': [outage_h['forced'].round(3), False, False], - 'outage_scheduled_h': [outage_h['scheduled'].round(3), False, False], - # DR - "dr_shed_out": [ - (dr_shed_avail_out.assign(h=dr_shed_avail_out.h.map(chunkmap)) - .groupby(['i','r','h'], as_index=False).cap.mean().round(5)), - False, False], - ## EVMC - "evmc_baseline_load": [ - ( - evmc_baseline_load_out.assign(h=evmc_baseline_load_out.h.map(chunkmap)) - .groupby(["r", "h", "t"], as_index=False) - .MWh.sum() - .round(decimals) - ), - False, - False, - ], - "evmc_shape_generation": [ - ( - evmc_shape_dec.assign(h=evmc_shape_dec.h.map(chunkmap)) - .groupby(["*i", "r", "h"]) - .mean() - .round(decimals) - .reset_index() - ), - False, - False, - ], - "evmc_shape_load": [ - ( - evmc_shape_inc.assign(h=evmc_shape_inc.h.map(chunkmap)) - .groupby(["*i", "r", "h"]) - .mean() - .round(decimals) - .reset_index() - ), - False, - False, - ], - "evmc_storage_discharge": [ - ( - evmc_storage_dec.assign(h=evmc_storage_dec.h.map(chunkmap)) - .groupby(["*i", "r", "h", "t"]) - .mean() - .round(decimals) - .reset_index() - ), - False, - False, - ], - "evmc_storage_charge": [ - ( - evmc_storage_inc.assign(h=evmc_storage_inc.h.map(chunkmap)) - .groupby(["*i", "r", "h", "t"]) - .mean() - .round(decimals) - .reset_index() - ), - False, - False, - ], - "evmc_storage_energy": [ - ( - evmc_storage_energy.assign(h=evmc_storage_energy.h.map(chunkmap)) - .groupby(["*i", "r", "h", "t"]) - .mean() - .round(decimals) - .reset_index() - ), - False, - False, - ], - ################################################################################## - ###### The next parameters are just diagnostics and are not actually used in ReEDS - ## Representative period weights for postprocessing (szn) - "period_weights": [period_weights, False, False], - ## Mapping from representative h to actual h - 'hmap_myr': [ - hmap_myr.assign(h=hmap_myr.h.map(chunkmap)), - False, False], - ## Mapping from representative h to actual h for full set of years - 'hmap_allyrs': [ - hmap_allyrs.assign(h=hmap_allyrs.h.map(chunkmap)), - False, False], - ## Mapping from representative period to actual period - "periodmap_1yr": [ - hmap_myr[["actual_period", "season"]].drop_duplicates(), - False, - False, - ], - ################################################################### - ###### The folowing parameters don't yet work for hourly resolution - ## Canada/Mexico - "canmexload": [pd.DataFrame(columns=["*r", "h"]), True, False], - ## GSw_EFS_Flex - "flex_frac_all": [ - pd.DataFrame(columns=["*flex_type", "r", "h", "t"]), - True, - False, - ], - "peak_h": [pd.DataFrame(columns=["*r", "h", "t", "MW"]), True, False], - } - - # Add climate inputs based on GSw_Climate* switch selection - if int(sw.GSw_ClimateHydro): - ## Climate-adjusted annual/seasonal nondispatchable hydropower availability - write['climate_hydadjsea'] = [climate_files['temp_hydadjsea'], True, True] - if int(sw.GSw_ClimateWater): - ## Climate-adjusted time-varying annual/seasonal water supply - write['climate_UnappWaterMult'] = [climate_files['temp_UnappWaterMult'], True, True] - ## Climate-adjusted time-varying fractional seasonal allocation of water - write['climate_UnappWaterSeaAnnDistr'] = [climate_files['temp_UnappWaterSeaAnnDistr'], True, True] - - #%% Write output csv files - for f in write: - ### Rename first column so GAMS reads it as a comment - if not write[f][1]: - write[f][0] = write[f][0].rename( - columns={write[f][0].columns[0]: "*" + str(write[f][0].columns[0])} - ) - ### If the file already exists and we're creating representative period data, - ### add a '_h17' to the filename and save it as a backup - if (debug - and os.path.isfile(os.path.join(inputs_case, f+'.csv')) - and (not periodtype.startswith('stress'))): - shutil.copy( - os.path.join(inputs_case, f + ".csv"), - os.path.join(inputs_case, f + "_h17.csv"), - ) - ### Write the new hourly parameters - write[f][0].to_csv( - os.path.join(outpath, f+'.csv'), - index=write[f][2], - ) - - #%% Map weighted average profile values and difference from full-resolution mean - if make_plots: - figpath = os.path.abspath( - os.path.join(os.path.dirname(inputs_case.rstrip(os.sep)), 'outputs', 'figures') - ) - try: - import matplotlib.pyplot as plt - import hourly_plots - ## Capacity factor and load - hourly_plots.plot_maps(sw, inputs_case, reeds_path, figpath) - ## Representative days - f, ax, _ = reeds.reedsplots.plot_repdays(os.path.dirname(os.path.abspath(inputs_case))) - plt.savefig(os.path.join(figpath, 'inputs_repdays.png')) - except Exception: - import traceback - print(traceback.format_exc()) - - return write diff --git a/input_processing/hydcf.py b/input_processing/hydcf.py deleted file mode 100644 index 0b22d6db..00000000 --- a/input_processing/hydcf.py +++ /dev/null @@ -1,469 +0,0 @@ -''' -This script calculates monthly hydro capacity factors (CFs) for each model -region and year. Historical CFs are calculated by taking the ratio of net and -max hydro generation for each region's existing hydro fleet. -Future CFs come from two sources: -1) In some cases, CFs are calculated by taking each plant's average net/max -generation across select years and calculating the ratio of total average net -and average max generation for each region's hydro fleet. -2) In other cases, capacity factors come from "hydcf_fixed.csv", -which contains pre-calculated CFs for the legacy 134 zones. -These are transformed into model region-level CFs by uniformly assigning the -zonal CFs to each legacy zone's counties and taking the average CF -across each model region's counties. -''' - -import argparse -import numpy as np -import pandas as pd -import os -import sys -import datetime -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - -def get_monthly_plant_generation(inputs_case: str) -> ( - tuple[pd.DataFrame, pd.DataFrame] -): - """ - Get monthly net generation and maximum generation in MWh for - each hydro plant. Net generation values are read from - inputs_case/net_gen_existing_hydro.csv, while maximum generation values - are derived from annual capcities (inputs_case/cap_existing_hydro.csv) - by calculating monthly generation assuming 100% capacity factor. - - Args: - inputs_case: Path to the inputs case directory. - - Returns: - tuple[pd.DataFrame, pd.DataFrame] - """ - # Read inputs - annual_plant_capacities = pd.read_csv( - os.path.join(inputs_case, 'cap_existing_hydro.csv'), - index_col='t' - ) - monthly_plant_net_generation = pd.read_csv( - os.path.join(inputs_case, 'net_gen_existing_hydro.csv'), - index_col=['t', 'month'] - ) - # Expand annual capacity data to monthly - monthly_plant_capacities = annual_plant_capacities.reindex( - monthly_plant_net_generation.index, - level=0 - ) - # Assign number of hours to each month - monthly_plant_capacities['date'] = pd.to_datetime( - ( - monthly_plant_capacities.index.get_level_values('t').astype(str) - + '-' - + monthly_plant_capacities.index.get_level_values('month') - ), - format='%Y-%b' - ) - monthly_plant_capacities['num_hours'] = ( - monthly_plant_capacities['date'].dt.daysinmonth * 24 - ) - # Multiply monthly capacities by number of hours in each month - monthly_plant_max_generation = ( - monthly_plant_capacities.drop(columns=['date', 'num_hours']) - .mul(monthly_plant_capacities['num_hours'], axis=0) - ) - # Align null values across datasets - monthly_plant_max_generation[monthly_plant_net_generation.isna()] = np.nan - monthly_plant_net_generation[monthly_plant_max_generation.isna()] = np.nan - - return monthly_plant_net_generation, monthly_plant_max_generation - - -def calculate_regional_generation( - plant_generation: pd.DataFrame, - hydro_plants: pd.DataFrame -) -> pd.DataFrame: - """ - Calculate total generation for each model region in MWh. - - Args: - plant_generation: Plant-level generation in MWh. - hydro_plants: Tech and region information for each hydro plant. - - Returns: - pd.DataFrame - """ - # Reformat plant generation data and append tech and region information - index_cols = list(plant_generation.index.names) - plant_generation = pd.melt( - plant_generation.reset_index(), - id_vars=index_cols, - var_name='EIA_PlantID' - ) - plant_generation = ( - plant_generation.merge( - hydro_plants, - left_on=['EIA_PlantID'], - right_index=True - ) - .rename(columns={'tech': '*i'}) - ) - # Group by tech and region and calculate total generation - groupby_cols = index_cols + ['*i', 'r'] - regional_generation = plant_generation.groupby(groupby_cols).sum() - regional_generation = ( - pd.pivot_table( - regional_generation, - index=['*i'] + index_cols, - columns=['r'], - values=['value'] - ) - .droplevel(level=0, axis=1) - .rename_axis(columns=['']) - ) - - return regional_generation - - -def calculate_historical_monthly_regional_cf( - monthly_plant_net_generation: pd.DataFrame, - monthly_plant_max_generation: pd.DataFrame, - hydro_plants: pd.DataFrame, - inputs_case: str -) -> pd.DataFrame: - """ - Calculate monthly CFs for each model region in historical years. - In historical years, CFs are calculated by aggregating plant-level - generation to the region level and taking the ratio of each region's - total net generation and total max generation. - - Args: - monthly_plant_net_generation: Monthly plant net generation in MWh. - monthly_plant_max_generation: Monthly plant max generation in MWh. - hydro_plants: Tech and region information for each hydro plant. - inputs_case: Path to the inputs case directory. - - Returns: - pd.DataFrame - """ - # Calculate monthly net and max generation for each model region - monthly_regional_net_generation = calculate_regional_generation( - monthly_plant_net_generation, - hydro_plants - ) - monthly_regional_max_generation = calculate_regional_generation( - monthly_plant_max_generation, - hydro_plants - ) - # Calculate monthly CFs for each model region - monthly_regional_cf = ( - monthly_regional_net_generation.div( - monthly_regional_max_generation.replace(0, np.nan) - ) - .rename_axis(columns=['r']) - .reorder_levels(order=['t', '*i', 'month']) - ) - # Downselect to model years - sw = reeds.io.get_switches(inputs_case) - startyear = int(sw.startyear) - monthly_regional_cf = monthly_regional_cf.loc[( - monthly_regional_cf.index.get_level_values('t') >= startyear - )] - - return monthly_regional_cf - - -def calculate_regional_average_generation( - monthly_plant_generation: pd.DataFrame, - hydro_plants: pd.DataFrame, - future_hydcf_rep_years: list[int] -) -> pd.DataFrame: - """ - Calculate average generation across years for each plant - in each month and then aggregate to the model region level. - - Args: - monthly_plant_generation: Monthly plant-level generation in MWh. - hydro_plants: Tech and region information for each hydro plant. - future_hydcf_rep_years: Set of years from which to calculate - future hydro CFs. - - Returns: - pd.DataFrame - """ - # Subset generation data to years representing future hydro - monthly_plant_generation = monthly_plant_generation.loc[( - monthly_plant_generation.index - .get_level_values('t') - .isin(future_hydcf_rep_years) - )] - # Calculate average generation across years for each plant in each month - plant_average_generation = ( - monthly_plant_generation.groupby(level='month') - .mean() - ) - # Aggregate average plant-level generation to the model region level - regional_average_generation = calculate_regional_generation( - plant_average_generation, - hydro_plants - ) - - return regional_average_generation - - -def calculate_future_monthly_regional_cf( - monthly_plant_net_generation: pd.DataFrame, - monthly_plant_max_generation: pd.DataFrame, - hydro_plants: pd.DataFrame, - inputs_case: str, -): - """ - Calculate monthly CFs for each model region in future years. - Future CFs come from two sources: - 1) In some cases, CFs are calculated by taking each plant's average net/max - generation across select years (based on the GSw_FutureHydCF_RepYears - switch) and calculating the ratio of total average - net and average max generation for each region's hydro fleet. - 2) In other cases, capacity factors come from inputs_case/hydcf_fixed.csv, - which contains pre-calculated CFs for the legacy 134 zones. - These are transformed into model region-level CFs by uniformly - assigning the zonal CFs to each legacy zone's counties and taking - the average CF across each model region's counties. - - In cases where data for a given time and region exist in both sources, - the first source (i.e., plant-level data) takes precedence. - - Args: - monthly_plant_net_generation: Monthly plant net generation in MWh. - monthly_plant_max_generation: Monthly plant max generation in MWh. - hydro_plants: Tech and region information for each hydro plant. - inputs_case: Path to the inputs case directory. - - Returns: - pd.DataFrame - """ - # Get the set of years that represents future hydro - sw = reeds.io.get_switches(inputs_case) - future_hydcf_rep_years = sw['future_hydcf_rep_years_list'] - # Calculate average net and max generation for each plant - # and aggregate to the model region level - regional_average_net_generation = calculate_regional_average_generation( - monthly_plant_net_generation, - hydro_plants, - future_hydcf_rep_years - ) - regional_average_max_generation = calculate_regional_average_generation( - monthly_plant_max_generation, - hydro_plants, - future_hydcf_rep_years - ) - # Calculate monthly CFs for each model region - future_cf_existing_techs = regional_average_net_generation.div( - regional_average_max_generation.replace(0, np.nan) - ) - # Duplicate monthly CF data for existing techs to derive CFs for - # upgrade techs, re-assigning "ED/END" hydro categories to "UD/UND" - upgrade_dict = {"hydED": "hydUD", "hydEND": "hydUND"} - future_cf_upgrade_techs = ( - future_cf_existing_techs.loc[( - future_cf_existing_techs.index - .get_level_values('*i') - .isin(upgrade_dict.keys()) - )] - .reset_index() - .replace(upgrade_dict) - .set_index(['*i', 'month']) - ) - # Read pre-calculated fixed CFs and reformat - future_cf_fixed = pd.read_csv( - os.path.join(inputs_case, 'hydcf_fixed.csv') - ) - future_cf_fixed = future_cf_fixed.pivot_table( - index=['*i', 'month'], - columns='r', - values='value' - ) - ## Concatenate all future CFs - # Note that we don't simply call pd.concat because the component dataframes - # are not guaranteed to be mutually exclusive (i.e., we may have both fixed - # CFs and CFs derived from plant data for a given region and tech), so - # pd.concat could result in duplicate indices with different values. - # Instead, we use the concatenation operation below, which is structured so - # that the CFs calculated from plant data are prioritized over the fixed - # CFs in cases of duplicate indices. - future_cf_columns = ( - future_cf_fixed.columns - .union(future_cf_existing_techs.columns) - .union(future_cf_upgrade_techs.columns) - ) - future_cf_index = ( - future_cf_fixed.index - .union(future_cf_existing_techs.index) - .union(future_cf_upgrade_techs.index) - ) - future_cf = pd.DataFrame( - columns=future_cf_columns, - index=future_cf_index - ) - future_cf.update(future_cf_fixed) - future_cf.update(future_cf_existing_techs) - future_cf.update(future_cf_upgrade_techs) - - return future_cf - - -def get_hydro_plants(inputs_case: str) -> pd.DataFrame: - """ - Reads the EIA plant database from inputs_case/unitdata.csv and - filters down to hydro plants (plants whose tech starts with "hyd"). - - Args: - inputs_case: Path to the inputs case directory. - - Returns: - pd.DataFrame - """ - # Get county-to-region mapping - county2zone = reeds.io.get_county2zone(os.path.dirname(inputs_case)) - county2zone.index = 'p' + county2zone.index - # Get plant database and filter down to hydro plants - gendb = pd.read_csv( - os.path.join(inputs_case, 'unitdata.csv'), - usecols=['T_PID', 'tech', 'FIPS'] - ) - hydro_plants = ( - gendb.loc[gendb.tech.str.startswith('hyd')] - .drop_duplicates('T_PID') - .set_index('T_PID') - ) - # Assign each plant to a model region and reformat - hydro_plants['r'] = hydro_plants['FIPS'].map(county2zone) - hydro_plants = hydro_plants.drop(columns='FIPS') - hydro_plants.index = hydro_plants.index.astype(str) - - return hydro_plants - - -def assemble_hydcf( - historical_monthly_regional_cf: pd.DataFrame, - future_monthly_regional_cf: pd.DataFrame, - inputs_case: str -) -> pd.DataFrame: - """ - Combines monthly historical and future hydro CF data, - forward-filling the future data up to the ReEDS model end year. - - Args: - historical_monthly_regional_cf: Monthly regional CFs - in historical years. - future_monthly_regional_cf: Monthly regional CFs - in an unspecified future year. These CFs are duplicated - across years from the end of the historical period - to the model end year. - inputs_case: Path to the inputs case directory. - - Returns: - pd.DataFrame - """ - # Assign a year to the future CFs corresponding to the - # year after the final year of historical CFs - historical_endyear = ( - historical_monthly_regional_cf.index.get_level_values('t').max() - ) - future_monthly_regional_cf = ( - future_monthly_regional_cf.assign(t=historical_endyear+1) - .set_index('t', append=True) - .reorder_levels(historical_monthly_regional_cf.index.names) - ) - # Concatenate historical and future CFs - hydcf = pd.concat([ - historical_monthly_regional_cf, - future_monthly_regional_cf - ]) - # Reformat so that hydcf is indexed by year and - # has column levels for tech, month, and region - hydcf = ( - hydcf.stack() - .rename_axis(['t','*i','month','r']) - .rename('value') - .to_frame() - .reset_index() - .pivot_table(index='t', columns=['*i','month','r'], values='value') - ) - # Forward-fill years up to model end year - sw = reeds.io.get_switches(inputs_case) - model_endyear = int(sw.endyear) - data_endyear = hydcf.index.max() - reindex = ( - hydcf.index.tolist() - + np.arange(data_endyear+1, model_endyear+1).tolist() - ) - hydcf = hydcf.reindex(reindex) - hydcf.loc[data_endyear:] = hydcf.loc[data_endyear:].ffill() - # Convert from "wide" to "long" format - hydcf = hydcf.stack(['*i', 'month']).stack().rename('value').to_frame() - - return hydcf - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== -def main(reeds_path, inputs_case): - print('Starting hydcf.py') - - monthly_plant_net_generation, monthly_plant_max_generation = ( - get_monthly_plant_generation(inputs_case) - ) - hydro_plants = get_hydro_plants(inputs_case) - historical_monthly_regional_cf = calculate_historical_monthly_regional_cf( - monthly_plant_net_generation, - monthly_plant_max_generation, - hydro_plants, - inputs_case - ) - future_monthly_regional_cf = calculate_future_monthly_regional_cf( - monthly_plant_net_generation, - monthly_plant_max_generation, - hydro_plants, - inputs_case - ) - hydcf = assemble_hydcf( - historical_monthly_regional_cf, - future_monthly_regional_cf, - inputs_case - ) - hydcf.to_csv(os.path.join(inputs_case, 'hydcf.csv')) - - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if __name__ == '__main__': - # Time the operation of this script - tic = datetime.datetime.now() - - ### Parse arguments - parser = argparse.ArgumentParser( - description='Process hydro capacity factors', - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument('reeds_path', help='ReEDS-2.0 directory') - parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - #%% Run it - main(reeds_path=reeds_path, inputs_case=inputs_case) - - reeds.log.toc(tic=tic, year=0, process='input_processing/hydcf.py', - path=os.path.join(inputs_case,'..')) - - print('Finished hydcf.py') \ No newline at end of file diff --git a/input_processing/mcs_sampler.py b/input_processing/mcs_sampler.py deleted file mode 100644 index 8dd3e514..00000000 --- a/input_processing/mcs_sampler.py +++ /dev/null @@ -1,1648 +0,0 @@ -""" -This module performs the Monte Carlo sampling for ReEDS. -""" - - -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== -import os -import sys -import numpy as np -import pandas as pd -import copy -import argparse -import yaml -import datetime -from typing import Tuple, List -from collections import defaultdict - -# Local Imports -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -from input_processing import copy_files - - -#%% =========================================================================== -### --- CONSTANTS --- -### =========================================================================== -class MCSConstants: - """ - Configuration constants for the Monte Carlo Sampling (MCS) process in ReEDS. - Contains synonyms, file names for special treatment, and valid distribution identifiers. - """ - ### --- Synonyms - TECH_DESCRIPTOR = ['i', 'type', 'Tech', 'Geo class', 'Depth', 'Turbine', 'tech', '*tech', 'class'] - YEAR_SYNONYMS = ['t', 'Year', 'year'] - REGION_SYNONYMS = ['r', 'region', 'cendiv', 'sc_point_gid', 'FIPS'] - - ### --- Fixed columns that should not be modified in most cases - OTHER_INDICES = ['columns', 'p', '*p'] # 'p' is used in h2_exog_cap.csv - NONMODIFIABLE_FINANCIAL_COLUMNS = ['debt_fraction', 'tax_rate'] - FIXED_COLUMN_NAMES = YEAR_SYNONYMS + TECH_DESCRIPTOR + OTHER_INDICES + NONMODIFIABLE_FINANCIAL_COLUMNS + REGION_SYNONYMS - - ### --- Files that require special treatment - SUPPLY_CURVE_FILES = [ - "supplycurve_upv.csv", - "supplycurve_wind-ofs.csv", - "supplycurve_wind-ons.csv", - ] - EXOG_CAP_FILES = ["exog_cap_upv.csv", "exog_cap_wind-ons.csv"] - PRESCRIBED_BUILDS_FILES = ["prescribed_builds_wind-ofs.csv", "prescribed_builds_wind-ons.csv"] - RECF_FILES = ["recf_wind-ons.h5", "recf_wind-ofs.h5", "recf_upv.h5"] - - ### --- Switch-File(s) combinations hardcoded in copy_files.py - # These files are explicitly handled in copy_files.py, bypassing the standard - # runfiles.csv instructions. In these cases, the switch is often used as a filter - # to select specific rows or columns within the file. - HARD_CODED_SWITCH_TO_FILE_READ = { - 'GSw_H2_Demand_Case': ["h2_exogenous_demand.csv"], - } - - SITING_SWITCHES = ["GSw_SitingUPV", "GSw_SitingWindOfs", "GSw_SitingWindOns"] - - ### --- Valid distributions - VALID_DISTRIBUTIONS = ["dirichlet", "discrete", "uniform_multiplier", "triangular_multiplier"] - MULTIPLICATIVE_DISTRIBUTIONS = ["uniform_multiplier", "triangular_multiplier"] - - -#%% =========================================================================== -### --- Auxiliary functions --- -### =========================================================================== -def max_decimal_places(data, columns: list = None) -> dict: - """ - Calculate the maximum number of decimal places in a single number, specific columns, or all columns of a DataFrame. - - Args: - data (pd.DataFrame or numeric): The input DataFrame or a single numeric value (float or int). - columns (list or None): List of column names to analyze if data is a DataFrame. If None, all columns will be analyzed. - - Returns: - int or dict: - - If data is a single number, returns the number of decimal places in the number. - - If data is a DataFrame, returns a dictionary with column names as keys and their respective maximum number of decimal places as values. - """ - # Function to count the number of decimals in a single number - def count_decimals(data): - if isinstance(data, (float, int, str)) and '.' in str(data): - return len(str(data).split('.')[1]) - else: - return 0 - - # If we have a single number or a list of numbers - if not isinstance(data, pd.DataFrame): - if isinstance(data, (list, np.ndarray)): - return max([count_decimals(val) for val in data]) - else: - return count_decimals(data) - - else: - # If columns is None, analyze all columns - if columns is None: - columns = data.keys() - - # Compute the maximum number of decimal places for each specified column - return {col: data[col].apply(count_decimals).max() for col in columns} - - -def read_exception_file(sw_assignment: str, file_name: str, file_path: str) -> pd.DataFrame: - """ - Handles exceptions for files that are hardcoded in copy_files.py - and written directly without using runfiles.csv. - - This function allows you to manually support special cases where - a switch-file combination is not automatically handled by the MCS module. - If you encounter a new unsupported case, you can add it here. - - Args: - sw_assignment (str): The switch assignment. - file_name (str): Name of the file (output filename). - file_path (str): Path to the reference file (inputs folder). - - Returns: - pd.DataFrame: A DataFrame formatted as expected before being written - to the inputs_case folder (as in copy_files.py). - """ - if file_name == 'h2_exogenous_demand.csv': - # h2_exogenous_demand.csv has a path in runfiles.csv (considered a non-region file) - df = pd.read_csv(file_path, index_col=['p', 't']) - df = df[sw_assignment].round(3).rename_axis(['*p', 't']).reset_index() - - # Rename the value column to 'million_tons' to avoid issues in writecapdat.py - df.rename(columns={sw_assignment: 'million_tons'}, inplace=True) - return df - - return None - - -def read_csv_h5_file(sw_runfiles_csv, aux_files, reeds_path, inputs_case) -> pd.DataFrame: - """ - This function reads a csv or h5 file based on a row of runfiles.csv and returns a dataframe with the data - in the ReEDS format. - - Args: - sw_runfiles_csv (pd.Series): A row of runfiles.csv with sw preassigned to the filepath. - aux_files (dict): A dictionary with auxiliary information for the copy_files.py module. - reeds_path (str): The path to the ReEDS directory. - inputs_case (str): The path to the inputs case directory. - - Returns: - pd.DataFrame: A DataFrame with the data in the ReEDS format. - """ - # Obtain the data used by copy_files.py to filter regions and create tailored dataframes - nonregion_files = aux_files['nonregion_files'] - region_files = aux_files['region_files'] - file_name = sw_runfiles_csv['filename'] - file_path = os.path.join(reeds_path, sw_runfiles_csv['full_filepath']) - - # Try to read the file using the read_exception_file function first - df = read_exception_file(sw_runfiles_csv['sw_assignment'], file_name, file_path) - if df is not None: - return df - - if file_name in region_files['filename'].values: - # Regional file (works for both csv and h5) - df = copy_files.subset_to_valid_regions( - aux_files['sw'], - sw_runfiles_csv, - aux_files['agglevel_variables'], - aux_files['regions_and_agglevel'], - inputs_case, - agg=False, - ) - - elif file_name in nonregion_files['filename'].values: - files_not_supported = ['scalars.csv'] - if file_path.endswith('.csv') and file_name not in files_not_supported: - #Read the csv file - df = pd.read_csv(file_path) - else: - #File not implemented yet - error_message = 'The file %s has not been implemented yet' % sw_runfiles_csv['filename'] - raise ValueError(error_message + ' improve function read_csv_h5_file') - - elif file_name in ['switches.csv']: - df = pd.read_csv(os.path.join(inputs_case, file_name), - header = None, index_col=0, dtype=str) - - else: - error_message = ( - f"The file '{file_name}' is not classified under nonregion_files or region_files, " - "and it is not currently handled by the read_exception_file function. " - "If you want to use this switch-file combination in MCS, please update the read_exception_file function " - "and add an entry to MCSConstants.HARD_CODED_SWITCH_TO_FILE_READ." - ) - raise ValueError(error_message) - - return df - - -def get_hierarchy_file(inputs_case: str, ReEDS_resolution: str) -> pd.DataFrame: - """ - The hierarchy file in `{inputs_case}/hierarchy.csv` does not contain a - differentiation between "ba" and "aggreg" resolution. This function - reconstructs the hierarchy file with all possible combinations relevant - to the MCS. - - Args: - inputs_case (str): Path to the inputs case directory. - ReEDS_resolution (str): The spatial resolution used in ReEDS (e.g., 'ba', 'aggreg'). - - Returns: - pd.DataFrame: A DataFrame with the hierarchy information relevant to the regions - considered in the inputs_casse run. - """ - original_hierarchy_file = pd.read_csv( - os.path.join(inputs_case, "hierarchy_original.csv") - ) - - valid_regions = pd.read_csv( - os.path.join(inputs_case, "hierarchy.csv") - )['*r'].values - - filtered_hierarchy = original_hierarchy_file[ - original_hierarchy_file[ReEDS_resolution].isin(valid_regions) - ].reset_index(drop=True) - - return filtered_hierarchy - - -#%% =========================================================================== -### --- FILE PATHS & DISTRIBUTION INSTRUCTIONS --- -### =========================================================================== -def mcs_find_copy_paths( - sw_name: str, - sw_assignments: list, - runfiles: pd.DataFrame, - reeds_path: str, - inputs_case: str, - run_ReEDS: bool = True -) -> Tuple[list, pd.DataFrame]: - """ - Find the paths where the MCS samples should be copied to and the associated runfiles.csv rows. - - Args: - sw_name (str): The name of the switch being sampled. - sw_assignments (list): The assignments for the switch. - runfiles (pd.DataFrame): The runfiles.csv DataFrame. - reeds_path (str): The path to the ReEDS directory. - inputs_case (str): The path to the inputs case directory. - run_ReEDS (bool): Whether to run the ReEDS model or not. - - Returns: - save_path_list: A list of destination paths for the MCS samples. - runfile_instructions: The runfiles.csv rows associated with the switch. - """ - # Find if the switch name needs to be assigned to a specific file path in runfiles.csv - rf_contains_sw = runfiles['filepath'].fillna('').str.contains('{' + sw_name + '}') - if any(rf_contains_sw): - runfile_instructions = runfiles[rf_contains_sw].reset_index(drop=True) - elif sw_name in MCSConstants.HARD_CODED_SWITCH_TO_FILE_READ: - # If the switch name is found in the hardcoded exceptions, fid the rows - # in runfiles.csv that contain all the files associated with the switch. - runfile_instructions = runfiles[ - runfiles['filename'].isin(MCSConstants.HARD_CODED_SWITCH_TO_FILE_READ[sw_name]) - ].reset_index(drop=True) - else: - # If the switch name is not found in runfiles.csv, or in the hardcoded exceptions, - # assume it is only part of switches.csv - runfile_instructions = runfiles[runfiles['filename'] == 'switches.csv'].reset_index(drop=True) - - # Reorder rows: if any filename has "supply_curve", place those rows first. - # For siting data we need to sample the supply curve data first - # (CF,... is dependent on the supply curve data) - if runfile_instructions['filename'].str.contains('supply_curve', na=False).any(): - supply_curve_rows = runfile_instructions[runfile_instructions['filename'].str.contains('supply_curve', na=False)] - other_rows = runfile_instructions[~runfile_instructions['filename'].str.contains('supply_curve', na=False)] - runfile_instructions = pd.concat([supply_curve_rows, other_rows], ignore_index=True) - - # Iterate through each instruction to determine the destination paths. - # Since some switches point to multiple files, you can have multiple destination paths. - save_path_list = [] - for _, row in runfile_instructions.iterrows(): - file_name = row['filename'] - - if run_ReEDS: - dest_path = os.path.join(inputs_case, file_name) - else: - # Save samples at runs/Sample_ for later use. Useful for checking the samples. - dest_path = os.path.join( - reeds_path, 'runs', 'Sample_{sample_n}', file_name - ) - save_path_list.append(dest_path) - - # Supply curve files are used in other distributions, so they need to be first in the list. - # This should be cleaned up. - if any([os.path.basename(i).startswith('supplycurve') for i in save_path_list]): - supplycurve_index = [ - i for (i,f) in enumerate(save_path_list) - if os.path.basename(f).startswith('supplycurve') - ][0] - other_indices = [i for i in range(len(save_path_list)) if i != supplycurve_index] - index_order = [supplycurve_index] + other_indices - save_path_list = [save_path_list[i] for i in index_order] - runfile_instructions = runfile_instructions.loc[index_order].reset_index(drop=True) - - return save_path_list, runfile_instructions - - -def general_mcs_dist_validation(reeds_path: str, mcs_dist_path: str, sw: pd.Series) -> None: - """ - Validate the contents of mcs_distributions_{MCS_dist}.yaml used for Monte Carlo sampling. - - Args: - reeds_path (str): Path to the ReEDS directory. - mcs_dist_path (str): Path to the input .yaml file. - sw (pd.Series): Case switches - - Raises: - ValueError: If any structure or content in the .yaml file is invalid. - """ - print('Validating the input distribution information for Monte Carlo sampling...') - - with open(mcs_dist_path, 'r') as f: - data = yaml.safe_load(f) - df_input_dist = pd.DataFrame(data) - - mcs_dist_groups = sw['MCS_dist_groups'].split('.') - - # Read cases.csv to get the list of valid switches. - cases_default = pd.read_csv(os.path.join(reeds_path, 'cases.csv')) - valid_switches = cases_default.iloc[:, 0].values - - # Validate mandatory keys in df_input_dist - required_keys = {'name', 'assignments_list', 'dist', 'dist_params', 'weight_r'} - missing_keys = required_keys - set(df_input_dist.columns) - if missing_keys: - raise ValueError(f"Missing mandatory keys in mcs_distributions.yaml object: {missing_keys}") - - # Make sure that dist_params is a list - if not all(isinstance(df_input_dist.at[i, 'dist_params'], list) for i in range(len(df_input_dist))): - raise ValueError('The dist_params field must be a list') - - # Verify that all dist group names in mcs_distributions.yaml are unique. - if df_input_dist['name'].nunique() != len(df_input_dist): - raise ValueError('The distribution names in mcs_distributions.yaml are not unique. Please correct the file') - - # Ensure that we are not missing data for each row of the input distribution file. - missing_data = df_input_dist.isnull().sum(axis=1) - if missing_data.any(): - raise ValueError(f"The following dist names have missing data: {df_input_dist.loc[missing_data > 0, 'name'].values}. " - "Make sure you have all mandatory fields in the input distribution file") - - # Ignore all cases not in mcs_dist_groups - df_input_dist = df_input_dist[df_input_dist['name'].isin(mcs_dist_groups)].reset_index(drop=True) - - # Ensure all MCS_dist_groups options are present in the input distribution names. - missing = set(mcs_dist_groups) - set(df_input_dist['name'].unique()) - if missing: - raise ValueError(f"The following MCS_dist_groups switch options are missing in mcs_distributions.yaml {missing}") - - for i, sample_group in df_input_dist.iterrows(): - distribution = sample_group['dist'] - - switch_names = [next(iter(s)) for s in sample_group["assignments_list"]] - sw_assignments = [next(iter(s.values())) for s in sample_group["assignments_list"]] - - for d in sample_group["assignments_list"]: - if not (isinstance(d, dict) and len(d) == 1): - raise ValueError("Each item in assignments_list must be a single-key dictionary") - - val = next(iter(d.values())) - if not isinstance(val, list): - raise ValueError("The value in each dictionary must be a list") - - if distribution not in ["dirichlet", "discrete"] and any( - switch in MCSConstants.SITING_SWITCHES for switch in switch_names - ): - raise ValueError( - "The siting related switches can only be sampled " - "using a dirichlet or discrete distribution" - ) - - if distribution not in MCSConstants.VALID_DISTRIBUTIONS: - raise ValueError( - f"The distribution {distribution} is not supported." - f"Please choose one of the following: {MCSConstants.VALID_DISTRIBUTIONS}") - - if distribution in MCSConstants.MULTIPLICATIVE_DISTRIBUTIONS: - num_files = np.max([len(c) for c in sw_assignments]) - if num_files > 1: - raise ValueError( - f"The distribution {distribution} can only have a single reference file/value per switch." - ) - - # Iterate over each switch in the instruction. - for sw_name in switch_names: - # Check if the switch is valid. - if sw_name not in valid_switches: - raise ValueError(f'The switch {sw_name} is not a valid switch. Please check cases.csv') - - -def get_dist_instructions(reeds_path: str, inputs_case: str, run_ReEDS: bool = True) -> Tuple[pd.DataFrame, dict]: - """ - Obtain the instructions to sample the distributions for each switch - and organize information to facilitate the Monte Carlo sampling process. - - Args: - reeds_path (str): The path to the ReEDS directory. - inputs_case (str): The path to the inputs case directory. - run_ReEDS (bool): Whether to run the ReEDS model or not. - - Returns: - df_input_dist_ex: A DataFrame with the distribution instructions for each switch. - aux_files: A dictionary with auxiliary information (mostly used in the copy_files.py module). - """ - print('Reading the input distribution information for Monte Carlo sampling') - - # Read yaml file with the input distribution information. - mcs_dist_path = os.path.join(inputs_case, 'mcs_distributions.yaml') - with open(mcs_dist_path, 'r') as f: - data = yaml.safe_load(f) - df_input_dist = pd.DataFrame(data) - - sw = reeds.io.get_switches(inputs_case) - mcs_dist_groups = sw['MCS_dist_groups'].split('.') - - if not run_ReEDS: - # Since you did not run using runbatch.py - check inputs here - general_mcs_dist_validation(reeds_path, mcs_dist_path, sw) - - # Ignore all cases not in mcs_dist_groups - df_input_dist = df_input_dist[df_input_dist['name'].isin(mcs_dist_groups)].reset_index(drop=True) - - # Expand df_input_dist with new information to facilitate the Monte Carlo sampling process. - # Sample ID here is used to uniquely identify each sample-process. - df_input_dist_ex = df_input_dist.copy(deep=True) - for col in ['Sample_ID', 'switch_names', 'sw_assignments', 'file_names', 'save_paths', 'runfiles_csv']: - df_input_dist_ex[col] = [[] for _ in range(len(df_input_dist))] - - # Save reeds_path and inputs_case for future use. - df_input_dist_ex['reeds_path'] = reeds_path - df_input_dist_ex['inputs_case'] = inputs_case - - agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) - # Read runfiles.csv to get instructions on how files must be copied. - runfiles, nonregion_files, region_files = copy_files.read_runfiles( - reeds_path, inputs_case, sw, agglevel_variables) - - ReEDS_resolution = sw['GSw_RegionResolution'] - # Process each distribution instruction. - for i, input_dist_row in df_input_dist.iterrows(): - - # If ReEDS_resolution is aggreg but weight_r is 'ba' change it to aggreg - if ReEDS_resolution == 'aggreg' and input_dist_row['weight_r'] == 'ba': - df_input_dist_ex.at[i, 'weight_r'] = 'aggreg' - print(f"[Warning]: The weight_r for {input_dist_row['name']} was changed to 'aggreg'") - - # Iterate over each switch in the instruction. - for sw_i, assignments_list in enumerate(input_dist_row['assignments_list']): - - sw_name, sw_assignments = next(iter(assignments_list.items())) - - filepaths, runfiles_csv = mcs_find_copy_paths( - sw_name, sw_assignments, runfiles, reeds_path, inputs_case, run_ReEDS=run_ReEDS - ) - - # handle cases where a single switch assignment is associated with multiple files and cases - # related to switches.csv, where multiple float switches may be associated with the same file. - for j in range(len(filepaths)): - file_name = runfiles_csv.iloc[j]['filename'] - df_input_dist_ex.at[i, 'switch_names'].append(sw_name) - df_input_dist_ex.at[i, 'sw_assignments'].append(sw_assignments) - df_input_dist_ex.at[i, 'save_paths'].append(filepaths[j]) - df_input_dist_ex.at[i, 'runfiles_csv'].append(runfiles_csv.iloc[j]) - df_input_dist_ex.at[i, 'file_names'].append(runfiles_csv.iloc[j]['filename']) - - if file_name != 'switches.csv': - df_input_dist_ex.at[i, 'Sample_ID'].append(f'{file_name}') - else: - df_input_dist_ex.at[i, 'Sample_ID'].append(f'{sw_name}') - - # Obtain the data used by copy_files.py to filter regions and create tailored dataframes. - regions_and_agglevel = copy_files.get_regions_and_agglevel( - reeds_path, inputs_case, save_regions_and_agglevel=False) - - source_deflator_map = copy_files.get_source_deflator_map(reeds_path) - - hierarchy_file = get_hierarchy_file(inputs_case, sw['GSw_RegionResolution']) - - # Save the auxiliary info in a dictionary. - aux_files = { - 'sw': sw, - 'nonregion_files': nonregion_files, - 'region_files': region_files, - 'source_deflator_map': source_deflator_map, - 'regions_and_agglevel': regions_and_agglevel, - 'agglevel_variables': agglevel_variables, - 'hierarchy_file': hierarchy_file, - } - - return df_input_dist_ex, aux_files - - -#%% =========================================================================== -### --- WEIGHT CALCULATION --- -### =========================================================================== -def get_region_weights(distribution: str, dist_params: list, n_samples: int = 1) -> np.ndarray: - """ - Generate weights for a single region based on the assigned distribution. - - Args: - distribution (str): The distribution to use for sampling. - dist_params (list): The parameters for the distribution. - n_samples (int): The number of samples to generate. - - Returns: - np.ndarray: The weights for the region-based sample ([n_samples, n_ref_files|values]). - """ - if distribution == "dirichlet": - r_weights = np.random.dirichlet(dist_params, n_samples) - - elif distribution == "discrete": - prob = np.array(dist_params) / np.sum(dist_params) - sampled_indices = np.random.choice(len(dist_params), n_samples, p=prob) - r_weights = np.zeros((n_samples, len(dist_params)), dtype=int) - r_weights[np.arange(n_samples), sampled_indices] = 1 - - elif distribution == "uniform_multiplier": - r_weights = np.random.uniform(dist_params[0], dist_params[1], n_samples) - - elif distribution == "triangular_multiplier": - r_weights = np.random.triangular(dist_params[0], dist_params[1], dist_params[2], n_samples) - - # Make sure r_weights is a 2D array - if r_weights.ndim == 1: - r_weights = r_weights[:, np.newaxis] - - return r_weights - - -def get_all_region_weights( - distribution: str, - dist_params: list, - hierarchy_file: pd.DataFrame, - sample_hierarchy_lvl: str = 'country', -) -> dict: - """ - Get the weights for all unique regions in sample_hierarchy_lvl and map them to the - relevant BAs and cendivs, levels. Those may be adjusted later for supply curve files - (in this case they may be combined with capacity data) - - Args: - distribution (str): The distribution to use for sampling. - dist_params (list): The parameters for the distribution. - hierarchy_file (pd.DataFrame): DataFrame with the hierarchy information from get_hierarchy_file (.) - sample_hierarchy_lvl (str): The hierarchy level which will be assigned unique weights. - - Returns: - dict: Dictionary with the weights for each region. - """ - - # Only needs to map weights to 'ba', and 'cendiv' - # levels since these are the only levels relevant to the files changed in the mcs sampling - all_r_weights = {} - unique_sample_levels = hierarchy_file[sample_hierarchy_lvl].unique() - - for region in unique_sample_levels: - # Generate region weights based on the specified distribution - r_weights = get_region_weights(distribution, dist_params) - - # Retrieve all BAs linked to the current region - bas = hierarchy_file.loc[hierarchy_file[sample_hierarchy_lvl] == region, "ba"].values - - # Assign weights to each BA, cendiv, and aggreg - for ba in bas: - all_r_weights[ba] = r_weights - - # Only save the cendiv weights if the sample_hierarchy_lvl is 'country' or 'cendiv' - if sample_hierarchy_lvl in ["country", "cendiv"]: - cendivs = hierarchy_file.loc[hierarchy_file[sample_hierarchy_lvl] == region, "cendiv"].unique() - for cendiv in cendivs: - all_r_weights[cendiv] = r_weights - - return all_r_weights - - -class WeightCalculator: - """ - Computes region-based weights for Monte Carlo Sampling in ReEDS. - - Args: - sample_group (pd.Series): a series with information about the sample group - from get_dist_instructions(.). - This contains the distribution, dist_params, switch_names, sw_assignments, file_names, save_paths, ... - It is a row of the df_input_dist_ex DataFrame. - aux_files (dict): Dictionary with auxiliary information - from get_dist_instructions (.) - n_samples (int): The number of samples - """ - def __init__( - self, - sample_group: pd.Series, - aux_files: dict, - n_samples: int = 1, - ): - self.sample_group = sample_group - self.aux_files = aux_files - self.distribution = sample_group['dist'] - self.dist_params = sample_group['dist_params'] - self.sample_hierarchy_lvl = sample_group['weight_r'].lower() - self.hierarchy_file = aux_files['hierarchy_file'] - self.n_samples = n_samples - - # Get all general region weights - self.r_weights = get_all_region_weights( - self.distribution, self.dist_params, self.hierarchy_file, self.sample_hierarchy_lvl) - ## Include aggregated region weights - if aux_files['sw']['GSw_RegionResolution'] == 'aggreg': - self.r_weights = { - **self.r_weights, - **{ - aux_files['hierarchy_file'].set_index('ba').aggreg.get(k,k): v - for k,v in self.r_weights.items() - }, - } - - # Store the weights for the recf files (CF files) - # Those are computed during the the supply curve file sampling - self.recf_weights_map = {} - - # Flag to validade that recf_weights_map was normalized - self.flag_recf_normalization = defaultdict(lambda: False) - - def _validate_inputs(self, dist_files: list, sw_name: str, file_name: str) -> None: - """ - Validate inputs - - Args: - dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) - sw_name (str): Name of the switch we are getting the weights for. - file_name (str): Name of the file we are getting the weights for. - """ - # Identify relevant columns that exist in the hierarchy - #Examples: p1, p2, New_England, ...(covers NG and LOAD) - columns_in_hierarchy = [col for col in dist_files[0].keys() if col in set(self.r_weights.keys())] - - # Columns that start with region, r, ... - generic_region_columns = [col for col in dist_files[0].keys() if col in MCSConstants.REGION_SYNONYMS] - - # We have as many unique weights as len(unique_sample_levels) - unique_sample_levels = self.hierarchy_file[self.sample_hierarchy_lvl].unique() - single_r_weight = len(unique_sample_levels) == 1 - - # Group files that require special treatment - except_files = MCSConstants.SUPPLY_CURVE_FILES + MCSConstants.EXOG_CAP_FILES + ( - MCSConstants.PRESCRIBED_BUILDS_FILES + MCSConstants.RECF_FILES) - - # Return an error if you have multiple weight assignments but the mcs_distributions.yaml object is - # pointing to a set of switches that have no region columns - # e.g. asking for a region-based sampling for swicthes.csv, or plantchar type files. - if not single_r_weight and not columns_in_hierarchy and not generic_region_columns and ( - file_name not in except_files): - raise ValueError( - f"Invalid sampling configuration for file: {file_name}\n" - f"Switch: {sw_name}\n" - f"weight_r group: {self.sample_hierarchy_lvl}\n" - "[Error] Either:\n" - " 1. The file does not contain any regional columns but was assigned" - " to a region-based sampling group different than country, or\n" - " 2. The selected weight_r resolution is not valid for this file" - " (e.g., BA for NG fuel prices, which are based on cendiv).\n\n" - "Please review the `mcs_distributions.yaml` configuration." - ) - - # Check if all elements in dist_files have the same index - if not all(df.index.equals(dist_files[0].index) for df in dist_files): - raise ValueError( - f"Invalid sampling configuration for file: {file_name}\n" - f"Switch: {sw_name}\n" - "All reference files must have the same indexes" - ) - - # Check if the distribution is multiplicative and the file has a year column - if self.distribution in MCSConstants.MULTIPLICATIVE_DISTRIBUTIONS: - if dist_files[0].columns.isin(MCSConstants.YEAR_SYNONYMS).any(): - raise ValueError( - "Files with year columns are not supported for multiplicative distributions. " - f"Change the distribution for switch {sw_name}" - ) - - def get_df_weights( - self, - dist_files: list, - modifiable_columns: list, - sw_name: str, - file_name: str, - ) -> dict: - """ - Dispatch to the appropriate method based on file type. - - Args: - dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) - modifiable_columns (list of str): List of columns that can be directly multiplied by the weights. - sw_name (str): Name of the switch we are getting the weights for. - file_name (str): Name of the file we are getting the weights for. - - Returns: - dict: Dictionary with the weights for each reference file and sample. - """ - - self._validate_inputs(dist_files, sw_name, file_name) - - if file_name in MCSConstants.SUPPLY_CURVE_FILES: - return self._get_weights_supply_curve(dist_files, modifiable_columns, sw_name) - elif file_name in MCSConstants.RECF_FILES: - return self._get_weights_recf(sw_name) - elif file_name in MCSConstants.EXOG_CAP_FILES + MCSConstants.PRESCRIBED_BUILDS_FILES: - return self._get_weights_exog_prescribed(dist_files) - else: - return self._get_weights_general(dist_files, modifiable_columns, sw_name, file_name) - - def _get_weights_general( - self, - dist_files: list, - modifiable_columns: list, - sw_name: str, - file_name: str - ) -> dict: - """ - Get weights for a general file that does not require special treatment. - Files that require special treatment are those associated with - supply curve switches. - - Args: - dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) - modifiable_columns (list of str): List of columns that can be directly multiplied by the weights. - sw_name (str): Name of the switch we are getting the weights for. - file_name (str): Name of the file we are getting the weights for. - - Returns: - dict: Dictionary with the weights for each reference file and sample. - """ - # Number of reference files/values. Sicne all sw_assignments - # have the same number of files, we can use the first one. - n_files = len(self.sample_group["sw_assignments"][0]) - - # Identify relevant columns that exist in the hierarchy - #Examples: p1, p2, New_England, ...(covers NG and LOAD) - columns_in_hierarchy = [col for col in dist_files[0].keys() if col in set(self.r_weights.keys())] - - # We have as many unique weights as len(unique_sample_levels) - unique_sample_levels = self.hierarchy_file[self.sample_hierarchy_lvl].unique() - single_r_weight = len(unique_sample_levels) == 1 - - # Dictionary to store computed weights for the modifiable columns - # (sample, file) -> pd.DataFrame - dict_df_weights = {} - - # Handle the simple case where there is only one weight for all regions - # (or no regions). - if single_r_weight: - # Get the first region key - first_region = next(iter(self.r_weights)) - weight_matrix = self.r_weights[first_region] - - if file_name == "switches.csv": - for s in range(self.n_samples): - for f in range(n_files): - dict_df_weights[(s, f)] = weight_matrix[s, f] - else: - for s in range(self.n_samples): - for f in range(n_files): - dict_df_weights[(s, f)] = pd.DataFrame( - data=weight_matrix[s, f], - columns=modifiable_columns, - index=dist_files[0].index, - ) - - # Cases that have regional columns from columns_in_hierarchy - # and the weights are not the same for all regions - elif not single_r_weight and len(columns_in_hierarchy) and file_name != "switches.csv" : - for s in range(self.n_samples): - for f in range(n_files): - - w_df_tmp = pd.DataFrame( - {col: self.r_weights[col][s, f] for col in columns_in_hierarchy}, - index=dist_files[0].index - ) - - dict_df_weights[(s, f)] = w_df_tmp - - return dict_df_weights - - def _get_weights_supply_curve( - self, - dist_files: list, - modifiable_columns: list, - sw_name: str - ) -> dict: - """ - Get the weights for supply curve files. These files require special treatment - because some columns are dependent on the capacity of each sc_point_gid. - - Args: - dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) - modifiable_columns (list of str): List of columns that can be directly multiplied by the weights. - sw_name (str): Name of the switch we are getting the weights for. - - Returns: - dict: Dictionary with the weights for each reference file and sample. - """ - # Dictionary to store computed weights for the modifiable columns - # (sample, file) -> pd.DataFrame - dict_df_weights = {} - - # Store weights to use later in the recf files (CF files) - self.recf_weights_map [sw_name] = {} - - # Create a new column with the class|region combination (like in the CF file) - dist_files_copy = [copy.deepcopy(df) for df in dist_files] - - for df in dist_files_copy: - df["old c|r"] = ( - df["class"].astype(int).astype(str) + "|" + df["region"].astype(str) - ) - - for s in range(self.n_samples): - for f, df in enumerate(dist_files_copy): - # Initial skeleton of the weights DataFrame - w_df_tmp = df[["region", "sc_point_gid", "old c|r"]] - - # Create a mapping from each unique region to its corresponding weight - region_to_weight = { - r: self.r_weights[r][s, f] - for r in w_df_tmp["region"].unique() - } - - # Compute the region weights for each row - region_weights = w_df_tmp["region"].map(region_to_weight).values - - # Build a new DataFrame for the modifiable columns using a dict comprehension. - # For "capacity", we assign the raw region weight; for others, multiply by capacity. - modifiable_df = pd.DataFrame({ - col: (region_weights if col == "capacity" else region_weights * df["capacity"]) - for col in modifiable_columns - }, index=w_df_tmp.index) - - # Join the modifiable columns back into the original DataFrame - w_df_tmp = w_df_tmp.join(modifiable_df) - - # Save the intermediate weights for the recf files (These are weights multiplied by capacity) - self.recf_weights_map [sw_name][(s, f)] = w_df_tmp[["old c|r","class"]].rename(columns={"class": "weight"}) - - # Store in dictionary - dict_df_weights[(s, f)] = w_df_tmp.drop(columns=["old c|r"]) - - # Normalize the weights to sum to 1 - # Divide the weights by the sum of the weights across all files - sum_weights = sum(dict_df_weights[(s, f)][modifiable_columns] for f in range(len(dist_files))) - sum_weights[sum_weights == 0] = 1 - - for f in range(len(dist_files)): - dict_df_weights[(s, f)][modifiable_columns] /= sum_weights - # recf_weights_map is not normalized here because it will be normalized later - # values need to be aggregated according to the new c|r column from supply curves - - return dict_df_weights - - def _get_weights_exog_prescribed(self, dist_files: list) -> dict: - """ - Get the weights for exogenous capacity and prescribed builds files. - - Args: - dist_files (list of pd.DataFrame): List of reference dataframes (ajusted to have the same # of rows) - - Returns: - dict: Dictionary with the weights for each reference file and sample. - """ - dict_df_weights = {} - for s in range(self.n_samples): - for f, df in enumerate(dist_files): - - region_to_weight = { - r: self.r_weights[r][s, f] - for r in df["region"].unique() - } - - dict_df_weights[(s, f)] = pd.DataFrame( - data=df["region"].map(region_to_weight).values, - columns=["capacity"], - index=df.index, - ) - - return dict_df_weights - - def _get_weights_recf(self, sw_name: str) -> dict: - """ - Get the weights for the recf files (CF files). This file construction is - dependent on the new supply curve samples and therefore is computed after - the supply curve files are sampled. - - Args: - sw_name (str): Name of the switch we are getting the weights for. - """ - # From get_dist_instructions(.) the supply curve file is deliberaly - # placed before the recf files, so that recf_weights_map is already populated. - - # Check if recf_weights_map is not empty and that it was normalized. - if not self.recf_weights_map[sw_name]: - raise ValueError( - f"The recf_weights_map for switch {sw_name} was not populated" - ) - - if not self.flag_recf_normalization[sw_name] : - raise ValueError( - f"The recf_weights_map for switch {sw_name} was not normalized" - ) - - return self.recf_weights_map[sw_name] - - def normalize_recf_weights_map(self, samples_sw: list, sw_name: str) -> None: - """ - The recf map is responsible for informing how the old class/region data files - need to be put together (weights) to form the new class/region data. - After creating the new supply curve sample, we normalize the weights to sum to 1. - - Args: - samples_sw (list of pd.DataFrame): List of samples for the supply curve files. - sw_name (str): Name of the switch being sampled. - - Updates: - self.recf_weights_map (dict): Dictionary with the normalized weights for the recf files. - Each element of this dictionary is a pd.DataFrame (for the sample s and reference file f) - with the normalized weights, indexed by new and old class|region (c|r). - """ - n_files = len({key[1] for key in self.recf_weights_map[sw_name].keys()}) - - for s in range(self.n_samples): - # The normalization can change depending on the sample # - for f in range(n_files): - # Add a new column with the new class|region combination - self.recf_weights_map[sw_name][(s, f)]["new c|r"] = ( - samples_sw[s]["class"].astype(str) + "|" + - samples_sw[s]["region"].astype(str) - ) - - # Sum the weights for each new class|region combination - self.recf_weights_map[sw_name][(s, f)] = self.recf_weights_map[sw_name][(s, f)].groupby( - ["new c|r","old c|r"], as_index=False).sum() - - # Remove cases with 0 weight (e.g old c|r had no capacity -> class 0) - self.recf_weights_map[sw_name][(s, f)] = self.recf_weights_map[sw_name][(s, f)][ - self.recf_weights_map[sw_name][(s, f)]["weight"] > 0 - ] - - # Go over all files and obtain the total sum of weights for each new class|region - sum_weights_recf_map = ( - pd.concat( - [self.recf_weights_map[sw_name][(s, f)] for f in range(n_files)] - ) - .groupby("new c|r")["weight"] - .sum() - .to_dict() - ) - - for f in range(n_files): - # Get current DataFrame - df = self.recf_weights_map[sw_name][(s, f)] - # Perform division using "new c|r" as the reference - df["weight"] = df["weight"] / df["new c|r"].map(sum_weights_recf_map) - # Assign back to original structure - self.recf_weights_map[sw_name][(s, f)] = df.set_index(["new c|r", "old c|r"]) - - # Flag to validade that recf_weights_map was normalized - self.flag_recf_normalization[sw_name] = True - - -#%% =========================================================================== -### --- MAIN SAMPLING CLASS --- -### =========================================================================== -class MCS_Sampler: - """ - Monte Carlo Sampling Distribution Manager for ReEDS. - - This class allows enforcing sampling variability at different ReEDS regions - (st, ba, ...) and enforcing correlation between samples from differen switches. - - The following sampling strategies have been implemented: - - 1. Dirichlet Sampling: - - Generates a Dirichlet sample (h1, ..., hn) ~ Dir(alpha1, ..., alphan). - - Uses these weights to compute a weighted average of reference files: - sample = h1*f1 + ... + hn*fn - - 2. Discrete Sampling: - - Chooses a single reference file based on a discrete probability distribution. - - 3. Multiplicative Sampling: - - Applies a random multiplier to a single reference file or switch. - - The multiplier is drawn from a uniform or triangular distribution: - sample = multiplier * f - - """ - def __init__(self, sample_group, aux_files, n_samples=1): - self.sample_group = sample_group - self.aux_files = aux_files - self.n_samples = n_samples - - # Derive parameters from inputs - self.reeds_path = sample_group['reeds_path'] - self.inputs_case = sample_group['inputs_case'] - self.distribution = sample_group['dist'] - self.dist_params = sample_group['dist_params'] - self.ReEDS_resolution = aux_files['sw']['GSw_RegionResolution'] - if self.ReEDS_resolution=='aggreg' and sample_group['weight_r']=='ba': - self.sample_hierarchy_lvl = 'aggreg' - else: - self.sample_hierarchy_lvl = sample_group['weight_r'] - - # Inputs that require special treatment - self.hierarchy_file = get_hierarchy_file(self.inputs_case, self.ReEDS_resolution) - - # Store the samples for each switch (a single sw may have multiple files that is - # why we refer to the switch by its adjusted name) - self.samples = {sw_name: [] for sw_name in self.sample_group['Sample_ID']} - - # Instantiate WeightCalculator. - self.weight_calc = WeightCalculator(sample_group, aux_files, n_samples) - - @staticmethod - def prepare_ref_data( - dist_files: list, - file_name: str, - sw_name: str | list, - aux_files, - ) -> Tuple[list, list, dict]: - """ - This function prepares the reference dataframes for the Monte Carlo sampling. - For some files like those related to supply curves we need to expand/modify - the reference files to include additional rows/columns. - - Args: - dist_files (list of pd.DataFrame): List of reference dataframes to be modified. - file_name (str): name given by reeds to the files in dist_files. - sw_name (str or list): Name of the switch being sampled. For the special case - of float switches, this will be a list of switch names. - - Returns: - list of pd.DataFrame: List of modified reference dataframes. - list of str: List of columns that can be directly multiplied by the weights. - dict: Dictionary with the number of decimal places for columns we will modify - """ - ### =========================================================================== - ### --- Expand dist_files if necessary --- - ### =========================================================================== - # For each file map the columns we need to verify in the df expansion - # (e.g For the supply curves we will make sure that all files are - # ajusted to contain all regions and sc_point_gid combinations) - map_files2ref_columns = { - **{file: ["region", "sc_point_gid"] for file in MCSConstants.SUPPLY_CURVE_FILES}, - **{file: ["region", "year", "sc_point_gid"] for file in MCSConstants.EXOG_CAP_FILES}, - **{file: ["region", "year"] for file in MCSConstants.PRESCRIBED_BUILDS_FILES}, - } - - if file_name in map_files2ref_columns: - ref_columns = map_files2ref_columns[file_name] - - # Get all unique combination for the reference columns - unique_reg_gid_point = pd.concat( - [df[ref_columns] for df in dist_files], - ignore_index=True - ).drop_duplicates().reset_index(drop=True) - - # Modify dfs in the dist_files list adding missing ref_columns combinations - # and initializing the modifiable rows with 0 - for i, df in enumerate(dist_files): - dist_files[i] = unique_reg_gid_point.merge( - df.reset_index(drop=True), - on=ref_columns, - how="left", - ).fillna(0).sort_values(by=ref_columns).reset_index(drop=True) - - ### =========================================================================== - ### --- Get a list of the columns we are allowed to apply weights directly --- - ### =========================================================================== - # Get the base set of general (modifiable) columns from dist_files[0] - general_mult_columns = { - col for col in dist_files[0].keys() if col not in MCSConstants.FIXED_COLUMN_NAMES - } - - # Ensure all dist_files have the same set of general columns - if file_name not in MCSConstants.RECF_FILES: - for i, df in enumerate(dist_files[1:], start=1): - current_cols = {col for col in df.keys() if col not in MCSConstants.FIXED_COLUMN_NAMES} - if current_cols != general_mult_columns: - error_msg = ( - f"Column mismatch between dist_files[0] and dist_files[i]:\n" - "This usually happens when you run MCS on a file whose columns " - "vary by switch assignment (e.g. RECF_FILES).\n If you really need to support " - f"'{file_name}' here, add the necessary handling in prepare_ref_data()." - ) - raise ValueError(error_msg) - - exceptions_mult_col = { - **{file: ["class"] + list(general_mult_columns) for file in MCSConstants.SUPPLY_CURVE_FILES}, - **{file: ["capacity"] for file in MCSConstants.EXOG_CAP_FILES}, - **{file: ["capacity"] for file in MCSConstants.PRESCRIBED_BUILDS_FILES}, - **{file: [] for file in MCSConstants.RECF_FILES}, # treated separately - } - modifiable_columns = exceptions_mult_col.get(file_name, list(general_mult_columns)) - - ### =========================================================================== - ### --- Map for the number of decimals in each column we will change - ### =========================================================================== - if file_name in ["switches.csv"]: - n_decimals = max_decimal_places(dist_files[0].loc[sw_name,1]) - else: - n_decimals_list = [ - max_decimal_places(df[modifiable_columns]) for df in dist_files - ] - - # Take the max decimal count per column across all files, capped at 6 - n_decimals = { - col: min(max(d[col] for d in n_decimals_list), 6) - for col in n_decimals_list[0] - } - - return dist_files, modifiable_columns, n_decimals - - def load_ref_files(self, sample_idx: int) -> List[pd.DataFrame]: - """ - Load the reference files associated with the sample. - - Args: - sample_idx (int): Index of the Sample_ID in sample_group. - Some switches have multiple files associated with them that is why we - track samples using Sample_ID in the sample_group. - - Returns: - List[pd.DataFrame]: List of DataFrames with the switch files. - """ - - sw_name = self.sample_group['switch_names'][sample_idx] - - # Create a list of dataframes with the data related to the switch - dist_files = [] - - for sw_assignment in self.sample_group['sw_assignments'][sample_idx]: - - sw_runfiles_csv = self.sample_group['runfiles_csv'][sample_idx].copy(deep=True) - sw_runfiles_csv['sw_assignment'] = sw_assignment - - if not pd.isna(sw_runfiles_csv['filepath']): - sw_runfiles_csv['full_filepath'] = os.path.join( - self.reeds_path, - sw_runfiles_csv['filepath'].replace(f'{{{sw_name}}}', sw_assignment), - ) - - df = read_csv_h5_file(sw_runfiles_csv, self.aux_files, self.reeds_path, self.inputs_case) - dist_files.append(df) - - return dist_files - - # ----------------------- Weight Application Helpers ----------------------- - def _adjust_supply_curve_sample(self, samples_sw: list, sw_name: str, sample_idx: int) -> list: - """ - Adjust samples for supply curve files: - - Convert the 'class' column to integers. - - Normalize the weights map. - - Remove rows with no capacity. - - Args: - samples_sw (list of pd.DataFrame): List of samples for the supply curve files. - sw_name (str): Name of the switch being sampled. - sample_idx (int): Index of the Sample_ID in sample_group. - - Returns: - list of pd.DataFrame: List of adjusted samples for the supply curve files. - """ - - for s in range(self.n_samples): - # Convert class to integer - samples_sw[s]["class"] = samples_sw[s]["class"].astype(int) - - # Update the recf weights map (weight_calc.recf_weights_map) - self.weight_calc.normalize_recf_weights_map(samples_sw, sw_name) - - # Remove samples with no capacity. - # Need to do this after normalizing the recf weights - samples_sw = [df[df["capacity"] > 0].copy() for df in samples_sw] - - return samples_sw - - def _adjust_exog_cap_samples(self, samples_sw: list, file_name: str) -> list: - """ - Adjust samples for exogenous capacity files: - - Remove rows with no capacity. - - Adjust the tech classes based on available classes per sc_point_gid. - - Args: - samples_sw (list of pd.DataFrame): List of samples for the exogenous capacity files. - file_name (str): Name of the file being sampled. - """ - # Remove samples with no capacity - samples_sw = [df[df["capacity"] > 0].copy() for df in samples_sw] - - tech_mapping = { - "exog_cap_upv.csv": ("upv", "supplycurve_upv.csv"), - "exog_cap_wind-ons.csv": ("wind-ons", "supplycurve_wind-ons.csv"), - } - tech_name, Sample_ID = tech_mapping[file_name] - - for s in range(self.n_samples): - # Get the class available for each sc_point_gid - class_sc_point_map = self.samples[Sample_ID][s][["sc_point_gid", "class"]] - class_sc_point_map = class_sc_point_map.set_index("sc_point_gid").to_dict()["class"] - - # Remove any rows from samples_sw[s] that cannot be mapped - # These are cases with zero supply in the region - valid_sc_point_gids = samples_sw[s]["sc_point_gid"].isin(class_sc_point_map.keys()) - samples_sw[s] = samples_sw[s][valid_sc_point_gids].copy() - - # Create a new tech name for each sc_point_gid - new_tech_name = [tech_name + "_" + str(int(c)) for c in - samples_sw[s]["sc_point_gid"].map(class_sc_point_map).values] - - samples_sw[s]["*tech"] = new_tech_name - - return samples_sw - - def _apply_weights_general( - self, - dist_files: list, - modifiable_columns: list, - n_decimals: dict|int, - dict_df_weights: dict, - sample_idx: int - ): - """ - Apply the distribution weights to the reference files. - Applicable to all cases but recf files and switches.csv. - - Args: - dist_files (List[pd.DataFrame]): List of input DataFrames for sampling. - modifiable_columns (List[str]): List of columns that can be directly multiplied by the weights. - n_decimals (Dict[str, int]): Dictionary with the number of decimal places for each column. - dict_df_weights (Dict[Tuple[int, int], pd.DataFrame]): Dictionary with the weights for each reference file and sample. - sample_idx (int): Index of the Sample_ID in sample_group. - - Update: - self.samples (Dict[str, List[pd.DataFrame]]): Dictionary with the samples for each switch/file_name. - """ - - Sample_ID = self.sample_group['Sample_ID'][sample_idx] - sw_name = self.sample_group['switch_names'][sample_idx] - file_name = self.sample_group["runfiles_csv"][sample_idx]["filename"] - - # Initialize samples with zero values - samples_sw = [dist_files[0].copy() for n in range(self.n_samples)] - - for s in range(self.n_samples): - # Initialize with zeros - samples_sw[s][modifiable_columns] = 0 - - for f, df in enumerate(dist_files): - samples_sw[s][modifiable_columns] += df[modifiable_columns] * dict_df_weights[s, f][modifiable_columns] - - for col in modifiable_columns: - samples_sw[s][col] = samples_sw[s][col].round(n_decimals[col]+1) - - if file_name in MCSConstants.SUPPLY_CURVE_FILES: - adjusted_samples = self._adjust_supply_curve_sample(samples_sw, sw_name, sample_idx) - - elif file_name in MCSConstants.EXOG_CAP_FILES: - adjusted_samples = self._adjust_exog_cap_samples(samples_sw, file_name) - - elif file_name in MCSConstants.PRESCRIBED_BUILDS_FILES: - # Remove samples with no capacity - adjusted_samples = [df[df["capacity"] > 0].copy() for df in samples_sw] - - else: - # For all other files we can directly apply the weights - adjusted_samples = samples_sw - - # Save the adjusted samples. - self.samples[Sample_ID] = adjusted_samples - - def _apply_weights_recf( - self, - dist_files: list, - sample_idx: int - ): - """ - Apply the distribution weights to the recf files. - This file gets compleatly overwriten so need to be treated separately - - Args: - dist_files (List[pd.DataFrame]): List of input DataFrames for sampling. - sample_idx (int): Index of the Sample_ID in sample_group. - - Update: - self.samples (Dict[str, List[pd.DataFrame]]): Dictionary with the samples for each switch/file_name. - """ - - Sample_ID = self.sample_group['Sample_ID'][sample_idx] - sw_name = self.sample_group['switch_names'][sample_idx] - - # For the recf files we need to apply the weights to the old class|region combinations - weights = self.weight_calc.recf_weights_map[sw_name] - # Index is the same for all files (time) - indexes = dist_files[0].index - - samples_sw = [] - - for s in range(self.n_samples): - sample_sw = defaultdict(int) - - for f, df in enumerate(dist_files): - # Get the old and new class|region combinations from weights[(s, f)] - for (new_c_r, old_c_r) in weights[(s, f)].index: - sample_sw[new_c_r] += df[old_c_r] * weights[(s, f)].loc[(new_c_r,old_c_r)].values[0] - - # Round numbers to 9 decimal places and allow a maxium values of 1 - for new_c_r in sample_sw.keys(): - sample_sw[new_c_r] = sample_sw[new_c_r].round(9).clip(0,1) - - samples_sw.append(pd.DataFrame(sample_sw, index=indexes)) - - self.samples[Sample_ID] = samples_sw - - def _apply_weights_switches_csv( - self, - dist_files: list, - n_decimals: dict, - dict_df_weights: dict, - sample_idx: int, - ): - """ - Apply the distribution weights to the switches.csv file. - - Args: - dist_files (List[pd.DataFrame]): List of input DataFrames for sampling. - n_decimals (Dict[str, int]): Dictionary with the number of decimal places for each column. - dict_df_weights (Dict[Tuple[int, int], pd.DataFrame]): Dictionary with the weights for each reference file and sample. - sample_idx (int): Index of the Sample_ID in sample_group. - - Update: - self.samples (Dict[str, List[pd.DataFrame]]): Dictionary with the samples for each switch/file_name. - """ - # Switches are saved only for the rows changed because this allow - # multiple json objects changing different switches using different distributions - - sw_name = self.sample_group['switch_names'][sample_idx] - - samples_sw = [None for n in range(self.n_samples)] - sw_assignments = self.sample_group['sw_assignments'][sample_idx] - - for s in range(self.n_samples): - for assingment_idx, sw_assignment in enumerate(sw_assignments): - - # The switch assignments case can be a int, a float or a string - # If it is a str or int it must be used in a discrete distribution - if isinstance(sw_assignment, (str, int)): - # Check if we have a discrete distribution - if self.distribution != "discrete": - raise ValueError( - f"You specified a str/int assignment for switch '{sw_name}', " - "but the distribution is not set to 'discrete'. " - "This file is likely hard-coded in `copy_files.py`.\n\n" - "To fix this, you can try to:\n" - " - Change the distribution to 'discrete'\n" - " - Use a float assignment instead, or\n" - " - Add support for this switch's files\n\n" - "A good place to start is the `read_exception_file()` function." - ) - - if isinstance(sw_assignment, str) and dict_df_weights[s, assingment_idx]: - # dict_df_weights[s,f] is a one hot encoding of the sw_assignment options - samples_sw[s] = sw_assignment - - elif isinstance(sw_assignment, int) and dict_df_weights[s, assingment_idx]: - # dict_df_weights[s,f] is a one hot encoding of the sw_assignment options - samples_sw[s] = str(sw_assignment) - - elif isinstance(sw_assignment, float): - if self.distribution == "discrete": - # dict_df_weights[s,f] is a one hot encoding of the sw_assignment options - samples_sw[s] = str(sw_assignment) - - elif self.distribution in MCSConstants.MULTIPLICATIVE_DISTRIBUTIONS: - # We have a validation process that makes sure that we only have one file - samples_sw[s] = ( - str(np.round(sw_assignment * dict_df_weights[s, 0], n_decimals+1)) - ) - else: - raise ValueError( - f"Float assignments can only be used with a discrete or multiplicative distribution. " - f"Check the distribution for switch '{sw_name}'." - ) - - self.samples[sw_name] = samples_sw - - def record_group_weights(self, log_folder: str) -> None: - """ - Record the weights for each distribution group in - lstfiles/mcs_group_weights.csv. Appends to the file if it exists. - - Args: - mcs_sampler (MCS): The MCS object containing the distribution groups and weights. - log_folder (str): Directory where the weights file will be stored. - """ - os.makedirs(log_folder, exist_ok=True) - save_path = os.path.join(log_folder, 'mcs_group_weights.csv') - r_weights = self.weight_calc.r_weights - group_name = self.sample_group["name"] - assignments_list = self.sample_group["assignments_list"] - - # Get shape of any region’s weight array - any_region = next(iter(r_weights)) - n_samples, n_assignments = r_weights[any_region].shape - - # Build column names - columns = ( - ['group_name', 'switch_name', 'sw_assignment', 'r'] + - [f"Weight Sample {i}" for i in range(1, n_samples + 1)] - ) - - data = [] - - for switch_idx, switch_dict in enumerate(assignments_list): - sw_name, sw_assignment = next(iter(switch_dict.items())) - for r in r_weights.keys(): - for assignment_idx, assignment_value in enumerate(sw_assignment): - weights = list(r_weights[r][:, assignment_idx]) # column for this switch - row = [group_name, sw_name, assignment_value, r] + weights - data.append(row) - - weight_record_df = pd.DataFrame(data, columns=columns) - - # Append if file exists, else write with header - if os.path.exists(save_path): - weight_record_df.to_csv(save_path, mode='a', index=False, header=False) - else: - weight_record_df.to_csv(save_path, mode='w', index=False, header=True) - # ----------------------- End of Weight Application Helpers ----------------------- - - def get_samples(self, aux_files): - """ - Generates Monte Carlo samples for each switch and applies the appropriate weight assignment. - - Returns: - Dict[str, List[pd.DataFrame]]: Dictionary with the samples for each switch/file_name. - """ - # Iterate over each switch file and apply the appropriate weight assignment method - for sample_idx, sample_ID in enumerate(self.sample_group['Sample_ID']): - - sw_name = self.sample_group['switch_names'][sample_idx] - file_name = self.sample_group["file_names"][sample_idx] - dist_files = self.load_ref_files(sample_idx) - - #In some small cases all dist_files are empty - if not all([len(df) for df in dist_files]): - self.samples[sample_ID] = [dist_files[0] for s in range(self.n_samples)] - continue - - # Extend/modify dist_files if necessary (e.g supply curve related data) - dist_files, modifiable_columns, n_decimals = self.prepare_ref_data( - dist_files, file_name, sw_name, aux_files, - ) - - # Get weights we will apply to the reference files - dict_df_weights = self.weight_calc.get_df_weights(dist_files, modifiable_columns, sw_name, file_name) - - # Dispatch weight application based on file type. - if file_name == "switches.csv": - self._apply_weights_switches_csv(dist_files, n_decimals, dict_df_weights, sample_idx) - elif file_name in MCSConstants.RECF_FILES: - self._apply_weights_recf(dist_files, sample_idx) - else: - self._apply_weights_general(dist_files, modifiable_columns, n_decimals, dict_df_weights, sample_idx) - - return self.samples - - -#%% =========================================================================== -### --- OUTPUT FUNCTIONS --- -### =========================================================================== -def write_samples( - sample_group: pd.Series, - samples_dict: dict, - aux_files: dict, - run_ReEDS: bool = True, -): - """ - Write the samples to the appropriate locations - - Args: - sample_group (pd.Series): Row of the input file with the sampling instructions. - samples_dict (dict): Dictionary with the samples for each switch/file_name. - aux_files (dict): Dictionary with the auxiliary files needed for sampling. - run_ReEDS (bool): If True, the script is being used to run ReEDS. - If False, the script is being used to test the samples before running ReEDS. - """ - - inputs_case = sample_group['inputs_case'] - - for sample_idx, sample_ID in enumerate(samples_dict.keys()): - samples = samples_dict[sample_ID] - sw_name = sample_group['switch_names'][sample_idx] - save_path_structure = sample_group['save_paths'][sample_idx] # Where the samples will be copied to - file_name = sample_group["file_names"][sample_idx] - - for n, sample in enumerate(samples): - save_path = save_path_structure.replace('{sample_n}', str(n)) - folder_path = os.path.dirname(save_path) # Folder path without the file name - file_termination = os.path.splitext(save_path)[-1] # File termination (.csv, .h5, etc.) - - # For run_ReEDS==False, create folder if it does not exist - if (not run_ReEDS): - os.makedirs(folder_path, exist_ok=True) - - # If we have a region-indexed file - if file_name in aux_files['region_files']['filename'].values: - # Get destination directory instead of save_path - dir_dst = os.path.dirname(save_path) - # Get the row of the region-indexed file - region_files_row = aux_files['region_files'].query('filename == @file_name').iloc[0] - copy_files.write_region_indexed_file(sample, dir_dst, aux_files['source_deflator_map'], - aux_files['sw'], region_files_row, - aux_files['regions_and_agglevel'], - aux_files['agglevel_variables']) - - elif file_termination == '.csv': - # Not a region-indexed file but it is a CSV file - if file_name != 'switches.csv': - sample.to_csv(save_path, index=False) - else: - # Read the original switches.csv file - original_switches = pd.read_csv(save_path, header=None, index_col=0) - # Update the original switches.csv file with the new samples - original_switches.loc[sw_name] = sample - original_switches.to_csv(save_path, header=False) - if run_ReEDS: - # Create gswitches.csv and .txt files - gswitches_path = reeds.io.write_gswitches(original_switches, inputs_case) - copy_files.scalar_csv_to_txt(gswitches_path) - - - if run_ReEDS: # Only print if running ReEDS optimization - reduced_path = os.sep.join(save_path.strip(os.sep).split(os.sep)[-3:]) - print(f"...Sample related to switch {sw_name} was copied to {reduced_path}") - - -#%% =========================================================================== -### --- MAIN PROCEDURE --- -### =========================================================================== -def main( - reeds_path: str, - inputs_case: str, - n_samples: int = 1, - seed: int = 0, - run_ReEDS: bool = True, -): - """ - Create samples for the Monte Carlo Simulation (MCS). - - Args: - reeds_path (str): Path to the ReEDS directory. - inputs_case (str): Path to the inputs_case directory. - n_samples (int): Number of samples to generate. - seed (int): Seed for the random number generator. - run_ReEDS (bool): If True, the script is being used to run ReEDS. - If False, the script is being used to test the samples before running ReEDS. - - Notes: - If run_ReEDS=False inputs_case only needs to be a folder containing switches.csv so - we can perform any spatial filtering needed to get the samples - """ - - if run_ReEDS: - # Set random seed as the (MCS run number + seed) to allow reproducibility without having - # the same sample for each MCS-ReEDS call - runs_folder_name = os.path.basename(os.path.dirname(inputs_case.rstrip(os.path.sep))) - mcs_run_number = int((runs_folder_name.split('_')[-1]).replace('MC', '')) - seed += mcs_run_number - np.random.seed(seed) - else: - # The script is being used to test the samples before running ReEDS - np.random.seed(seed) - - # Obtain instructions to sample the distributions for each switch - df_input_dist_instructions, aux_files = get_dist_instructions(reeds_path, inputs_case, run_ReEDS=run_ReEDS) - - print('Sampling...') - for _, sample_group in df_input_dist_instructions.iterrows(): - dist_switches = sample_group['switch_names'] - unique_switches = set(dist_switches) - - print(f"Sampling for switch(es): {unique_switches}") - # Create Samples - mcs_sampler = MCS_Sampler(sample_group, aux_files, n_samples) - samples_dict = mcs_sampler.get_samples(aux_files) - - # Record the weights of each sample group - mcs_sampler.record_group_weights( - os.path.join(os.path.dirname(inputs_case), 'lstfiles') - ) - # Write Samples - write_samples(sample_group, samples_dict, aux_files, run_ReEDS=run_ReEDS) - - -if __name__ == '__main__' and not hasattr(sys, 'ps1'): - parser = argparse.ArgumentParser(description='Copy files needed for this run') - parser.add_argument('reeds_path', help='ReEDS directory') - parser.add_argument('inputs_case', help='Output directory') - - args = parser.parse_args() - reeds_path = os.path.abspath(args.reeds_path) - inputs_case = os.path.abspath(args.inputs_case) - - # ---- Settings for testing ---- - # reeds_path = reeds.io.reeds_path - # inputs_case = os.path.join(reeds_path,'runs','v20250825_revM2_MonteCarlo_MC1','inputs_case') - # n_samples = 1 - # seed = 0 - # run_ReEDS = True - - # Set up logger - tic = datetime.datetime.now() - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(os.path.dirname(inputs_case), 'gamslog.txt'), - ) - - # Read switches and check if MCS_runs is enabled. - sw = reeds.io.get_switches(inputs_case) - MCS_Runs = int(sw.get('MCS_runs', 0)) - if MCS_Runs >= 1: - print('Starting mcs_sampler.py') - main(reeds_path, inputs_case, n_samples=1) - else: - print('MCS_runs switch is set to 0 or not found. No Monte Carlo sampling will be performed') - - # Final log/timing update. - reeds.log.toc( - tic=tic, - year=0, - process='input_processing/mcs_sampler.py', - path=os.path.join(os.path.dirname(inputs_case)) - ) diff --git a/input_processing/outage_rates.py b/input_processing/outage_rates.py deleted file mode 100644 index 53ce64fa..00000000 --- a/input_processing/outage_rates.py +++ /dev/null @@ -1,506 +0,0 @@ -#%%### Imports -import pandas as pd -import numpy as np -import os -import sys -import datetime -import argparse -import h5py -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -## Time the operation of this script -tic = datetime.datetime.now() - -reeds_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) - -#%%### Fixed inputs -tz_in = 'UTC' -tz_out = 'Etc/GMT+6' -temp_min = -50 -temp_max = 60 -## Only use during_quarters for techs without a monthly scheduled outage rate -during_quarters = ['spring', 'fall'] -## Cap the extrapolation of forced outage rates at high/low temperatures to 0.4 because -## PJM uses a 60% capacity credit (40% = 0.4 derate) for gas CT: -## https://www.pjm.com/-/media/DotCom/planning/res-adeq/elcc/2026-27-bra-elcc-class-ratings.pdf -max_extrapolated_outage_forced = 0.4 -## assume temperature-dependent outage rates for ng-fuel-cell to be the same as for combined_cycle plants -primemover2techgroup = { - 'combined_cycle': ['GAS_CC', 'FUEL_CELL'], - 'combustion_turbine': ['GAS_CT', 'H2_COMBUSTION'], - 'diesel': ['OGS'], - 'hydro_and_psh': ['HYDRO', 'PSH'], - 'nuclear': ['NUCLEAR'], - 'steam': ['COAL', 'BIO'], -} - -#%%### Functions -def extrapolate_forward_backward( - dfin, xmin, xmax, - numfitvals=2, polyfit_deg=1, ymin=0, ymax=1, - ): - """Extrapolate slopes forward and backward and fill gaps in integer steps. - Parameters - ---------- - dfin: pd.DataFrame - Dataframe with x values to extrapolate as the index - xmin: int - Minimum x value to extrapolate to - xmax: int - Maximum x value to extrapolate to - numfitvals: int, default 2 - Number of values from the beginning and end of the series to use to fit the - backward and forward slopes, respectively - polyfit_deg: int, default 1 - Degree of fitting polynomial to use in forward/backward extrapolations - ymin: float, default 0 - Lower limit for extrapolated values - ymax: float, default 1 - Upper limit for extrapolated values - - Returns - ------- - pd.DataFrame - Dataframe extrapolated forward and backward - """ - xs_low = dfin.index[:numfitvals].values - xs_high = dfin.index[-numfitvals:].values - - slope_low, intercept_low = np.polyfit( - x=xs_low, y=dfin.loc[xs_low].values, deg=polyfit_deg) - slope_high, intercept_high = np.polyfit( - x=xs_high, y=dfin.loc[xs_high].values, deg=polyfit_deg) - - ## Combine back-casted, input, and forward-casted data into a single dataframe - dfout = pd.concat([ - pd.DataFrame( - {xmin: intercept_low + slope_low * xmin}, - index=dfin.columns, - ).T, - dfin, - pd.DataFrame( - {xmax: intercept_high + slope_high * xmax}, - index=dfin.columns, - ).T, - ]).reindex(range(xmin, xmax+1)).interpolate('linear').clip(upper=ymax, lower=ymin) - - return dfout - - -def pm_to_tech(df, inputs_case): - """ - Broadcast prime mover timeseries data to techs. - - Parameters - ---------- - df: pd.DataFrame - index = timeseries - top column level = prime movers - inputs_case: str - path to ReEDS-2.0/runs/{case}/inputs_case - - Returns - ------- - pd.DataFrame - Same format as df but with prime movers broadcasted to techs - """ - tech_subset_table = reeds.techs.get_tech_subset_table(inputs_case) - df_prefill = pd.concat( - { - i: df[pm] - for pm in df.columns.get_level_values('prime_mover').unique() - for i in tech_subset_table.loc[primemover2techgroup[pm]].unique() - }, - axis=1, names=('i',) - ) - - return df_prefill - - -def fill_empty_techs(df_prefill, inputs_case, fillvalues_tech=None, during_quarters='all'): - """ - Parameters - ---------- - fillvalues_tech: pd.Series or dict with average values with which to fill missing techs. - keys = technologies. - during_quarters: 'all' or list of quarters. - If a list of quarters is provided, the annual fill values will be scaled and - applied only during the provided quarters. - - Returns - ------- - """ - ### Parse inputs - quarters = pd.read_csv( - os.path.join(inputs_case, 'sets', 'quarter.csv'), - header=None, - ).squeeze(1).map(lambda x: x[:4]).tolist() - if isinstance(during_quarters, str): - assert during_quarters == 'all' - elif isinstance(during_quarters, list): - during_quarters = [q[:4] for q in during_quarters] - assert all([i in quarters for i in during_quarters]) - else: - raise ValueError( - f"during_quarters={during_quarters} but must be 'all' or a list of quarters" - ) - - ### Case inputs - techlist = reeds.techs.get_techlist_after_bans(inputs_case) - hierarchy = reeds.io.get_hierarchy(reeds.io.standardize_case(inputs_case)) - - ### Identify techs with nonzero values that are not yet included in dataframe - keep_techs = [i for i in fillvalues_tech.index if i in techlist] - - included_techs = df_prefill.columns.get_level_values('i').unique() - missing_techs = [ - c for c in fillvalues_tech.index - if ((c.lower() not in included_techs) and (c.lower() in keep_techs)) - ] - print(f"included ({len(included_techs)}): {' '.join(sorted(included_techs))}") - print(f"missing ({len(missing_techs)}): {' '.join(sorted(missing_techs))}") - - ### Fill data for missing techs - if len(missing_techs): - if 'r' in df_prefill.columns.names: - dfout_filled = pd.concat( - { - (i,r): pd.Series(index=df_prefill.index, data=fillvalues_tech[i]) - for i in missing_techs for r in hierarchy.index - }, - axis=1, names=('i','r'), - ) - else: - dfout_filled = pd.concat( - { - i: pd.Series(index=df_prefill.index, data=fillvalues_tech[i]) - for i in missing_techs - }, - axis=1, names=('i'), - ) - - ## If during_quarters is provided, only apply outages during those quarters - if isinstance(during_quarters, list): - month2quarter = pd.read_csv( - os.path.join(inputs_case, 'month2quarter.csv'), - index_col='month', - ).squeeze(1).map(lambda x: x[:4]) - total_hours = len(dfout_filled) - outage_hours = ( - dfout_filled.index.month.map(month2quarter) - .isin(during_quarters).sum() - ) - dfout_filled *= (total_hours / outage_hours) - dfout_filled.loc[ - ~dfout_filled.index.month.map(month2quarter).isin(during_quarters) - ] = 0 - ## Make sure it worked - assert ( - dfout_filled.mean().round(3) - == fillvalues_tech.loc[dfout_filled.columns].round(3) - ).all() - - dfout = pd.concat([df_prefill, dfout_filled], axis=1) - else: - dfout = df_prefill - - return dfout - - -def calc_outage_forced( - reeds_path, - inputs_case, - max_extrapolated_outage_forced=max_extrapolated_outage_forced, -): - """ - """ - ### Derived inputs - sw = reeds.io.get_switches(inputs_case) - hierarchy = reeds.io.get_hierarchy(reeds.io.standardize_case(inputs_case)) - val_ba = ( - pd.read_csv(os.path.join(inputs_case, 'val_ba.csv'), header=None) - .squeeze(1).values - ) - ## Static forced outage rates (for filling empties) - outage_forced_static = pd.read_csv( - os.path.join(inputs_case, 'outage_forced_static.csv'), - header=None, index_col=0, - ).squeeze(1) - outage_forced_static.index = outage_forced_static.index.str.lower() - outage_forced_static = ( - outage_forced_static - .drop(reeds.techs.ignore_techs, errors='ignore') - .copy() - ) - - ### Load temperatures - print('Load temperatures and broadcast from states to zones') - temperatures = reeds.io.get_temperatures(inputs_case)[hierarchy.st] - temperatures.columns = hierarchy.index - - ### Input data - if sw.GSw_OutageScen.lower() == 'static': - ### Fill static data for all techs and modeled regions - df = pd.concat( - {r: outage_forced_static for r in val_ba}, - axis=0, - names=('r','i'), - ).reorder_levels(['i','r']).sort_index() - forcedoutage_prefill = pd.concat({i: df for i in temperatures.index}, axis=1).T - fits_forcedoutage = pd.DataFrame() - forcedoutage_pm = pd.DataFrame() - - else: - fits_forcedoutage_in = pd.read_csv( - os.path.join(inputs_case, 'outage_forced_temperature.csv'), - comment='#', - ).pivot(index='deg_celsius', columns='prime_mover', values='outage_frac') - - ### Extrapolate slopes and fill gaps in integer steps - fits_forcedoutage = extrapolate_forward_backward( - dfin=fits_forcedoutage_in, xmin=temp_min, xmax=temp_max, - ymax=max_extrapolated_outage_forced, - ) - - ### Get temperature-dependent outage rate by prime mover and state - forcedoutage_pm = pd.concat( - {pm: temperatures.replace(fits_forcedoutage[pm]) for pm in fits_forcedoutage}, - axis=1, names=('prime_mover',), - ).astype(np.float32) - - ### Map from prime movers to techs - forcedoutage_prefill = pm_to_tech(df=forcedoutage_pm, inputs_case=inputs_case) - - ### Fill missing hourly data with tech-specific static values - outage_forced_hourly = fill_empty_techs( - df_prefill=forcedoutage_prefill, - inputs_case=inputs_case, - fillvalues_tech=outage_forced_static, - ) - - return { - 'fits_forcedoutage': fits_forcedoutage, - 'forcedoutage_pm': forcedoutage_pm, - 'outage_forced_hourly': outage_forced_hourly, - } - - -def write_outages(df, h5path): - names = df.columns.names - namelengths = { - name: df.columns.get_level_values(name).map(len).max() - for name in names - } - column_level_type = f'S{max([len(i) for i in names])}' - with h5py.File(h5path, 'w') as f: - f.create_dataset('index', data=df.index, dtype='S29') - ## Save both the individual column levels and the single-level delimited version - for name in names: - f.create_dataset( - f'columns_{name}', - data=df.columns.get_level_values(name), - dtype=f'S{namelengths[name]}') - f.create_dataset( - 'columns', - data=( - df.columns.map(lambda x: '|'.join(x)).rename('|'.join(names)) - if isinstance(df.columns, pd.MultiIndex) - else df.columns - ), - dtype=f'S{sum(namelengths.values()) + 1}') - f.create_dataset( - 'data', data=df, dtype=np.float32, - compression='gzip', compression_opts=4, - ) - - f.create_dataset( - 'column_levels', - data=np.array(names, dtype=column_level_type), - dtype=column_level_type) - - -def plot_outage_forced( - case, - fits_forcedoutage, - forcedoutage_pm, - interactive=True, -): - ### Prep plots - import matplotlib.pyplot as plt - import matplotlib as mpl - reeds.plots.plotparams() - case = os.path.dirname(inputs_case.rstrip(os.sep)) - figpath = os.path.join(case, 'outputs', 'figures') - os.makedirs(figpath, exist_ok=True) - - ### Plot the fits - nicelabels = { - 'combined_cycle': 'Combined cycle', - 'combustion_turbine': 'Combustion turbine', - 'steam': 'Steam turbine', - 'nuclear': 'Nuclear', - 'hydro_and_psh': 'Hydro and PSH', - 'diesel': 'Diesel', - } - colors = dict(zip(nicelabels.values(), [f'C{i}' for i in range(10)])) - fits_in = pd.read_csv( - os.path.join(case, 'inputs_case', 'outage_forced_temperature.csv'), - comment='#', - ) - mintemp = fits_in.deg_celsius.min() - maxtemp = fits_in.deg_celsius.max() - - temperatures = reeds.io.get_temperatures(case) - - plt.close() - f,ax = plt.subplots() - df = fits_forcedoutage.rename(columns=nicelabels)*100 - for k, v in nicelabels.items(): - df.loc[mintemp:maxtemp, v].plot(ax=ax, color=colors[v], label=v, ls='-') - df.loc[:, v].plot(ax=ax, color=colors[v], ls='--', label='_nolabel') - ax.legend(frameon=False, title=None, fontsize='large') - ax.set_ylabel('Forced outage rate [%]') - ax.set_xlabel('Temperature [°C]') - ax.set_ylim(0) - ax.set_xlim(temperatures.min().min(), temperatures.max().max()) - ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(10)) - ax.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) - reeds.plots.despine(ax) - plt.savefig(os.path.join(figpath, 'FOR-fits.png')) - if interactive: - plt.show() - else: - plt.close() - - ### Plot forced outage rates - dfmap = reeds.io.get_dfmap(case) - dfzones = dfmap['r'] - aggfunc = 'mean' - - for pm in primemover2techgroup: - dfdata = forcedoutage_pm[pm].copy() * 100 - - plt.close() - f, ax = reeds.plots.map_years_months( - dfzones=dfzones, dfdata=dfdata, aggfunc=aggfunc, - title=f"Monthly {aggfunc}\nforced outage rate,\n{nicelabels.get(pm,pm)} [%]", - ) - plt.savefig(os.path.join(figpath, f'FOR_monthly-{aggfunc}-{pm}.png')) - if interactive: - plt.show() - else: - plt.close() - - -def calc_outage_scheduled(reeds_path, inputs_case, during_quarters=during_quarters): - sw = reeds.io.get_switches(inputs_case) - ## Static scheduled outage rates (for filling empties) - outage_scheduled_static = pd.read_csv( - os.path.join(inputs_case, 'outage_scheduled_static.csv'), - header=None, index_col=0, - ).squeeze(1) - outage_scheduled_static.index = outage_scheduled_static.index.str.lower() - outage_scheduled_static = ( - outage_scheduled_static - .drop(reeds.techs.ignore_techs, errors='ignore') - .copy() - ) - - ### Monthly scheduled outage rates - outage_scheduled_monthly = pd.read_csv( - os.path.join(inputs_case, 'outage_scheduled_monthly.csv'), - index_col=['prime_mover','month'], - ).squeeze(1).unstack('prime_mover') - - timeindex = pd.Series( - index=reeds.timeseries.get_timeindex(sw.resource_adequacy_years_list) - ) - outage_scheduled_pm = pd.DataFrame( - { - pm: timeindex.index.month.map(outage_scheduled_monthly[pm]).values - for pm in outage_scheduled_monthly - }, - index=timeindex.index, - dtype=np.float32, - ) - outage_scheduled_pm.columns = outage_scheduled_pm.columns.rename('prime_mover') - - outage_scheduled_tech = pm_to_tech(outage_scheduled_pm, inputs_case) - - outage_scheduled_hourly = fill_empty_techs( - df_prefill=outage_scheduled_tech, - inputs_case=inputs_case, - fillvalues_tech=outage_scheduled_static, - during_quarters=during_quarters, - ) - - return outage_scheduled_hourly - - -#%% -def main(reeds_path, inputs_case, debug=0, interactive=False): - ### Forced outages - print('Get forced outage rates') - dfforced = calc_outage_forced(reeds_path, inputs_case) - print('Write forced outage rates') - write_outages( - df=dfforced['outage_forced_hourly'], - h5path=os.path.join(inputs_case, 'outage_forced_hourly.h5'), - ) - ## Make sure it worked - reeds.io.get_outage_hourly(inputs_case, 'forced') - if debug: - plot_outage_forced( - case=os.path.abspath(os.path.join(inputs_case, '..')), - fits_forcedoutage=dfforced['fits_forcedoutage'], - forcedoutage_pm=dfforced['forcedoutage_pm'], - interactive=interactive, - ) - - ### Scheduled outages - print('Get scheduled outage rates') - outage_scheduled_hourly = calc_outage_scheduled(reeds_path, inputs_case) - print('Write scheduled outage rates') - write_outages( - df=outage_scheduled_hourly, - h5path=os.path.join(inputs_case, 'outage_scheduled_hourly.h5'), - ) - ## Make sure it worked - reeds.io.get_outage_hourly(inputs_case, 'scheduled') - - -#%%### Run it -if __name__ == '__main__': - #%% Parse args - parser = argparse.ArgumentParser( - description='Calculate temperature-dependent forced-outage rates', - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument('reeds_path', help='ReEDS-2.0 directory') - parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - # #%% Settings for testing - # reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # inputs_case = os.path.join(reeds_path, 'runs', 'v20260113_temperatureM1_Everything', 'inputs_case') - # interactive = True - # debug = 1 - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - print('Starting outage_rates.py') - #%% Run it - main(reeds_path=reeds_path, inputs_case=inputs_case) - - #%% All done - reeds.log.toc( - tic=tic, year=0, process='input_processing/outage_rates.py', - path=os.path.join(inputs_case,'..'), - ) - print('Finished outage_rates.py') diff --git a/input_processing/plantcostprep.py b/input_processing/plantcostprep.py deleted file mode 100644 index 0026ccfa..00000000 --- a/input_processing/plantcostprep.py +++ /dev/null @@ -1,504 +0,0 @@ -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import pandas as pd -import numpy as np -import os -import sys -import argparse -import datetime -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -# Time the operation of this script -tic = datetime.datetime.now() - -#%% Parse arguments -parser = argparse.ArgumentParser(description="""This file processes plant cost data by tech""") -parser.add_argument("reeds_path", help="ReEDS directory") -parser.add_argument("inputs_case", help="output directory") - -args = parser.parse_args() -reeds_path = args.reeds_path -inputs_case = args.inputs_case - -# #%% Settings for testing -#reeds_path = os.path.expanduser('~/github2/ReEDS-2.0/') -#inputs_case = os.path.join(reeds_path,'runs','v20220421_prmM0_ercot_seq','inputs_case') -#reeds_path = '/Users/apham/Documents/GitHub/ReEDS/ReEDS-2.0/' -#inputs_case = '/Users/apham/Documents/GitHub/ReEDS/ReEDS-2.0/runs/test_Pacific/inputs_case/' - -#%% Set up logger -log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), -) -print('Starting plantcostprep.py') - -#%% Inputs from switches -sw = reeds.io.get_switches(inputs_case) - - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -def deflate_func(data,case): - deflate = dollaryear.loc[dollaryear['Scenario'] == case,'Deflator'].values[0] - if 'capcost' in data.columns: - data['capcost'] *= deflate - if 'capcost_energy' in data.columns: - data['capcost_energy'] *= deflate - if 'fom' in data.columns: - data['fom'] *= deflate - data['vom'] *= deflate - if 'fom_energy' in data.columns: - data['fom_energy'] *= deflate - return data - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -dollaryear = pd.concat( - [pd.read_csv(os.path.join(inputs_case,"dollaryear_plant.csv")), - pd.read_csv(os.path.join(inputs_case,"dollaryear_consume.csv"))] -) -deflator = pd.read_csv( - os.path.join(inputs_case,"deflator.csv"), - header=0, names=['Dollar.Year','Deflator'], index_col='Dollar.Year').squeeze(1) - -dollaryear = dollaryear.merge(deflator,on="Dollar.Year",how="left") - -#%% Get ILR_ATB from scalars -scalars = reeds.io.get_scalars(inputs_case) - -#%%############### -# -- PV -- # -################## - -# Adjust cost data to 2004$ -upv = pd.read_csv(os.path.join(inputs_case,'plantchar_upv.csv')) -upv = deflate_func(upv, sw.plantchar_upv).set_index('t') - -# Prior to ATB 2020, PV costs are in $/kW_DC -# Starting with ATB 2020 cost inputs, PV costs are in $/kW_AC -# ReEDS uses DC capacity, so divide by inverter loading ratio -if '2019' not in sw.plantchar_upv: - upv[['capcost','fom','vom']] = upv[['capcost','fom','vom']] / scalars['ilr_utility'] - -# Broadcast costs to all UPV resource classes -upv_stack = pd.concat( - {'UPV_{}'.format(c): upv for c in range(1,11)}, - axis=0, names=['i','t'] -).reset_index() - -#%%######################## -# -- Other Techs -- # -########################### - -conv_in = [] -conv_techs = ['gas', 'gas_ccs', 'coal', 'coal_ccs', 'biopower', 'nuclear', 'nuclear_smr','fuelcell', 'other'] -for ct in conv_techs: - print(f"Loading plantchar_{ct}") - df = pd.read_csv(os.path.join(inputs_case,f'plantchar_{ct}.csv')) - df = deflate_func(df, sw[f'plantchar_{ct}']) - conv_in.append(df) -# combine into one set of "conventional" generators -conv = pd.concat(conv_in).reset_index(drop=True) - -ccsflex = pd.read_csv(os.path.join(inputs_case,'plantchar_ccsflex_cost.csv')) -ccsflex = deflate_func(ccsflex, sw.ccsflexscen) - -beccs = pd.read_csv(os.path.join(inputs_case,'plantchar_beccs.csv')) -beccs = deflate_func(beccs, sw.plantchar_beccs) - -h2combustion = pd.read_csv(os.path.join(inputs_case,'plantchar_h2combustion.csv')) -h2combustion = deflate_func(h2combustion, sw.plantchar_h2combustion) - -if sw.upgradescen != 'default': - upgrade = pd.read_csv(os.path.join(inputs_case,'plantchar_upgrades.csv')) - upgrade = deflate_func(upgrade, sw.upgradescen) - upgrade = upgrade.rename(columns={'capcost':'upgradecost'}) - upgrade = upgrade[['i','t','upgradecost']] - upgrade['upgradecost'] *= 1000 - upgrade['upgradecost'] = upgrade['upgradecost'].round(0).astype(int) - -#%%######################### -# -- Onshore Wind -- # -############################ - -onswinddata = pd.read_csv(os.path.join(inputs_case,'plantchar_onswind.csv')) -#We will have a 'Turbine' column. For each turbine, we assume 10 classes -onswinddata.columns= ['turbine','t','cf_mult','capcost','fom','vom'] -onswinddata['tech'] = 'ONSHORE' -class_bin_num = 10 -turb_ls = [] -for turb in onswinddata['turbine'].unique(): - turb_ls += [turb]*class_bin_num -df_class_turb = pd.DataFrame({'turbine':turb_ls, 'class':range(1, len(turb_ls) + 1)}) -onswinddata = onswinddata.merge(df_class_turb, on='turbine', how='inner') -onswinddata = onswinddata[['tech','class','t','cf_mult','capcost','fom','vom']] -onswinddata = deflate_func(onswinddata, sw.plantchar_onswind) - -#%%########################## -# -- Offshore Wind -- # -############################# - -ofswinddata = pd.read_csv(os.path.join(inputs_case,'plantchar_ofswind.csv')) -if 'Turbine' in ofswinddata: - #ATB 2024 style - #We will have a 'Turbine' column (fixed vs floating). For each turbine, we assume 5 classes - #(fixed = 1-5 and floating = 6-10) - ofswinddata.columns= ['turbine','t','cf_mult','capcost','fom','vom','rsc_mult'] - ofswinddata['tech'] = 'OFFSHORE' - class_bin_num = 5 - turb_ls = [] - for turb in ofswinddata['turbine'].unique(): - turb_ls += [turb]*class_bin_num - df_class_turb = pd.DataFrame({'turbine':turb_ls, 'class':range(1, len(turb_ls) + 1)}) - ofswinddata = ofswinddata.merge(df_class_turb, on='turbine', how='inner') - ofswind_rsc_mult = ofswinddata[['t','class','rsc_mult']].copy() - ofswind_rsc_mult['tech'] = 'wind-ofs_' + ofswind_rsc_mult['class'].astype(str) - ofswind_rsc_mult = ofswind_rsc_mult.pivot_table(index='t',columns='tech', values='rsc_mult') - ofswinddata = ofswinddata[['tech','class','t','cf_mult','capcost','fom','vom']] -else: - #ATB 2023 style - #We need to reduce to 5 classes for fixed and 5 for floating. We'll leave classes 1-5 alone (for fixed), remove classes 6,7,13, and 14, and then rename classes 8-12 to 6-10 (for floating) - ofswinddata = ofswinddata[~ofswinddata['Wind class'].isin([6,7,13,14])] - float_cond = ofswinddata['Wind class'] > 7 - ofswinddata.loc[float_cond, 'Wind class'] = ofswinddata.loc[float_cond, 'Wind class'] - 2 - ofswind_rsc_mult = ofswinddata[['Year','Wind class','rsc_mult']].copy() - ofswind_rsc_mult['tech'] = 'wind-ofs_' + ofswind_rsc_mult['Wind class'].astype(str) - ofswind_rsc_mult = ofswind_rsc_mult.rename(columns={'Year':'t'}) - ofswind_rsc_mult = ofswind_rsc_mult.pivot_table(index='t',columns='tech', values='rsc_mult') - ofswinddata = ofswinddata.drop(columns=['rsc_mult']) - ofswinddata.columns = ['tech','class','t','cf_mult','capcost','fom','vom'] -ofswinddata = deflate_func(ofswinddata, sw.plantchar_ofswind) -winddata = pd.concat([onswinddata.copy(),ofswinddata.copy()]) - -winddata.loc[winddata['tech'].str.contains('ONSHORE'),'tech'] = 'wind-ons' -winddata.loc[winddata['tech'].str.contains('OFFSHORE'),'tech'] = 'wind-ofs' -winddata['i'] = winddata['tech'] + '_' + winddata['class'].astype(str) -wind_stack = winddata[['t','i','capcost','fom','vom']].copy() - -#%%####################### -# -- Geothermal -- # -########################## - -geodata = pd.read_csv(os.path.join(inputs_case,'plantchar_geo.csv')) -geodata.columns = ['tech','class','depth','t','capcost','fom','vom'] -geodata['i'] = geodata['tech'] + '_' + geodata['depth'] + '_' + geodata['class'].astype(str) -geodata = deflate_func(geodata, sw.plantchar_geo) -geo_stack = geodata[['t','i','capcost','fom','vom']].copy() - - -#%%################ -# -- CSP -- # -################### - - -csp = pd.read_csv(os.path.join(inputs_case,'plantchar_csp.csv')) -csp = deflate_func(csp, sw.plantchar_csp) - -csp_stack = pd.DataFrame(columns=csp.columns) - -#create categories for all upv categories -for n in range(1,13): - tcsp = csp.copy() - tcsp['i'] = csp['type']+"_"+str(n) - csp_stack = pd.concat([csp_stack,tcsp],sort=False) - -csp_stack = csp_stack[['t','capcost','fom','vom','i']] - -#%%#################### -# -- Storage -- # -####################### - -battery = pd.read_csv(os.path.join(inputs_case,'plantchar_battery.csv')) -battery = deflate_func(battery, sw.plantchar_battery) - -evmc_storage = pd.read_csv(os.path.join(inputs_case,'plantchar_evmc_storage.csv')) -evmc_storage = deflate_func(evmc_storage, 'evmc_storage_' + sw.evmcscen) -evmc_shape = pd.read_csv(os.path.join(inputs_case,'plantchar_evmc_shape.csv'), dtype = {'fom':float,'vom':float,'rte':float}) -evmc_shape = deflate_func(evmc_shape, 'evmc_shape_' + sw.evmcscen) - -#%%############################ -# -- Concat all data -- # -############################### - -alldata = pd.concat([conv,upv_stack,wind_stack,geo_stack,csp_stack,battery, - evmc_storage,evmc_shape,beccs,ccsflex,h2combustion],sort=False) - -if sw.upgradescen != 'default': - alldata = pd.concat([alldata,upgrade]) - -alldata['t'] = alldata['t'].astype(int) - -#Convert from $/kw to $/MW -alldata['capcost'] = alldata['capcost']*1000 -alldata['capcost_energy'] = alldata.get('capcost_energy', 0) * 1000 -alldata['fom'] = alldata['fom']*1000 -alldata['fom_energy'] = alldata.get('fom_energy', 0) * 1000 - -alldata['capcost'] = alldata['capcost'].round(0).astype(int) -alldata['capcost_energy'] = (alldata['capcost_energy'].fillna(0)).round(0).astype(int) -alldata['fom'] = alldata['fom'].round(0).astype(int) -alldata['fom_energy'] = (alldata['fom_energy'].fillna(0)).round(0).astype(int) -alldata['vom'] = alldata['vom'].round(4) -alldata['heatrate'] = alldata['heatrate'].round(4) - -# Fill empty values with 0, melt to long format -outdata = ( - alldata.fillna(0) - .melt(id_vars=['i','t']) - ### Rename the columns so GAMS reads them as a comment - .rename(columns={'i':'*i'}) -) - -outdata = outdata.loc[outdata.variable.isin(['capcost', 'capcost_energy', - 'fom', 'fom_energy', - 'vom','heatrate','upgradecost','rte'])] - -#%%################################## -# -- Wind Capacity Factors -- # -##################################### - -windcfmult = winddata[['t','i','cf_mult']].set_index(['i','t'])['cf_mult'] -windcfmult = windcfmult.round(6) -outwindcfmult = windcfmult.reset_index().pivot_table(index='t',columns='i', values='cf_mult') - -#%%########################################################### -# -- Electrolyzer Stack Replacement Cost Adjustment -- # -############################################################## - -consume_char = pd.read_csv(os.path.join(inputs_case,'consume_char.csv')) - -# grab the electrolyzer cost 'h2_elec_stack_replace_year' years into the future -current_year = datetime.date.today().year -mask = (consume_char['*i'].isin(['electrolyzer'])) & (consume_char['parameter'].isin(['cost_cap']) & (consume_char['t'].isin([current_year+scalars['h2_elec_stack_replace_year']]))) -elec_cost_future = consume_char[mask]['value'].values[0] - -# read in financials_sys from inputs_case and take the average of all past years to get an average discount rate -financials_sys = pd.read_csv(os.path.join(inputs_case,'financials_sys.csv')) -discount_rate = np.average(financials_sys[(financials_sys['t'] <= current_year)]['d_real'].values) - -# the capital cost of electrolyzers needs to be increased by the cost to replace the stack ('h2_elec_stack_replace_perc') -# this replacement cost is represented as a percent of the capital cost of a new electrolyzer in that year. This cost occurs 'h2_elec_stack_replace_year' years in the future so we discount it. -mask = (consume_char['*i'].isin(['electrolyzer'])) & (consume_char['parameter'].isin(['cost_cap'])) -consume_char.loc[mask, 'value'] = consume_char[mask]['value'] + round( (elec_cost_future * scalars['h2_elec_stack_replace_perc'])/(discount_rate**scalars['h2_elec_stack_replace_year']) ,3) - -#%%############################### -# -- DR Shed -- # -################################## -# Capital cost multipliers for DR Shed vary by state and year -# Input cost data are state-level and are assigned to model region resolution here -# We assume all regions within the same state have uniform costs -dr_shed = pd.read_csv(os.path.join(inputs_case,'plantchar_dr_shed.csv'), index_col=0).round(6) -# FOM & VOM inputs are also state-level and need to be disaggregated -fom = pd.read_csv(os.path.join(inputs_case,'plantchar_dr_shed_fom.csv'), index_col=0).round(6) -vom = pd.read_csv(os.path.join(inputs_case,'plantchar_dr_shed_vom.csv'), index_col=0).round(6) -# If there are no DR shed data for regions being run, write dr_shed_capcostmult.csv -if dr_shed.empty: - dr_shed_capcost_mult = dr_shed.copy() - dr_shed_fom_regional = fom.copy() - dr_shed_vom_regional = vom.copy() -else: - state2r = pd.read_csv(os.path.join(inputs_case,'disagg_state_lpf.csv'),usecols=['state','r']) - # Map each unique state to all r values within that state - state2r = state2r.groupby('state')['r'].unique().apply(list).to_dict() - - def disaggregate_to_regions(data, state2r): - regional_data = {} - for st in data['r'].unique(): - bas_in_st = state2r[st] - data2r = {} - for r in bas_in_st: - copy_state = data.loc[data['r'] == st].copy() - copy_state['r'] = r - data2r[r] = copy_state - regional_data[st] = pd.concat(data2r.values()) - return pd.concat(regional_data.values()) - - dr_shed_capcost_mult = disaggregate_to_regions(dr_shed, state2r) - dr_shed_fom_regional = disaggregate_to_regions(fom, state2r) - dr_shed_vom_regional = disaggregate_to_regions(vom, state2r) - -#%%############################### -# -- Other Technologies -- # -################################## - -ccsflex_perf = pd.read_csv(os.path.join(inputs_case,'plantchar_ccsflex_perf.csv'),index_col=0).round(6) -hydro = pd.read_csv(os.path.join(inputs_case,'plantchar_hydro.csv'), index_col=0).round(6) -degrade = pd.read_csv( - os.path.join(inputs_case,'degradation_annual.csv'), - header=None) -degrade.columns = ['i','rate'] -degrade = reeds.techs.expand_GAMS_tech_groups(degrade) -degrade = degrade.set_index('i').round(6) - -#%%################################## -# -- PV+Battery Cost Model -- # -##################################### - -# Get PVB designs -pvb_ilr = pd.read_csv( - os.path.join(inputs_case, 'pvb_ilr.csv'), - header=0, names=['pvb_type','ilr'], index_col='pvb_type').squeeze(1) -pvb_bir = pd.read_csv( - os.path.join(inputs_case, 'pvb_bir.csv'), - header=0, names=['pvb_type','bir'], index_col='pvb_type').squeeze(1) -# Get PV and battery $/Wac costs for PVB -battery_USDperWac = ( - battery.loc[battery.i==f'battery_li'].set_index('t').capcost - + float(sw.GSw_PVB_Dur) - * battery.loc[battery.i==f'battery_li'].set_index('t').capcost_energy -) -UPV_defaultILR_USDperWac = upv.capcost * scalars['ilr_utility'] -# Get cost-sharing assumptions -pvbvalues = pd.read_csv(os.path.join(inputs_case,'plantchar_pvb.csv'), index_col='parameter') -fixed_ac_noninverter_cost_USDperWac = ( - pvbvalues.loc['fixed','value'] - * deflator[pvbvalues.loc['fixed','dollaryear']] - # Input units are in $/Wac, so convert to $/MWac to match units used in ReEDS - * 1000 -) - -def get_pvb_cost( - UPV_defaultILR_USDperWac, battery_USDperWac, - ILR_user=1.8, BIR_user=0.5, - inverter_cost_ac_fraction=0.05, - fixed_ac_noninverter_cost_USDperWac=0.0455, - ILR_ATB=1.3, -): - # Inverter cost is taken as a fixed fraction of UPV AC cost - inverter_USDperWac = UPV_defaultILR_USDperWac * inverter_cost_ac_fraction - # Standalone PV $/Wdc is [$/Wac] * [Wac/Wdc], where [$/Wac] is the full - # $/Wac minus the inverter cost and AC fixed costs - UPV_USDperWdc = ( - UPV_defaultILR_USDperWac - - inverter_USDperWac - - fixed_ac_noninverter_cost_USDperWac - ) / ILR_ATB - # Standalone PV $/Wac for the user-defined ILR: [$/Wac] + [$/Wdc] * ILR - UPV_USDperWac = ( - inverter_USDperWac - + fixed_ac_noninverter_cost_USDperWac - + UPV_USDperWdc * ILR_user - ) - # PVB system cost - PVB_USDperWac = ( - ## PV - UPV_USDperWac - ## Battery sized relative to PV - + BIR_user * ( - battery_USDperWac - ## Minus fixed AC costs - - fixed_ac_noninverter_cost_USDperWac - - inverter_USDperWac - ) - ) - # Standalone system cost for two systems with same DC capacity; - # here the battery needs its own inverter and AC fixed costs, - # so we don't subtract them out as we did above for PVB - standalone_USDperWac = ( - # PV - UPV_USDperWac - # Battery sized relative to PV - + BIR_user * battery_USDperWac - ) - ### Savings vs standalone - # savings_fraction = 1 - PVB_USDperWac / standalone_USDperWac - # savings_USDperWac = standalone_USDperWac - PVB_USDperWac - pvb_cost_fraction = PVB_USDperWac / standalone_USDperWac - ### Outputs - out = { - 'pvb': PVB_USDperWac, - 'standalone_pv_dc': UPV_USDperWdc, - 'standalone_pv_ac': UPV_USDperWac, - 'standalone_both': standalone_USDperWac, - 'inverter': inverter_USDperWac, - 'pvb_cost_fraction': pvb_cost_fraction, - } - return out - -#%% Calculate PVB cost fraction for each PVB design -pvb = {} -for i in sw['GSw_PVB_Types'].split('_'): - pvb['pvb{}'.format(i)] = get_pvb_cost( - UPV_defaultILR_USDperWac=UPV_defaultILR_USDperWac, - battery_USDperWac=battery_USDperWac, - ILR_user=pvb_ilr['pvb{}'.format(i)], - BIR_user=pvb_bir['pvb{}'.format(i)], - inverter_cost_ac_fraction=pvbvalues.loc['inverter_fraction','value'], - fixed_ac_noninverter_cost_USDperWac=fixed_ac_noninverter_cost_USDperWac, - ILR_ATB=scalars['ilr_utility'], - )['pvb_cost_fraction'] -pvb = pd.concat(pvb, axis=1) - - -## Create Electric DAC scenario output -# For electric DAC, we assume a sorbent system: https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a -# FYI, for DAC-gas we assume a solvent system: https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987 -dac_elec = pd.read_csv(os.path.join(inputs_case,'dac_elec.csv')) -dac_elec = deflate_func(dac_elec, f'dac_elec_{sw.dacscen}').round(4) -# Fill empty values with 0, melt to long format -outdac_elec = ( - dac_elec.fillna(0) - .melt(id_vars=['i','t'],value_vars=['capcost','fom','vom','conversionrate']) - ### Rename the columns so GAMS reads them as a comment - .rename(columns={'i':'*i'}) -) - - -## Create Gas DAC scenario output -# For DAC-gas we assume a solvent system: https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987 -dac_gas = pd.read_csv(os.path.join(inputs_case,'dac_gas.csv')) -dac_gas = deflate_func(dac_gas, f'dac_gas_{sw.GSw_DAC_Gas_Case}').round(4) - - -#%%################################################### -# -- Cost Adjustment for cost_upgrade Techs -- # -###################################################### -upgrade_mult_mid = pd.read_csv(os.path.join(inputs_case,"upgrade_mult_mid.csv")) -upgrade_mult_advanced = pd.read_csv(os.path.join(inputs_case,"upgrade_mult_advanced.csv")) -upgrade_mult_conservative = pd.read_csv(os.path.join(inputs_case,"upgrade_mult_conservative.csv")) - -if int(sw.GSw_UpgradeCost_Mult) == 0: - upgrade_mult = upgrade_mult_mid -elif int(sw.GSw_UpgradeCost_Mult) == 1: - upgrade_mult = upgrade_mult_advanced -elif int(sw.GSw_UpgradeCost_Mult) == 2: - upgrade_mult = upgrade_mult_conservative -elif int(sw.GSw_UpgradeCost_Mult) == 3: - upgrade_mult = 1 -elif int(sw.GSw_UpgradeCost_Mult) == 4: - upgrade_mult = 1 + (1-upgrade_mult_mid) -elif int(sw.GSw_UpgradeCost_Mult) == 5: - upgrade_mult = 1.2 - -#%%########################### -# -- Data Write-Out -- # -############################## - -print('writing plant data to:', os.getcwd()) -outdata.to_csv(os.path.join(inputs_case,'plantcharout.csv'), index=False) -upv.cf_improvement.round(3).to_csv(os.path.join(inputs_case,'pv_cf_improve.csv'), header=False) -outwindcfmult.to_csv(os.path.join(inputs_case,'windcfmult.csv')) -ccsflex_perf.to_csv(os.path.join(inputs_case,'ccsflex_perf.csv')) -consume_char.to_csv(os.path.join(inputs_case,'consume_char.csv'),index=False) -hydro.to_csv(os.path.join(inputs_case,'hydrocapcostmult.csv')) -dr_shed_capcost_mult.to_csv(os.path.join(inputs_case,'dr_shed_capcostmult.csv')) -dr_shed_fom_regional.to_csv(os.path.join(inputs_case,'plantchar_dr_shed_fom.csv')) -dr_shed_vom_regional.to_csv(os.path.join(inputs_case,'plantchar_dr_shed_vom.csv')) -ofswind_rsc_mult.to_csv(os.path.join(inputs_case,'ofswind_rsc_mult.csv')) -degrade.to_csv(os.path.join(inputs_case,'degradation_annual.csv'),header=False) -pvb.to_csv(os.path.join(inputs_case,'pvbcapcostmult.csv')) -upgrade_mult.round(4).to_csv(os.path.join(inputs_case,'upgrade_mult_final.csv'), index=False) -outdac_elec.to_csv(os.path.join(inputs_case,'consumechardac.csv'), index=False) -dac_gas.to_csv(os.path.join(inputs_case,'dac_gas.csv'), index=False) - -reeds.log.toc(tic=tic, year=0, process='input_processing/plantcostprep.py', - path=os.path.join(inputs_case,'..')) - -print('Finished plantcostprep.py') diff --git a/input_processing/recf.py b/input_processing/recf.py deleted file mode 100644 index 78faedc3..00000000 --- a/input_processing/recf.py +++ /dev/null @@ -1,508 +0,0 @@ -''' -This script handles the modifications of static inputs for the first solve year. These inputs -include the 8760 renewable energy capacity factor (RECF) profiles. RECF and resource data for -various technologies are combined into single files for output: - -Resources: - - Creates a resource-to-(i,r,ccreg) lookup table for use in hourly_writesupplycurves.py - and Augur - - Add the distributed PV resources -RECF: - - Add the distributed PV recf profiles - - Sort the columns in recf to be in the same order as the rows in resources - - Scale distributed resource CF profiles by distribution loss factor and tiein loss factor -''' - -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import datetime -import numpy as np -import os -import pandas as pd -import sys -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -def csp_dispatch(cfcsp, sm=2.4, storage_duration=10): - """ - Use a simple no-foresight heuristic to dispatch CSP. - Excess energy from the solar field (i.e. energy above the max plant power output) - is sent to storage, and energy in storage is dispatched as soon as possible. - - --- Inputs --- - cfcsp: hourly energy output of solar field [fraction of max field output] - sm: solar multiple [solar field max output / plant max power output] - storage_duration: hours of storage as multiple of plant max power output - """ - ### Calculate derived dataframes - ## Field energy output as fraction of plant max output - dfcf = cfcsp * sm - ## Excess energy as fraction of plant max output - clipped = (dfcf - 1).clip(lower=0) - ## Remaining generator capacity after direct dispatch (can be used for storage dispatch) - headspace = (1 - dfcf).clip(lower=0) - ## Direct generation from solar field - direct_dispatch = dfcf.clip(upper=1) - - ### Numpy arrays - clipped_val = clipped.values - headspace_val = headspace.values - hours = range(len(clipped_val)) - storage_dispatch = np.zeros(clipped_val.shape) - ## Need one extra storage hour at the end, though it doesn't affect dispatch - storage_energy_hourstart = np.zeros((len(hours)+1, clipped_val.shape[1])) - - ### Loop over all hours and simulate dispatch - for h in hours: - ### storage dispatch is... - storage_dispatch[h] = np.where( - clipped_val[h], - ## zero if there's clipping in hour - 0, - ## otherwise... - np.where( - headspace_val[h] > storage_energy_hourstart[h], - ## storage energy at start of hour if more headspace than energy - storage_energy_hourstart[h], - ## headspace if more storage energy than headspace - headspace_val[h] - ) - ) - ### storage energy at start of next hour is... - storage_energy_hourstart[h+1] = np.where( - clipped_val[h], - ## storage energy in current hour plus clipping if clipping - storage_energy_hourstart[h] + clipped_val[h], - ## storage energy in current hour minus dispatch if not clipping - storage_energy_hourstart[h] - storage_dispatch[h] - ) - storage_energy_hourstart[h+1] = np.where( - storage_energy_hourstart[h+1] > storage_duration, - ## clip storage energy to storage duration if energy > duration - storage_duration, - ## otherwise no change - storage_energy_hourstart[h+1] - ) - - ### Format as dataframe and calculate total plant dispatch - storage_dispatch = pd.DataFrame( - index=clipped.index, columns=clipped.columns, data=storage_dispatch) - - total_dispatch = direct_dispatch + storage_dispatch - - return total_dispatch - - -def calculate_class_region_cf_hourly( - inputs_case, - tech, - weather_years, - tz_out='Etc/GMT+6' -): - if not tz_out.startswith('Etc/GMT'): - raise ValueError("tz_out must be formatted as 'Etc/GMT[+/-][number].") - - # Get supply curve information - df_sc = reeds.io.assemble_supplycurve( - os.path.join(inputs_case, f'supplycurve_{tech}.csv'), - case=os.path.dirname(inputs_case), - agg=True, - ) - # Calculate total capacity for each class-region pair - df_sc['class_region'] = ( - df_sc['class'].astype(str) + '|' + df_sc['region'] - ) - class_region_cap = ( - df_sc.groupby('class_region') - ['capacity'] - .sum() - ) - # Note we calculate on a per-year basis to avoid loading - # all of the site-level hourly data in memory at once - df_list = [] - for year in weather_years: - # Get site-level hourly CFs - weather_year_site_cf_hourly = reeds.io.get_site_cf_hourly( - tech=tech, - year=year, - case=inputs_case, - ) - # Downselect to relevant sites - weather_year_site_cf_hourly = weather_year_site_cf_hourly[df_sc.index] - # Calculate the capacity-weighted average CF for each class-region pair - weather_year_class_region_cf_hourly = ( - weather_year_site_cf_hourly.mul(df_sc['capacity']) - .rename(columns=df_sc['class_region']) - .groupby(axis=1, level=0) - .sum() - .div(class_region_cap) - ) - # For timezone conversion, we need a few hours of CF data for the next - # year. If we don't have data for the next year, assume the profile - # for the last day of this year is repeated for the first day of - # the next year and append to the end of the set of profiles. - next_year = year + 1 - if next_year not in weather_years: - next_year_first_day_data = ( - weather_year_class_region_cf_hourly.tail(24) - ) - next_year_first_day_data.index += pd.Timedelta(days=1) - weather_year_class_region_cf_hourly = ( - pd.concat([ - weather_year_class_region_cf_hourly, - next_year_first_day_data - ]) - ) - # Append to list of yearly data - df_list.append(weather_year_class_region_cf_hourly) - - # Concatenate all CF data - class_region_cf_hourly = pd.concat(df_list) - - # Shift timezone from UTC to tz_out - utc_offset = -1 * int(tz_out.split('Etc/GMT')[1]) - class_region_cf_hourly = ( - class_region_cf_hourly.shift(utc_offset) - .tz_localize(None) - .tz_localize(tz_out) - ) - class_region_cf_hourly = class_region_cf_hourly.loc[( - class_region_cf_hourly.index.year.isin(weather_years) - )] - class_region_cf_hourly.index.names = ['datetime'] - - return class_region_cf_hourly - - -def calculate_regional_distpv_cf(inputs_case, cap_min=0.0001): - # Get county-to-region mapping - county2zone = reeds.io.get_county2zone(os.path.dirname(inputs_case)) - county2zone.index = 'p' + county2zone.index - # Read county-level distpv capacity factors and - # downselect to relevant counties - county_distpv_cf = reeds.io.get_distpv_cf_hourly() - county_distpv_cf = county_distpv_cf[county2zone.index] - # Read county- and model region-level distpv capacities to use - # in capacity-weighted averages - sw = reeds.io.get_switches(inputs_case) - county_distpv_cap = reeds.io.get_distpv_capacities(distpvscen=sw.distpvscen) - regional_distpv_cap = reeds.io.get_distpv_capacities(inputs_case) - # Increment hourly cluster year if there is no data for the provided year - GSw_HourlyClusterYear = sw.GSw_HourlyClusterYear - if GSw_HourlyClusterYear not in county_distpv_cap: - GSw_HourlyClusterYear = str(int(GSw_HourlyClusterYear) + 1) - # Downselect to relevant counties and hourly cluster year values. - # Some counties (and regions defined by small groups of counties) have zero - # distpv capacity. We assign these an arbitrarily small capacity for the - # weighting to avoid division-by-zero errors. - county_distpv_cap = ( - county_distpv_cap.loc[county_distpv_cf.columns, GSw_HourlyClusterYear] - .clip(lower=cap_min) - ) - regional_distpv_cap = regional_distpv_cap[GSw_HourlyClusterYear].clip(lower=cap_min) - # Calculate capacity-weighted average capacity factors by calculating - # regional distpv generation and dividing by each region's distpv capacity - regional_distpv_cf = ( - county_distpv_cf.mul(county_distpv_cap) - .rename(columns=county2zone) - .groupby(axis=1, level=0) - .sum() - .div(regional_distpv_cap) - ) - - return regional_distpv_cf - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== -def main(reeds_path, inputs_case): - print('Starting recf.py') - - # #%% Settings for testing - # reeds_path = os.path.realpath(os.path.join(os.path.dirname(__file__),'..')) - # inputs_case = os.path.join( - # reeds_path,'runs','v20250129_cspfixM0_ISONE','inputs_case') - - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - resource_adequacy_years = sw['resource_adequacy_years_list'] - GSw_CSP_Types = [int(i) for i in sw.GSw_CSP_Types.split('_')] - GSw_PVB_Types = sw.GSw_PVB_Types - GSw_PVB = int(sw.GSw_PVB) - - - #%%### Load inputs - ### Load the input parameters - scalars = reeds.io.get_scalars(inputs_case) - ### distloss - distloss = scalars['distloss'] - - ### Load spatial hierarchy - hierarchy = pd.read_csv( - os.path.join(inputs_case,'hierarchy.csv') - ).rename(columns={'*r':'r'}).set_index('r') - hierarchy_original = ( - pd.read_csv(os.path.join(inputs_case, 'hierarchy_original.csv')) - .rename(columns={'ba':'r'}) - .set_index('r') - ) - ### Add ccreg column with the desired hierarchy level - if sw['capcredit_hierarchy_level'] == 'r': - hierarchy['ccreg'] = hierarchy.index.copy() - hierarchy_original['ccreg'] = hierarchy_original.index.copy() - else: - hierarchy['ccreg'] = hierarchy[sw.capcredit_hierarchy_level].copy() - hierarchy_original['ccreg'] = hierarchy_original[sw.capcredit_hierarchy_level].copy() - ### Map regions to new ccreg's - r2ccreg = hierarchy['ccreg'] - - # Get technology subsets - tech_table = pd.read_csv( - os.path.join(inputs_case,'tech-subset-table.csv'), index_col=0).fillna(False).astype(bool) - techs = {tech:list() for tech in list(tech_table)} - for tech in techs.keys(): - techs[tech] = tech_table[tech_table[tech]].index.values.tolist() - techs[tech] = [x.lower() for x in techs[tech]] - temp_save = [] - temp_remove = [] - # Interpreting GAMS syntax in tech-subset-table.csv - for subset in techs[tech]: - if '*' in subset: - temp_remove.append(subset) - temp = subset.split('*') - temp2 = temp[0].split('_') - temp_low = pd.to_numeric(temp[0].split('_')[-1]) - temp_high = pd.to_numeric(temp[1].split('_')[-1]) - temp_tech = '' - for n in range(0,len(temp2)-1): - temp_tech += temp2[n] - if not n == len(temp2)-2: - temp_tech += '_' - for c in range(temp_low,temp_high+1): - temp_save.append('{}_{}'.format(temp_tech,str(c))) - for subset in temp_remove: - techs[tech].remove(subset) - techs[tech].extend(temp_save) - vre_dist = techs['VRE_DISTRIBUTED'] - - # ------- Read in the static inputs for this run ------- - - ### Onshore Wind - df_windons = calculate_class_region_cf_hourly( - inputs_case, - 'wind-ons', - resource_adequacy_years - ) - df_windons.columns = ['wind-ons_' + col for col in df_windons] - ### Don't do aggregation in this case, so make a 1:1 lookup table - lookup = pd.DataFrame({'ragg':df_windons.columns.values}) - lookup['r'] = lookup.ragg.map(lambda x: x.rsplit('|',1)[1]) - lookup['i'] = lookup.ragg.map(lambda x: x.rsplit('|',1)[0]) - - ### Offshore Wind - if int(sw['GSw_OfsWind']) != 0: - df_windofs = calculate_class_region_cf_hourly( - inputs_case, - 'wind-ofs', - resource_adequacy_years - ) - df_windofs.columns = ['wind-ofs_' + col for col in df_windofs] - - ### UPV - df_upv = calculate_class_region_cf_hourly( - inputs_case, - 'upv', - resource_adequacy_years - ) - df_upv.columns = ['upv_' + col for col in df_upv] - - # If DistPV is turned off, create an empty dataframe with the same index as df_upv to concat - if int(sw['GSw_distpv']) == 0: - df_distpv = pd.DataFrame(index=df_upv.index) - else: - df_distpv = calculate_regional_distpv_cf(inputs_case) - df_distpv.columns = [f"distpv|{col}" for col in df_distpv.columns] - - ### CSP - # If CSP is turned off, create an empty dataframe with the same index as df_upv to concat - if int(sw['GSw_CSP']) == 0: - cspcf = pd.DataFrame(index=df_upv.index) - else: - cspcf = reeds.io.read_file( - os.path.join(inputs_case, 'recf_csp.h5'), - parse_timestamps=True, - ) - - ### Format PV+battery profiles - # Get the PVB types - pvb_ilr = pd.read_csv( - os.path.join(inputs_case, 'pvb_ilr.csv'), - header=0, names=['pvb_type','ilr'], index_col='pvb_type').squeeze(1) - df_pvb = {} - # Override GSw_PVB_Types if GSw_PVB is turned off - GSw_PVB_Types = ( - [int(i) for i in GSw_PVB_Types.split('_')] if int(GSw_PVB) - else [] - ) - for pvb_type in GSw_PVB_Types: - ilr = int(pvb_ilr['pvb{}'.format(pvb_type)] * 100) - # If PVB uses same ILR as UPV then use its profile - infile = 'recf_upv' if ilr == scalars['ilr_utility'] * 100 else f'recf_upv_{ilr}AC' - df_pvb[pvb_type] = reeds.io.read_file( - os.path.join(inputs_case,infile+'.h5'), - parse_timestamps=True, - ) - df_pvb[pvb_type].columns = [f'pvb{pvb_type}_{c}' - for c in df_pvb[pvb_type].columns] - df_pvb[pvb_type].index = df_upv.index.copy() - - ### Concat RECF data - recf = pd.concat( - [df_windons, df_windofs, df_upv, df_distpv] - + [df_pvb[pvb_type] for pvb_type in df_pvb], - sort=False, axis=1, copy=False) - - ### Downselect RECF data to resource adequacy and weather years - recf = recf.loc[recf.index.year.isin(resource_adequacy_years)] - - ### Add the other recf techs to the resources lookup table - toadd = pd.DataFrame({'ragg': [c for c in recf.columns if c not in lookup.ragg.values]}) - toadd['r'] = [c.rsplit('|', 1)[1] for c in toadd.ragg.values] - toadd['i'] = [c.rsplit('|', 1)[0] for c in toadd.ragg.values] - resources = ( - pd.concat([lookup, toadd], axis=0, ignore_index=True) - .rename(columns={'ragg':'resource','r':'area','i':'tech'}) - .sort_values('resource').reset_index(drop=True) - ) - - #%%%############################################# - # -- Performing Resource Modifications -- # - ################################################# - if int(sw['GSw_OfsWind']) == 0: - wind_ofs_resource = ['wind-ofs_' + str(n) for n in range(1,16)] - resources = resources[~resources['tech'].isin(wind_ofs_resource)] - - # Sorting profiles of resources to match the order of the rows in resources - resources = resources.sort_values(['resource','area']) - recf = recf.reindex(labels=resources['resource'].drop_duplicates(), axis=1, copy=False) - - ### Scale up distpv by 1/(1-distloss) - recf.loc[ - :, resources.loc[resources.tech.isin(vre_dist),'resource'].values - ] /= (1 - distloss) - - # Set the column names for resources to match ReEDS-2.0 - resources['ccreg'] = resources.area.map(r2ccreg) - resources.rename(columns={'area':'r','tech':'i'}, inplace=True) - resources = resources[['r','i','ccreg','resource']] - - - #%%### Concentrated solar thermal power (CSP) - ### Create CSP resource label for each CSP type (labeled by "tech" as csp1, csp2, etc) - csptechs = [f'csp{c}' for c in GSw_CSP_Types] - csp_resources = pd.concat({ - tech: - pd.DataFrame({ - 'resource': cspcf.columns, - 'r': cspcf.columns.map(lambda x: x.split('|')[1]), - 'class': cspcf.columns.map(lambda x: x.split('|')[0]), - }) - for tech in csptechs - }, axis=0, names=('tech',)).reset_index(level='tech') - - csp_resources = ( - csp_resources - .assign(i=csp_resources['tech'] + '_' + csp_resources['class'].astype(str)) - .assign(resource=csp_resources['tech'] + '_' + csp_resources['resource']) - .assign(ccreg=csp_resources.r.map(r2ccreg)) - [['i','r','resource','ccreg']] - ) - ###### Simulate CSP dispatch for each design - ### Get solar multiples - sms = {tech: scalars[f'csp_sm_{tech.strip("csp")}'] for tech in csptechs} - ### Get storage durations - storage_duration = pd.read_csv( - os.path.join(inputs_case,'storage_duration.csv'), header=None, index_col=0).squeeze(1) - ## All CSP resource classes have the same duration for a given tech, so just take the first one - durations = {tech: storage_duration[f'csp{tech.strip("csp")}_1'] for tech in csptechs} - ### Run the dispatch simulation for modeled regions - - csp_system_cf = pd.concat({ - tech: csp_dispatch(cspcf, sm=sms[tech], storage_duration=durations[tech]) - for tech in csptechs - }, axis=1) - ## Collapse multiindex column labels to single strings - csp_system_cf.columns = ['_'.join(c) for c in csp_system_cf.columns] - - ### Add CSP to RE output dataframes - csp_system_cf = csp_system_cf.loc[recf.index] - recf = pd.concat([recf, csp_system_cf], axis=1) - resources = pd.concat([resources, csp_resources], axis=0) - - #%% Check for errors - nulls = recf.isnull().sum() - missing = nulls.loc[nulls > 0] - if len(missing): - print(missing) - err = f"Missing RECF values for {len(missing)} columns" - raise ValueError(err) - - - #%%########################### - # -- Data Write-Out -- # - ############################## - - reeds.io.write_profile_to_h5(recf.astype(np.float16), 'recf.h5', inputs_case) - resources.to_csv(os.path.join(inputs_case,'resources.csv'), index=False) - ### Write the CSP solar field CF (no SM or storage) for hourly_writetimeseries.py - cspcf = cspcf.rename(columns=dict(zip(cspcf.columns, [f'csp_{i}' for i in cspcf.columns]))) - reeds.io.write_profile_to_h5(cspcf.astype(np.float32), 'csp.h5', inputs_case) - ### Overwrite the original hierarchy.csv based on capcredit_hierarchy_level - hierarchy.rename_axis('*r').to_csv( - os.path.join(inputs_case, 'hierarchy.csv'), index=True, header=True) - pd.Series(hierarchy.ccreg.unique()).to_csv( - os.path.join(inputs_case,'ccreg.csv'), index=False, header=False) - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if __name__ == '__main__': - # Time the operation of this script - tic = datetime.datetime.now() - - ### Parse arguments - parser = argparse.ArgumentParser( - description='Create run-specific hourly profiles', - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument('reeds_path', help='ReEDS-2.0 directory') - parser.add_argument('inputs_case', help='ReEDS-2.0/runs/{case}/inputs_case directory') - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - #%% Run it - main(reeds_path=reeds_path, inputs_case=inputs_case) - - reeds.log.toc(tic=tic, year=0, process='input_processing/recf.py', - path=os.path.join(inputs_case,'..')) - - print('Finished recf.py') diff --git a/input_processing/transmission.py b/input_processing/transmission.py deleted file mode 100644 index 913ea293..00000000 --- a/input_processing/transmission.py +++ /dev/null @@ -1,641 +0,0 @@ -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import geopandas as gpd -import pandas as pd -import numpy as np -import os -import sys -import datetime -from pathlib import Path -sys.path.append(str(Path(__file__).parent.parent)) -import reeds -tic = datetime.datetime.now() - -#%% Parse arguments -parser = argparse.ArgumentParser(description="Format and write climate inputs") -parser.add_argument('reeds_path', help='ReEDS directory') -parser.add_argument('inputs_case', help='output directory (inputs_case)') - -args = parser.parse_args() -reeds_path = args.reeds_path -inputs_case = args.inputs_case - -# #%% Settings for testing ### -# reeds_path = reeds.io.reeds_path -# inputs_case = str(Path(reeds_path,'runs','v20260409_itlM0_WECC_county','inputs_case')) - -#%%################# -### FIXED INPUTS ### - -decimals = 5 -drop_canmex = True -dollar_year = 2004 -weight = 'cost' - -costcol = f'USD{dollar_year}perMW' - -#%% Set up logger -log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), -) -print('Starting transmission.py', flush=True) - -#%% Inputs from switches -sw = reeds.io.get_switches(inputs_case) - -## networksource must end in a 4-digit year indicating the year represented by the network -trans_init_year = int(sw.GSw_TransNetworkSource[-4:]) - -valid_regions = {} -for level in ['r','itlgrp','transgrp']: - valid_regions[level] = pd.read_csv( - os.path.join(inputs_case, f'val_{level}.csv'), header=None).squeeze(1).tolist() - - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== -def get_trancap_init(case, networksource='NARIS2024', level='r'): - """ - AC capacity is defined for each direction and calculated using the scripts at - https://github.nrel.gov/ReEDS/TSC - """ - sw = reeds.io.get_switches(case) - trancap_init_ac = ( - reeds.inputs.get_itls(case, level=level, GSw_ZoneSet=sw.GSw_ZoneSet) - [['r', 'rr', 'MW_forward', 'MW_reverse']] - .assign(trtype='AC') - ) - ### DEPRECATED: p19 is islanded with NARIS transmission data, so connect it manually - if ( - (networksource == 'NARIS2024') - and (level != 'transgrp') - and ('p19' in valid_regions['r']) - and ('p20' in valid_regions['r']) - ): - trancap_init_ac = pd.concat([ - trancap_init_ac, - pd.Series({ - 'r':'p19', - 'rr':'p20', - 'MW_forward':0.001, - 'MW_reverse':0.001, - 'trtype':'AC', - }).to_frame().T - ], ignore_index=True) - - ### DC - if level == 'r': - ## transgrp capacity is only defined for AC - hvdc = reeds.inputs.map_hvdc_lines_to_interfaces(case).assign(trtype='LCC') - b2b = reeds.inputs.get_b2b(case).assign(trtype='B2B') - ## DC capacity is only defined in one direction, - ## so duplicate it for the opposite direction - trancap_init_nonac_undup = pd.concat([hvdc, b2b])[['r', 'rr', 'trtype', 'MW']] - trancap_init_nonac = pd.concat([ - trancap_init_nonac_undup, - trancap_init_nonac_undup.rename(columns={'r':'rr', 'rr':'r'}) - ], axis=0) - else: - trancap_init_nonac = pd.DataFrame(columns=['r', 'rr', 'trtype', 'MW']) - - ### Initial trading limit, using contingency levels specified by contingency level - ### (but assuming full capacity of DC is available for both energy and capcity) - dfout = ( - pd.concat( - [ - ## AC - pd.concat([ - ## Forward direction - (trancap_init_ac[['r', 'rr', 'trtype', 'MW_forward']] - .rename(columns={'MW_forward':'MW'})), - ## Reverse direction - (trancap_init_ac[['r', 'rr', 'trtype', 'MW_reverse']] - .rename(columns={'r':'rr', 'rr':'r', 'MW_reverse':'MW'})) - ], axis=0), - ## DC - trancap_init_nonac[['r', 'rr', 'trtype', 'MW']] - ], - axis=0 - ) - ## Drop entries with zero capacity - .replace(0.,np.nan).dropna() - .groupby(['r', 'rr', 'trtype']).sum().reset_index() - ) - dfout = dfout.loc[ - dfout['r'].isin(valid_regions[level]) - & dfout['rr'].isin(valid_regions[level]) - ].copy() - - ## Get alias for level (e.g. rr, transgrpp) - levell = level + level[-1] - return dfout.rename(columns={'r':level, 'rr':levell}) - - -def calculate_adjacent_routes(dfzones): - routes_adjacent = dfzones.copy() - routes_adjacent['r_adj'] = routes_adjacent.apply( - axis=1, - func=lambda x: ( - routes_adjacent.loc[( - routes_adjacent.touches(x['geometry']) - | routes_adjacent.overlaps(x['geometry']) - )] - .index - .values - .tolist() - ) - ) - # Reformat so that each row represents a pair of regions - routes_adjacent = ( - routes_adjacent.drop(columns='geometry') - .explode('r_adj') - .reset_index(names=['r']) - .rename(columns={'r': '*r', 'r_adj': 'rr'}) - [['*r', 'rr']] - .dropna() - ) - - return routes_adjacent - - -def calculate_co2_storage_routes(dfzones, co2_storage_sites): - # Determine the storage sites that are within 200 miles - # of each region's transmission endpoint - region_centroids = ( - gpd.GeoDataFrame( - dfzones[['x', 'y']], - geometry=gpd.points_from_xy(dfzones.x, dfzones.y), - crs=dfzones.crs - ) - [['geometry']] - .rename_axis(index='*r') - .reset_index() - ) - region_centroids['cs'] = region_centroids.apply( - axis=1, - func=lambda x: ( - co2_storage_sites.loc[( - co2_storage_sites.distance(x['geometry']) / 1609.34 <= 200 - )] - ['cs'] - .tolist() - ) - ) - - # Calculate the lengths of the spurlines between regions and storage sites, - # excluding routes not completely within the U.S. - routes_cs = ( - region_centroids.explode('cs') - .merge( - co2_storage_sites[['cs', 'geometry']], - on='cs', - suffixes=('_region', '_site') - ) - .assign( - geometry=lambda x: ( - gpd.GeoSeries(x['geometry_region']) - .shortest_line(gpd.GeoSeries(x['geometry_site'])) - ) - ) - [['*r', 'cs', 'geometry']] - ) - routes_cs = gpd.GeoDataFrame(routes_cs, geometry='geometry', crs=dfzones.crs) - routes_cs = routes_cs.loc[( - routes_cs.within( - reeds.io.get_dfmap(levels=['country'])['country'].loc['USA','geometry'] - ) - | (routes_cs.length == 0) - )] - routes_cs['distance_m'] = routes_cs.length - routes_cs['miles'] = (routes_cs['distance_m'] / 1609.34).round(2) - - return routes_cs - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -#%% Limits on PRMTRADE across nercr boundaries -if not int(sw.GSw_PRM_NetImportLimit): - ## No limit - firm_import_limit = pd.DataFrame(columns=['*nercr','t','fraction']).set_index(['*nercr','t']) -else: - limits = pd.Series( - {int(i.split('_')[0]): i.split('_')[1] for i in sw.GSw_PRM_NetImportLimitScen.split('/')} - ) - - solveyears = pd.read_csv( - os.path.join(inputs_case,'modeledyears.csv') - ).columns.astype(int).tolist() - startyear = min(solveyears) - endyear = max(solveyears) - allyears = range(startyear, max(endyear, limits.index.max())+1) - - ## calculate the historical net_firm_import fraction for each region and drop negative values - peak_net_imports = pd.read_csv( - os.path.join(inputs_case,'peak_net_imports.csv'), - index_col=['nercr'] - ) - net_firm_import_frac = ( - peak_net_imports.MW / peak_net_imports.MW_TotalDemand - ).clip(lower=0) - nercrs = net_firm_import_frac.index - - _dfout = {} - for key, val in limits.items(): - ## If 'hist' is in GSw_PRM_NetImportLimitScen, - ## all years up until that year use the historical regional max - if val == 'hist': - for y in range(startyear, key+1): - _dfout[y] = net_firm_import_frac - ## If 'histmax', all prior years use the historical max across all regions - elif val == 'histmax': - for y in range(startyear, key+1): - _dfout[y] = net_firm_import_frac.clip(lower=net_firm_import_frac.max()) - else: - ## Input values are percentages so convert to fractions - _dfout[key] = pd.Series(index=nercrs, data=float(val) / 100) - - firm_import_limit = ( - pd.concat(_dfout, names=('t',)).unstack('nercr') - ## Linear interpolation between values; flat projections before and after - .reindex(allyears).interpolate('linear').bfill().ffill() - .loc[solveyears] - .unstack('t').rename('fraction').rename_axis(['*nercr','t']) - ) - -firm_import_limit.to_csv(os.path.join(inputs_case, 'firm_import_limit.csv')) - - -#%% Load the transmission scalars -scalars = reeds.io.get_scalars(inputs_case) -### Put some in dicts for easier access -tranloss_permile = { - 'AC': scalars['tranloss_permile_ac'], - ### B2B converters are AC-AC/DC-DC/AC-AC, so use AC per-mile losses - 'B2B': scalars['tranloss_permile_ac'], - 'LCC': scalars['tranloss_permile_dc'], - 'VSC': scalars['tranloss_permile_dc'], -} -tranloss_fixed = { - 'AC': 1 - scalars['converter_efficiency_ac'], - 'B2B': 1 - scalars['converter_efficiency_lcc'], - 'LCC': 1 - scalars['converter_efficiency_lcc'], - 'VSC': 1 - scalars['converter_efficiency_vsc'], -} - - -#%% Get single-link distances and losses -interface_params = pd.read_csv( - os.path.join(inputs_case,'transmission_distance.csv'), -) -interface_params['r_rr'] = interface_params.r + '_' + interface_params.rr - -# Apply the distance multiplier -interface_params['miles'] = interface_params['miles'] * float(sw.GSw_TransSquiggliness) - -# Make sure there are no duplicates -if interface_params[['r','rr']].duplicated().sum(): - print( - interface_params.loc[ - interface_params[['r','rr']].duplicated(keep=False) - ].sort_values(['r','rr']) - ) - raise Exception('Duplicate entries in transmission_distance.csv') - -### Calculate losses -def getloss(row, trtype='AC'): - """ - Fixed losses are entered as per-endpoint values (e.g. for each AC/DC converter station - on a LCC DC line). There are two endpoints per line, so multiply fixed losses by 2. - Note that this approach only applies for LCC DC lines; VSC AC/DC losses are applied later. - """ - return row.miles * tranloss_permile[trtype] + tranloss_fixed[trtype] * 2 - -trtypes = ['AC', 'LCC', 'B2B', 'VSC'] -interface_params = pd.concat( - { - trtype: - interface_params.assign(loss=interface_params.apply(getloss, args=(trtype,), axis=1)) - for trtype in trtypes - }, - axis=0, - names=('trtype',), -).reset_index(level='trtype').set_index(['r','rr','trtype']) - - -#%% Include distances for existing lines -transmission_distance = interface_params.miles.copy() - -#%% Write the line-specific transmission FOM costs [$/MW/year] -trans_fom_region_mult = int(scalars['trans_fom_region_mult']) -trans_fom_frac = scalars['trans_fom_frac'] - -### For simplicity we just take the unweighted average base cost across -### the four regions for which we have transmission cost data. -### Future work should identify a better assumption. -rev_transcost_base = pd.read_csv( - os.path.join(inputs_case,'rev_transmission_basecost.csv'), - header=[0], skiprows=[1], -).replace({'500ACsingle':'AC','500DCbipole':'LCC'}).set_index('Voltage') - -transfom_USDperMWmileyear = { - trtype: ( - rev_transcost_base.loc[trtype][['TEPPC','SCE','MISO','Southeast']].mean() - * trans_fom_frac - ) - for trtype in ['AC','LCC'] -} -### B2B is treated like (AC line)-(AC/DC converter)-(AC/DC converter)-(AC line) so uses AC line FOM -transfom_USDperMWmileyear['B2B'] = transfom_USDperMWmileyear['AC'] -transfom_USDperMWmileyear['VSC'] = transfom_USDperMWmileyear['LCC'] - -if trans_fom_region_mult: - ### Multiply line-specific $/MW by FOM fraction to get $/MW/year - transmission_line_fom = interface_params[costcol] * trans_fom_frac - ### Use regional average * distance_initial for existing lines - append = transmission_distance.loc[ - transmission_distance.reset_index().trtype.isin( - ['AC','LCC','B2B','VSC']).set_axis(transmission_distance.index) - ] -else: - ### Multiply $/MW/mile/year by distance [miles] to get $/MW/year for ALL lines - transmission_line_fom = ( - transmission_distance.reset_index().trtype.map(transfom_USDperMWmileyear) - * transmission_distance.values - ).set_axis(transmission_distance.index).rename('USDperMWyear') - - -#%%### Write files for ReEDS (adding * to make GAMS read column names as comment) -### transmission_distance -transmission_distance.round(3).reset_index().rename(columns={'r':'*r'}).to_csv( - os.path.join(inputs_case,'transmission_miles.csv'), index=False) - -### tranloss -tranloss = interface_params['loss'].reset_index() -tranloss.round(decimals).rename(columns={'r':'*r'}).to_csv( - os.path.join(inputs_case,'tranloss.csv'), index=False, header=True) - -### transmission_line_fom -transmission_line_fom.round(2).rename_axis(('*r','rr','trtype')).to_csv( - os.path.join(inputs_case,'transmission_line_fom.csv')) - -#%% Write the initial capacities -case = Path(inputs_case).parent -trancap_init = {} -for captype, level in [ - ('energy', 'r'), - ('transgroup', 'transgrp'), -]: - trancap_init[captype] = get_trancap_init( - case=case, networksource=sw.GSw_TransNetworkSource, level=level) - ### TEMPORARY 20260402: Drop county interfaces with no distance/cost - if (level == 'r') and (sw.GSw_RegionResolution in ['county', 'mixed']): - indices = ['r', 'rr', 'trtype'] - drop = ( - trancap_init[captype] - .merge(transmission_line_fom.reset_index(), on=indices, how='left') - ) - drop = list(drop.loc[drop.USDperMWyear.isnull(), indices].itertuples(index=False)) - trancap_init[captype] = trancap_init[captype].set_index(indices).drop(drop).reset_index() - trancap_init[captype].rename(columns={level:'*'+level}).round(3).to_csv( - os.path.join(inputs_case,f'trancap_init_{captype}.csv'), - index=False, - ) -trancap_init['energy'].rename(columns={'r':'*r'}).round(3).to_csv( - os.path.join(inputs_case,'trancap_init_prm.csv'), - index=False, -) -### TEMPORARY 20260402: Skip itlgrp functionality until we fix it -# ### Also write itlgrp capacity -# trancap_itlgrp = trancap_init['energy'].copy() -# ## Map counties to itlgrp's -# hierarchy_itlgrp = pd.read_csv(os.path.join(inputs_case, 'hierarchy_itlgrp.csv')) -# itl_d = dict(zip(hierarchy_itlgrp['*r'], hierarchy_itlgrp['itlgrp'])) -# for r in ['r', 'rr']: -# trancap_itlgrp[r] = trancap_itlgrp[r].map(lambda x: itl_d.get(x,x)) -# trancap_itlgrp.rename(columns={'r':'*itlgrp', 'rr':'itlgrpp'}).round(3).to_csv( -# os.path.join(inputs_case, 'trancap_init_itlgrp.csv'), -# index=False, -# ) - - -#%%### Future transmission capacity -## note that '0' is used as a filler value in the t column for firstyear_trans, which is defined -## in inputs/scalars.csv. So we replace it whenever we load a transmission_capacity_future file. -trancap_fut = ( - pd.concat( - [ - pd.read_csv( - os.path.join(inputs_case, 'transmission_capacity_future_baseline.csv'), - comment='#', - ), - pd.read_csv( - os.path.join(inputs_case, 'transmission_capacity_future.csv'), - comment='#', - ) - ], - axis=0, - ignore_index=True, - ) - .astype({'t': int}) - .drop(['Notes', 'notes', 'Note', 'note'], axis=1, errors='ignore') - .replace({'t': {0: int(scalars['firstyear_trans_longterm'])}}) -) - -### Drop prospective AC lines from years <= trans_init_year -trancap_fut = trancap_fut.drop( - trancap_fut.loc[ - (trancap_fut.t <= trans_init_year) - & (trancap_fut.trtype == 'AC') - ].index, -).copy() - -trancap_fut.rename(columns={'r':'*r'}).astype({'t':int}).round(3).to_csv( - os.path.join(inputs_case,'trancap_fut.csv'), index=False) - - -#%%### Transmission upgrade supply curve -transmission_cost_ac = pd.read_csv( - os.path.join(inputs_case, 'transmission_cost_ac.csv') -) -### Interfaces are always defined with the zones sorted in lexicographic order -reverse_interfaces = transmission_cost_ac.loc[ - transmission_cost_ac.apply(lambda row: row.r > row.rr, axis=1) -] -for i, row in reverse_interfaces.iterrows(): - transmission_cost_ac.loc[i, ['r', 'rr']] = transmission_cost_ac.loc[i, ['rr', 'r']].values - transmission_cost_ac.loc[i, ['USD2004perMW_forward', 'USD2004perMW_reverse']] = ( - transmission_cost_ac.loc[i, ['USD2004perMW_reverse', 'USD2004perMW_forward']].values - ) - -_test = transmission_cost_ac.apply(lambda row: row.r < row.rr, axis=1) -if not _test.all(): - print(transmission_cost_ac.loc[~_test]) - err = ( - "Must have r < rr in AC transmission cost inputs but the interfaces " - "listed above are out of order" - ) - raise ValueError(err) - -labels = { - 'binwidth_USD2004': 'binwidth', - 'USD2004perMW_forward': 'forward', - 'USD2004perMW_reverse': 'reverse', -} -for col, label in labels.items(): - transmission_cost_ac[['r','rr','tscbin',col]].rename(columns={'r':'*r'}).round(2).to_csv( - os.path.join(inputs_case, f'tsc_{label}.csv'), - index=False, - ) -transmission_cost_ac.tscbin.drop_duplicates().to_csv( - os.path.join(inputs_case, 'tscbin.csv'), - index=False, - header=False, -) - - -#%% DC and B2B transmission cost -## Get DC line cost -transmission_cost_dc = pd.read_csv(os.path.join(inputs_case, 'transmission_cost_dc.csv')) - -## B2B is: (zone center)--------(AC/DC converter)(DC/AC converter)--------(zone center) -## ^ AC line ^ AC line -## so use AC per-mile costs. -b2b_links = trancap_init['energy'].loc[ - (trancap_init['energy'].trtype=='B2B') - & (trancap_init['energy'].r < trancap_init['energy'].rr) -].set_index(['r','rr']).index -## Take the weighted average of the whole supply curve (for the default 500 kV assumption -## the supply curve only has one bin per interface, so it doesn't matter; when we add the -## full supply curve, we'll need to include entries for these B2B-containing interfaces). -df = transmission_cost_ac.set_index(['r','rr']).loc[b2b_links].copy() -df['cost_weighted'] = ( - df.binwidth_USD2004 - * df.USD2004perMW_forward -) -transmission_cost_b2b = ( - df.groupby(['r','rr','tscbin']).cost_weighted.sum() - / df.groupby(['r','rr','tscbin']).binwidth_USD2004.sum() -).reset_index(level='tscbin', drop=True).rename('USD2004perMW').reset_index() -## Add the reverse direction and write it -transmission_cost_b2b = pd.concat([ - transmission_cost_b2b, - transmission_cost_b2b.rename(columns={'r':'rr', 'rr':'r'}) -]) - -### Write the combined cost table -transmission_cost_nonac = ( - pd.concat({ - 'LCC': transmission_cost_dc, - 'B2B': transmission_cost_b2b, - 'VSC': transmission_cost_dc, - }, names=('trtype','drop')) - .reset_index('drop', drop=True) - .reset_index() - .rename(columns={'r':'*r'}) - [['*r','rr','trtype','USD2004perMW']] -) -transmission_cost_nonac.round(2).to_csv( - os.path.join(inputs_case, 'transmission_cost_nonac.csv'), - index=False, -) - - -#%%### Hurdle rates -hurdle_levels = [1, 2] -cost_hurdle_intra = ( - pd.read_csv(os.path.join(inputs_case, 'cost_hurdle_intra.csv')) - .rename(columns={'t':'*t'}).set_index('*t').round(3) -) -cost_hurdle_rate = { - i: ( - cost_hurdle_intra[sw[f'GSw_TransHurdleLevel{i}']] if int(sw.GSw_TransHurdleRate) - else pd.Series(name='region').rename_axis('*t') - ) - for i in hurdle_levels -} -for i in hurdle_levels: - cost_hurdle_rate[i].to_csv(os.path.join(inputs_case, f'cost_hurdle_rate{i}.csv')) - - -#%%### H2 pipeline cost multipliers -# Calculate H2 pipeline cost multipliers by dividing the [$/mile] cost of DC transmission -# between each pair of regions by the minimum interface [$/mile] cost for DC transmission -# and subtracting 1 to get a fractional adder (which is then added to 1 in b_inputs.gms) -fpath = os.path.join(inputs_case, 'pipeline_cost_mult.csv') -if len(transmission_cost_nonac): - dc_cost_permile = ( - transmission_cost_nonac.rename(columns={'*r':'r'}) - .set_index(['trtype','r','rr']).loc['LCC'].squeeze(1) - / interface_params.xs('LCC', 0, 'trtype').miles - ) - pipeline_cost_mult = dc_cost_permile.rename('multiplier') / dc_cost_permile.min() - 1 - - pipeline_cost_mult.reset_index().rename(columns={'r':'*r'}).round(3).to_csv( - fpath, - index=False, - ) -else: - pd.DataFrame(columns=['*r','rr','multiplier']).to_csv(fpath, index=False) - - -# Get model regions -dfzones = reeds.io.get_dfmap( - os.path.dirname(inputs_case), - levels=['r'], - exclude_water_areas=True -)['r'] - -#%%### Adjacent regions -# Determine which pairs of model regions are adjacent to each other -routes_adjacent = calculate_adjacent_routes(dfzones) -routes_adjacent.to_csv( - os.path.join(inputs_case,'routes_adjacent.csv'), - index=False -) - -#%%### CO2 storage sites -# Determine spurline routes from model regions to carbon storage sites -co2_storage_sites = reeds.io.get_co2_storage_sites() -routes_cs = calculate_co2_storage_routes(dfzones, co2_storage_sites) -routes_cs[['*r', 'cs']].to_csv( - os.path.join(inputs_case, 'r_cs.csv'), index=False -) -routes_cs[['*r', 'cs', 'miles']].to_csv( - os.path.join(inputs_case,'r_cs_distance_mi.csv'), - index=False -) - -# Determine sites that have valid routes to model regions -val_cs = pd.Series(routes_cs['cs'].unique()) -val_cs.to_csv(os.path.join(inputs_case, 'val_cs.csv'), header=False, index=False) - -# Subset CO2 site characteristics data to valid sites -co2_site_char = pd.read_csv(os.path.join(inputs_case, 'co2_site_char.csv')) -co2_site_char = co2_site_char.loc[co2_site_char['cs'].isin(val_cs)] -co2_site_char.to_csv(os.path.join(inputs_case, 'co2_site_char.csv'), index=False) - -# Create WKT file of region-to-site spurlines -r_cs_spurlines = ( - routes_cs.loc[routes_cs['distance_m'] > 0] - .rename(columns={'*r': 'ba_str', 'cs': 'FmnID'}) - .to_crs('EPSG:4326') - .assign(WKT=lambda x: x['geometry'].to_wkt()) - [['ba_str', 'FmnID', 'distance_m', 'WKT']] -) -r_cs_spurlines.to_csv( - os.path.join(inputs_case,'ctus_r_cs_spurlines_200mi.csv'), - index=False -) - -#%% Finish the timer -reeds.log.toc(tic=tic, year=0, process='input_processing/transmission.py', - path=os.path.join(inputs_case,'..')) -print('Finished transmission.py', flush=True) diff --git a/input_processing/writecapdat.py b/input_processing/writecapdat.py deleted file mode 100644 index aa5dde86..00000000 --- a/input_processing/writecapdat.py +++ /dev/null @@ -1,896 +0,0 @@ -""" -The purpose of this script is to gather individual generator data from the -NEMS generator database and organize this data into various categories, such as: - - Non-RSC Existing Capacity - - Non-RSC Prescribed Capacity - - RSC Existing Capacity - - RSC Prescribed Capacity - - SMR Existing Capacity - - Retirement Data - - Generator Retirements - - Wind Retirements - - Non-RSC Retirements - - Hydro Capacity Adjustment Factors - ccseasons - - Waterconstraint Indexing - - Canadian Imports -The categorized datasets are then written out to various csv files for use -throughout the ReEDS model. - -Some notes on the NEMS database: -* Capacity is assumed to retire at the BEGINNING of 'RetireYear'. So if a row's - 'RetireYear' is 2015, that capacity is assumed to retire at 2014-12-31T23:59:59. -""" - -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== -import argparse -import datetime -import numpy as np -import os -import sys -import pandas as pd -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -#%%################# -### FIXED INPUTS ### - -# Generator database column selections: -Sw_onlineyearcol = 'StartYear' - - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== -def create_rsc_wsc(gendb,TECH,scalars,startyear): - - rsc_wsc = gendb.loc[(gendb['tech'].isin(TECH['rsc_wsc'])) & - (gendb[Sw_onlineyearcol] < startyear) & - (gendb['RetireYear'] > startyear) - ] - - rsc_wsc = rsc_wsc[['r','tech','summer_power_capacity_MW']].rename(columns={'tech':'i','summer_power_capacity_MW':'value'}) - # Multiply all PV capacities by ILR - for j,row in rsc_wsc.iterrows(): - if row['i'] == 'upv': - rsc_wsc.loc[j,'value'] *= scalars['ilr_utility'] - - return rsc_wsc - -#%% =========================================================================== -### --- SUPPLEMENTAL DATA --- -### =========================================================================== - -######################### -### STATIC DICTIONARY ### -''' -This dictionary must be placed at the module level of this script to be used with the -create_rsc_wsc() function in aggregate_regions -''' - -TECH = { - 'capnonrsc': [ - 'coaloldscr', 'coalolduns', 'biopower', 'coal-igcc', - 'coal-new', 'gas-cc', 'gas-ct', 'lfill-gas', - 'nuclear', 'o-g-s', 'battery_li', 'pumped-hydro' - ], - 'capnonrsc_energy': [ - 'battery_li' - ], - 'prescribed_nonRSC': [ - 'coal-new', 'lfill-gas', 'gas-ct', 'o-g-s', 'gas-cc', - 'hydED', 'hydEND', 'hydND', 'hydNPND', 'hydUD', 'hydUND', - 'geothermal', 'biopower', 'coal-igcc', 'nuclear', - 'battery_li','pumped-hydro','coaloldscr', - ], - 'prescribed_nonRSC_energy': [ - 'battery_li', - ], - 'storage' : ['battery_li', 'pumped-hydro' - ], - 'rsc_all': ['upv','pvb','csp-ns'], - 'rsc_csp': ['csp-ns'], - 'rsc_wsc': ['upv','pvb','csp-ns','csp-ws','wind-ons','wind-ofs', - 'geohydro_allkm','egs_allkm'], - 'prsc_all': ['upv','pvb','csp-ns','csp-ws'], - 'prsc_upv': ['upv','pvb'], - 'prsc_w': ['wind-ons','wind-ofs'], - 'prsc_csp': ['csp-ns','csp-ws'], - 'prsc_geo': ['geohydro_allkm','egs_allkm'], - 'retirements': [ - 'coalolduns', 'o-g-s', 'hydED', 'hydEND', 'gas-ct', 'lfill-gas', - 'coaloldscr', 'biopower', 'gas-cc', 'coal-new', - 'battery_li','nuclear', 'pumped-hydro', 'coal-igcc', - ], - 'retirements_energy': [ - 'battery_li' - ], - 'windret': ['wind-ons'], - 'georet': ['geohydro_allkm','egs_allkm'], - # This is not all technologies that do not having cooling, but technologies - # that are (or could be) in the plant database. - 'no_cooling': [ - 'upv', 'pvb', 'gas-ct', 'geohydro_allkm','egs_allkm', - 'battery_li', 'pumped-hydro', 'pumped-hydro-flex', - 'hydUD', 'hydUND', 'hydD', 'hydND', 'hydSD', 'hydSND', 'hydNPD', - 'hydNPND', 'hydED', 'hydEND', 'wind-ons', 'wind-ofs', - ], -} - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== - -def main(reeds_path, inputs_case, agglevel, regions): - - # #%% Settings for testing - #reeds_path = "/Users/apham/Documents/GitHub/ReEDS/ReEDS-2.0/" - #inputs_case = os.path.join(reeds_path,'runs','test_newNEMS_OR_water','inputs_case') - - - ######################### - ### SUPPLEMENTAL DATA ### - - quartershorten = {'spring':'spri','summer':'summ','fall':'fall','winter':'wint'} - - hotcold_months = {'NOV':'cold', 'DEC':'cold', 'JAN':'cold', 'FEB':'cold', - 'JUN':'hot', 'JUL':'hot', 'AUG':'hot' - } - - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - retscen = sw.retscen - GSw_WaterMain = int(sw.GSw_WaterMain) - GSw_PVB = int(sw.GSw_PVB) - startyear = int(sw.startyear) - endyear = int(sw.endyear) - - scalars = reeds.io.get_scalars(inputs_case) - - years = pd.read_csv( - os.path.join(inputs_case,'modeledyears.csv') - ).columns.astype(int).values.tolist() - - #################### - ### DICTIONARIES ### - - COLNAMES = { - 'capnonrsc': ( - ['tech','coolingwatertech','r','ctt','wst','summer_power_capacity_MW'], - ['i','coolingwatertech','r','ctt','wst','value'] - ), - 'capnonrsc_energy': ( - ['tech','r','energy_capacity_MWh'], - ['i','r','value'] - ), - 'prescribed_nonRSC': ( - [Sw_onlineyearcol,'r','tech','coolingwatertech','ctt','wst','summer_power_capacity_MW'], - ['t','r','i','coolingwatertech','ctt','wst','value'] - ), - 'prescribed_nonRSC_energy': ( - [Sw_onlineyearcol,'r','tech','coolingwatertech','ctt','wst','energy_capacity_MWh'], - ['t','r','i','coolingwatertech','ctt','wst','value'] - ), - 'rsc': ( - ['tech','r','ctt','wst','summer_power_capacity_MW'], - ['i','r','ctt','wst','value'] - ), - 'rsc_wsc': ( - ['r','tech','summer_power_capacity_MW'], - ['r','i','value'] - ), - 'prsc_upv': ( - [Sw_onlineyearcol,'r','tech','summer_power_capacity_MW'], - ['t','r','i','value'] - ), - 'prsc_w': ( - [Sw_onlineyearcol,'r','tech','summer_power_capacity_MW'], - ['t','r','i','value'] - ), - 'prsc_csp': ( - [Sw_onlineyearcol,'r','tech','ctt','wst','summer_power_capacity_MW'], - ['t','r','i','ctt','wst','value'] - ), - 'prsc_geo': ( - [Sw_onlineyearcol,'r','tech','summer_power_capacity_MW'], - ['t','r','i','value'] - ), - 'retirements': ( - [retscen,'r','tech','coolingwatertech','ctt','wst','summer_power_capacity_MW'], - ['t','r','i','coolingwatertech','ctt','wst','value'] - ), - 'retirements_energy': ( - [retscen,'r','tech','energy_capacity_MWh'], - ['t','r','i','value'] - ), - 'windret': ( - ['r','tech','RetireYear','summer_power_capacity_MW'], - ['r','i','t','value'] - ), - 'georet': ( - ['r','tech','RetireYear','summer_power_capacity_MW'], - ['r','i','t','value'] - ), - } - - - #%% - print('Importing generator database:') - gdb_use = pd.read_csv(os.path.join(inputs_case,'unitdata.csv'), low_memory=False) - - - rcol_dict = {'county':'FIPS', 'ba':'reeds_ba'} - # Create the 'r_col' column - if agglevel in ['county','ba']: - r_col = rcol_dict[agglevel] - gdb_use['r'] = gdb_use[r_col].copy() - # Filter generator database to regions that match the spatial resolution of the run - gdb_use = gdb_use[gdb_use['r'].isin(regions)] - elif agglevel == 'aggreg': - rb_aggreg = pd.read_csv(os.path.join(inputs_case,'rb_aggreg.csv'), index_col='ba').squeeze(1) - gdb_use = gdb_use.assign(r=gdb_use.reeds_ba.map(rb_aggreg)) - # Filter generator database to regions that match the spatial resolution of the run - gdb_use = gdb_use[gdb_use['r'].isin(regions)] - - # If PVB is turned off, consider all PVB as UPV and battery_li for existing and prescribed builds - # If PVB is turned on, consider all PVB as 'pvb' - if GSw_PVB == 0: - gdb_use['tech'] = gdb_use['tech'].replace('pvb_battery','battery_li') - gdb_use['tech'] = gdb_use['tech'].replace('pvb_pv','upv') - else: - gdb_use['tech'] = gdb_use['tech'].replace('pvb_battery','pvb') - gdb_use['tech'] = gdb_use['tech'].replace('pvb_pv','pvb') - - - # Consider all DUPV as UPV for existing and prescribed builds. - gdb_use['tech'] = gdb_use['tech'].replace('dupv','upv') - - # Change tech category of hydro that will be prescribed to use upgrade tech - # This is a coarse assumption that all recent new hydro is upgrades - # Existing hydro techs (hydED/hydEND) specifically refer to hydro that exists in startyear - # Future work could incorporate this change into unit database creation and possibly - # use data from ORNL HydroSource to assign a more accurate hydro category. - gdb_use.loc[ - (gdb_use['tech']=='hydEND') & (gdb_use[Sw_onlineyearcol] >= startyear), 'tech' - ] = 'hydUND' - gdb_use.loc[ - (gdb_use['tech']=='hydED') & (gdb_use[Sw_onlineyearcol] >= startyear), 'tech' - ] = 'hydUD' - - # We model csp-ns (CSP No Storage) as upv throughout ReEDS, but switch it back for reporting. - # So save the csp-ns capacity separately, then rename it. - csp_units = ( - gdb_use.loc[(gdb_use['tech']=='csp-ns') & (gdb_use['RetireYear'] > startyear)] - .groupby(['r','StartYear','RetireYear']).summer_power_capacity_MW.sum() - .reset_index() - ) - if len(csp_units): - cap_cspns = ( - pd.concat( - {i: pd.Series( - [row.summer_power_capacity_MW]*(row.RetireYear - row.StartYear + 2), - index=range(row.StartYear, row.RetireYear + 2) - ) for (i,row) in csp_units.iterrows()}, - axis=1) - .rename(columns=csp_units['r']).fillna(0) - .groupby(axis=1, level=0).sum() - .stack().replace(0,np.nan).dropna() - .rename_axis(['t','*r']).reorder_levels(['*r','t']).rename('MWac') - ) - cap_cspns = ( - cap_cspns.loc[cap_cspns.index.get_level_values('t') >= startyear].copy()) - else: - cap_cspns = pd.DataFrame(columns=['*r','t','MWac']).set_index(['*r','t']) - # csp-ns capacity is MWac measured at the power block, while PV capacity is MWdc, - # so multiply csp-ns capacity by the ILR [MWdc/MWac] of PV - gdb_use.loc[gdb_use['tech']=='csp-ns','summer_power_capacity_MW'] *= scalars['ilr_utility'] - # Rename csp-ns to upv - gdb_use.loc[gdb_use['tech']=='csp-ns','coolingwatertech'] = ( - gdb_use.loc[gdb_use['tech']=='csp-ns','coolingwatertech'] - .map(lambda x: x.replace('csp-ns','upv')) - ) - gdb_use.loc[gdb_use['tech']=='csp-ns','tech'] = 'upv' - - # If using cooling water, set the coolingwatertech of technologies with no - # cooling to be the same as the tech - if GSw_WaterMain == 1: - gdb_use.loc[gdb_use['tech'].isin(TECH['no_cooling']), - 'coolingwatertech'] = gdb_use.loc[gdb_use['tech'].isin(TECH['no_cooling']), - 'tech'] - - #%%################################## - # -- All Existing Capacity -- # - ##################################### - - ### Used as the starting point for intra-zone network reinforcement costs - # Power capacity in MW - poi_cap_init = gdb_use.loc[(gdb_use[Sw_onlineyearcol] < startyear) & - (gdb_use['RetireYear'] > startyear) - ].groupby('r').summer_power_capacity_MW.sum().rename('MW').round(3) - poi_cap_init.index = poi_cap_init.index.rename('*r') - - #%%###################################### - # -- non-RSC Existing Capacity -- # - ######################################### - - print('Gathering non-RSC Existing Capacity...') - capnonrsc = gdb_use.loc[(gdb_use['tech'].isin(TECH['capnonrsc'])) & - (gdb_use[Sw_onlineyearcol] < startyear) & - (gdb_use['RetireYear'] > startyear) - ] - capnonrsc = capnonrsc[COLNAMES['capnonrsc'][0]] - capnonrsc.columns = COLNAMES['capnonrsc'][1] - capnonrsc = capnonrsc.groupby(COLNAMES['capnonrsc'][1][:-1]).sum().reset_index() - - capnonrsc_energy = gdb_use.loc[(gdb_use['tech'].isin(TECH['capnonrsc_energy'])) & - (gdb_use[Sw_onlineyearcol] < startyear) & - (gdb_use['RetireYear'] > startyear) - ] - capnonrsc_energy = capnonrsc_energy[COLNAMES['capnonrsc_energy'][0]] - capnonrsc_energy.columns = COLNAMES['capnonrsc_energy'][1] - capnonrsc_energy = capnonrsc_energy.groupby(COLNAMES['capnonrsc_energy'][1][:-1]).sum().reset_index() - - - #%%######################################## - # -- non-RSC Prescribed Capacity -- # - ########################################### - - print('Gathering non-RSC Prescribed Capacity...') - ### prescribed power capacity - prescribed_nonRSC = gdb_use.loc[(gdb_use['tech'].isin(TECH['prescribed_nonRSC'])) & - (gdb_use[Sw_onlineyearcol] >= startyear) - ] - prescribed_nonRSC = prescribed_nonRSC[COLNAMES['prescribed_nonRSC'][0]] - prescribed_nonRSC.columns = COLNAMES['prescribed_nonRSC'][1] - # Remove ctt and wst data from storage, set coolingwatertech to tech type ('i') - for j, row in prescribed_nonRSC.iterrows(): - if row['i'] in TECH['storage']: - prescribed_nonRSC.loc[j,['ctt','wst','coolingwatertech']] = ['n','n',row['i']] - - - if int(sw.GSw_NuclearDemo)==1: - # Load in demo data and stack it on prescribed non-RSC - demo = pd.read_csv( - os.path.join(inputs_case,'demonstration_plants.csv')).drop("notes", axis=1) - # Filter demonstration plants to regions in function call - demo = demo[demo['r'].isin(regions)] - prescribed_nonRSC = pd.concat([prescribed_nonRSC,demo],sort=False) - - prescribed_nonRSC = ( - prescribed_nonRSC.groupby(COLNAMES['prescribed_nonRSC'][1][:-1]).sum().reset_index()) - - ### prescribed energy capacity - prescribed_nonRSC_energy = gdb_use.loc[(gdb_use['tech'].isin(TECH['prescribed_nonRSC_energy'])) & - (gdb_use[Sw_onlineyearcol] >= startyear) - ] - prescribed_nonRSC_energy = prescribed_nonRSC_energy[COLNAMES['prescribed_nonRSC_energy'][0]] - prescribed_nonRSC_energy.columns = COLNAMES['prescribed_nonRSC_energy'][1] - # Remove ctt and wst data from storage, set coolingwatertech to tech type ('i') - for j, row in prescribed_nonRSC_energy.iterrows(): - if row['i'] in TECH['storage']: - prescribed_nonRSC_energy.loc[j,['ctt','wst','coolingwatertech']] = ['n','n',row['i']] - - prescribed_nonRSC_energy = ( - prescribed_nonRSC_energy.groupby(COLNAMES['prescribed_nonRSC_energy'][1][:-1]).sum().reset_index()) - - #%%################################## - # -- RSC Existing Capacity -- # - ##################################### - ''' - The following are RSC tech that are treated differently in the model - ''' - print('Gathering RSC Existing Capacity...') - # DUPV and UPV values are collected at the same time here: - caprsc = gdb_use.loc[(gdb_use['tech'].isin(TECH['rsc_all'][:2])) & - (gdb_use[Sw_onlineyearcol] < startyear) & - (gdb_use['RetireYear'] > startyear) - ] - caprsc = caprsc[COLNAMES['rsc'][0]] - caprsc.columns = COLNAMES['rsc'][1] - caprsc = caprsc.groupby(COLNAMES['rsc'][1][:-2]).value.sum().reset_index() - # Multiply all PV capacities by ILR - caprsc['value'] = caprsc['value'] * scalars['ilr_utility'] - - # Add existing CSP builds: - # Note: Since CSP data is affected by GSw_WaterMain, it must be dealt with - # separate from the other RSC tech (UPV, DUPV, wind, etc) - csp = gdb_use.loc[(gdb_use['tech'].isin(TECH['rsc_csp'])) & - (gdb_use[Sw_onlineyearcol] < startyear) & - (gdb_use['RetireYear'] > startyear) - ] - csp = csp[COLNAMES['rsc'][0]] - csp.columns = COLNAMES['rsc'][1] - csp = csp.groupby(COLNAMES['rsc'][1][:-1]).sum().reset_index() - if GSw_WaterMain == 1: - csp['i'] = csp['i'] + '_' + csp['ctt'] + '_' + csp['wst'] - csp.drop('wst', axis=1, inplace=True) - - # Add existing hydro builds: - gendb = gdb_use[["tech", 'r', "summer_power_capacity_MW"]] - gendb = gendb[(gendb.tech == 'hydED') | (gendb.tech == 'hydEND')] - - hyd = gendb.groupby(['tech', 'r']).sum() \ - .reset_index() \ - .rename({"tech":"i","summer_power_capacity_MW":"value"}, axis=1) - - hyd['ctt'] = 'n' - - # Concat all RSC Existing Data to one dataframe: - caprsc = pd.concat([caprsc, csp, hyd]) - - # Export Existing RSC data specifically used in writesupplycurves.py - rsc_wsc = create_rsc_wsc(gdb_use, TECH=TECH, scalars=scalars,startyear=startyear) - - # Create geoexist.csv and copy to inputs_case - geoexist = gdb_use.loc[(gdb_use['tech'].isin(['geohydro_allkm','egs_allkm'])) & - (gdb_use[Sw_onlineyearcol] < startyear) & - (gdb_use['RetireYear'] > startyear) - ] - geoexist = (geoexist[['tech','r','summer_power_capacity_MW']] - .rename(columns={'tech':'*i','summer_power_capacity_MW':'MW'}) - ) - geoexist = geoexist.groupby(['*i','r']).sum().reset_index() - # Rename generic geothermal tech category to geohydro_allkm_1 - geoexist['*i'] = 'geohydro_allkm_1' - - - #%%#################################### - # -- RSC Prescribed Capacity -- # - ####################################### - - print('Gathering RSC Prescribed Capacity...') - # DUPV and UPV values are collected at the same time here: - pupv = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_upv'])) & - (gdb_use[Sw_onlineyearcol] >= startyear) - ] - pupv = pupv[COLNAMES['prsc_upv'][0]] - pupv.columns = COLNAMES['prsc_upv'][1] - pupv = pupv.groupby(['t','r','i']).sum().reset_index() - # Multiply all PV capacities by ILR - pupv['value'] = pupv['value'] * scalars['ilr_utility'] - - # Load in wind builds: - pwind = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_w'])) & - (gdb_use[Sw_onlineyearcol] >= startyear) - ] - pwind = pwind[COLNAMES['prsc_w'][0]] - pwind.columns = COLNAMES['prsc_w'][1] - - pwind = pwind.groupby(['t','r','i']).sum().reset_index() - pwind.sort_values(['t','r'], inplace=True) - - # Add prescribed csp builds: - # Note: Since csp is affected by GSw_WaterMain, it must be dealt with separate - # from the other RSC tech (dupv, upv, wind, etc) - pcsp = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_csp'])) & - (gdb_use[Sw_onlineyearcol] >= startyear) - ] - pcsp = pcsp[COLNAMES['prsc_csp'][0]] - pcsp.columns = COLNAMES['prsc_csp'][1] - if GSw_WaterMain == 1: - pcsp['i'] = np.where(pcsp['i']=='csp-ws',pcsp['i']+'_'+pcsp['ctt']+'_'+pcsp['wst'],'csp-ws') - - # Load in geo builds: - pgeo = gdb_use.loc[(gdb_use['tech'].isin(TECH['prsc_geo'])) & - (gdb_use[Sw_onlineyearcol] >= startyear) - ] - pgeo = pgeo[COLNAMES['prsc_geo'][0]] - pgeo.columns = COLNAMES['prsc_geo'][1] - - pgeo = pgeo.groupby(['t','r','i']).sum().reset_index() - pgeo.sort_values(['t','r'], inplace=True) - - # Concat all RSC Existing Data to one dataframe: - prescribed_rsc = pd.concat([pupv,pwind,pcsp,pgeo],sort=False) - - #%%---------------------------------------------------------------------------- - ################################ - # -- SMR Existing Capacity -- # - ################################ - print('Gathering SMR Existing Capacity...') - # Grab the first year for smr because that is when new capacity can begin to be built (for - # smr, smr_ccs and electrolyzers) - firstyear = pd.read_csv( - os.path.join(inputs_case,'firstyear.csv'), - ).rename(columns={'*i':'i'}).set_index('i').squeeze(1) - h2_prod_first_year = firstyear['smr'] - # Get exogenous H2 demand - h2_exogenous_demand = ( - pd.read_csv(os.path.join(inputs_case,'h2_exogenous_demand.csv')) - .rename(columns={f'{sw.GSw_H2_Demand_Case}':'million_tons'},) - .drop(['*p'], axis=1).set_index('t').squeeze(1) - ) - ### Get BA share of national H2 demand - h2_ba_share = pd.read_csv( - os.path.join(inputs_case,'h2_ba_share.csv')) - # Filter to regions in function call - h2_ba_share = h2_ba_share[h2_ba_share['*r'].isin(regions)] - h2_ba_share = h2_ba_share.rename(columns={'*r':'r'}).pivot(index='t', columns='r', values='fraction') - ## h2_ba_share is only populated for 2021 and 2050, so need to fill the empty data - h2_ba_share = h2_ba_share.reindex(sorted(set(years+[2021,2050]))) - ## If a region has no data for 2021, it's zero (GAMS convention) - h2_ba_share.loc[2021] = h2_ba_share.loc[2021].fillna(0) - ## Backfill before 2021 - h2_ba_share.loc[:2021] = h2_ba_share.loc[:2021].fillna(method='bfill') - ## Interpolate between 2021-2050 - h2_ba_share.loc[2021:] = h2_ba_share.loc[2021:].interpolate('index') - ## Only keep the modeled years - h2_ba_share = h2_ba_share.loc[years].copy() - ## Reshape from wide to long format - h2_ba_share_out = h2_ba_share.reset_index().melt(id_vars='t', var_name='*r', value_name='fraction')[['*r','t','fraction']] - - # Calculating the consumption characteristics (has columns i, t, parameter, value) - consume_char0 = pd.read_csv( - os.path.join(inputs_case,'consume_char.csv')).rename(columns={'*i':'i'}) - consume_char0['i'] = consume_char0['i'].str.lower() - consume_char0 = consume_char0.set_index(['i','t','parameter']).value - - outage_forced_static = pd.read_csv(os.path.join(inputs_case,'outage_forced_static.csv'), - header=None, index_col=0, - ).squeeze(1) - - smr_init_ele_efficiency = consume_char0['smr',startyear,'ele_efficiency'] - smr_outage_forced = outage_forced_static['smr'] - h2_demand_initial = h2_exogenous_demand[h2_prod_first_year] - - # Now make some calculations to get the existing SMR capacity - # Hydrogen demand per r,t (million metric tons) * (10^9 kg/million metric ton) * (kWh/kg) - # / 8760 to convert kWh --> kW / (10^3 kW/MW) / outage rate - # * to make a tiny adjustment upwards to avoid infeasibilities - h2_existing_smr_cap = ( - h2_ba_share.stack('r').reorder_levels(['r','t']).rename('fraction').reset_index()) - # If this was multiplied by the H2 demand per year, then we would be forcing - # existing SMR to meet exogenous H2 demand forever and we don't want that. - # Only for it to meet 2023 demand - h2_existing_smr_cap['million_tons'] = h2_existing_smr_cap['fraction'] * h2_demand_initial - h2_existing_smr_cap['value'] = ( - h2_existing_smr_cap['million_tons'] * 1e9 * smr_init_ele_efficiency - / 8760 / 1000 / (1 - smr_outage_forced) * 1.0001) - # Make any value after h2_prod_first_year to be the same MW value as h2_prod_first_year - # (aka we will not force model to build more SMR capacity in 2030 once it has already - # met h2 demand in 2024). aka if model year is 2024, then from 2024-2050, the data - # will be the same df with columns t, r, fraction, million metric tons, - # value for 134 different BAs in h2_prod_first_year - # (but only do this if endyear > h2_prod_first_year, otherwise it will introduce NaNs) - if endyear > h2_prod_first_year: - h2_prod_first_year_df = h2_existing_smr_cap[ - h2_existing_smr_cap['t']==h2_prod_first_year - ].drop(['t'], axis=1) - # For any years after h2_prod_first_year - after_h2_prod_first_year_df = h2_existing_smr_cap[ - h2_existing_smr_cap['t'] > h2_prod_first_year - ].drop(['fraction','million_tons','value'], axis=1) - # New df from 2025 --> 2050 - after_h2_prod_first_year_df = pd.merge( - h2_prod_first_year_df, - after_h2_prod_first_year_df, - how='left', on=['r'], - ) - # Concat 2010-2024 df and 2025-->end of model - h2_existing_smr_cap = pd.concat([ - h2_existing_smr_cap[h2_existing_smr_cap['t']<=h2_prod_first_year], - after_h2_prod_first_year_df - ]) - # Filter down to modeled regions and years (otherwise b_inputs will throw an error) - h2_existing_smr_cap = (h2_existing_smr_cap - .rename(columns={'r':'*r'}) - .sort_values(by=['t','*r']) - ) - - - #%%---------------------------------------------------------------------------- - ################################ - # -- Retirements Data -- # - ################################ - print('Gathering Retirement Data...') - rets = gdb_use.loc[(gdb_use['tech'].isin(TECH['retirements'])) & - (gdb_use[retscen]>startyear) - ] - rets = rets[COLNAMES['retirements'][0]] - rets.columns = COLNAMES['retirements'][1] - rets.sort_values(by=COLNAMES['retirements'][1],inplace=True) - rets = rets.groupby(COLNAMES['retirements'][1][:-1]).sum().reset_index() - - rets_energy = gdb_use.loc[(gdb_use['tech'].isin(TECH['retirements_energy'])) & - (gdb_use[retscen]>startyear) - ] - rets_energy = rets_energy[COLNAMES['retirements_energy'][0]] - rets_energy.columns = COLNAMES['retirements_energy'][1] - rets_energy.sort_values(by=COLNAMES['retirements_energy'][1],inplace=True) - rets_energy = rets_energy.groupby(COLNAMES['retirements_energy'][1][:-1]).sum().reset_index() - - ################################ - # -- Wind Retirements -- # - ################################ - print('Gathering Wind Retirement Data...') - wind_rets = gdb_use.loc[(gdb_use['tech'].isin(TECH['windret'])) & - (gdb_use[Sw_onlineyearcol] <= startyear) & - (gdb_use['RetireYear'] > startyear) & - (gdb_use['RetireYear'] < startyear + 30) - ] - wind_rets = wind_rets[COLNAMES['windret'][0]] - wind_rets.columns = COLNAMES['windret'][1] - wind_rets['v'] = 'init-1' - wind_rets = wind_rets.groupby(['i','v','r','t']).sum().reset_index() - - wind_rets = (wind_rets.pivot_table(index = ['i','v','r'], columns = 't', values='value') - .reset_index() - .fillna(0) - ) - #================================ - # --- Geothermal Retirements --- - #================================ - print('Gathering Geothermal Retirement Data...') - geo_retirements = gdb_use.loc[(gdb_use['tech'].isin(TECH['georet'])) & - (gdb_use[Sw_onlineyearcol] <= startyear) & - (gdb_use['RetireYear'] > startyear) & - (gdb_use['RetireYear'] < startyear + 30) - ] - geo_retirements = geo_retirements[COLNAMES['georet'][0]] - geo_retirements.columns = COLNAMES['georet'][1] - geo_retirements['v'] = 'init-1' - geo_retirements = geo_retirements.groupby(['i','v','r','t']).sum().reset_index() - - geo_retirements = (geo_retirements - .pivot_table(index = ['i','v','r'], columns = 't', values='value') - .reset_index() - .fillna(0) - ) - - - #%%---------------------------------------------------------------------------- - ############################################################# - # -- Hydro Capacity Adjustment Factors: CC-Seasaon -- # - ############################################################# - - # Initialize with monthly hydropower capacity adjustment factor values - hydcapadj_ccszn = pd.read_csv(os.path.join(inputs_case,'hydcapadj.csv')) - #Filter to regions in function call - hydcapadj_ccszn = hydcapadj_ccszn[hydcapadj_ccszn['r'].isin(regions)] - # Map hot/cold values to ccseason months and filter for ccseason data - hydcapadj_ccszn['ccseason'] = hydcapadj_ccszn['month'].map(hotcold_months) - hydcapadj_ccszn = (hydcapadj_ccszn[hydcapadj_ccszn['ccseason'].isin(['cold','hot'])] - .drop(columns='month')) - # Average monthly data to get factor values by ccseason - hydcapadj_ccszn = hydcapadj_ccszn.groupby(['*i','r','ccseason']).mean().reset_index() - hydcapadj_ccszn['value'] = hydcapadj_ccszn['value'].round(5) - - - #%%---------------------------------------------------------------------------- - ######################################## - # -- Waterconstraint Indexing -- # - ######################################## - - rets['i'] = rets['i'].str.lower() - rets_energy['i'] = rets_energy['i'].str.lower() - prescribed_nonRSC['i'] = prescribed_nonRSC['i'].str.lower() - prescribed_nonRSC_energy['i'] = prescribed_nonRSC_energy['i'].str.lower() - - # When water constraints are enabled, retirements are also indexed by cooling technology - # and cooling water source. otherwise, they only have the indices of year, region, and tech - if GSw_WaterMain == 1: - ### Group by all cols except 'value' - rets = rets.groupby(COLNAMES['retirements'][1][:-1]).sum().reset_index() - rets.columns = COLNAMES['retirements'][1] - - capnonrsc = capnonrsc.groupby(COLNAMES['capnonrsc'][1][:-1]).sum().reset_index() - capnonrsc.columns = COLNAMES['capnonrsc'][1] - - prescribed_nonRSC = ( - prescribed_nonRSC - .groupby(COLNAMES['prescribed_nonRSC'][1][:-1]).sum().reset_index()) - prescribed_nonRSC.columns = COLNAMES['prescribed_nonRSC'][1] - - prescribed_nonRSC_energy = ( - prescribed_nonRSC_energy - .groupby(COLNAMES['prescribed_nonRSC_energy'][1][:-1]).sum().reset_index()) - - rets['i'] = rets['coolingwatertech'] - rets = rets.groupby(['t','r','i']).value.sum().reset_index() - rets.columns = ['t','r','i','value'] - - capnonrsc['i'] = capnonrsc['coolingwatertech'] - capnonrsc = capnonrsc.groupby(['i','r']).value.sum().reset_index() - capnonrsc.columns = ['i','r','value'] - - prescribed_nonRSC['i'] = prescribed_nonRSC['coolingwatertech'] - prescribed_nonRSC = prescribed_nonRSC.groupby(['t','r','i']).value.sum().reset_index() - prescribed_nonRSC.columns = ['t','r','i','value'] - - prescribed_nonRSC_energy['i'] = prescribed_nonRSC_energy['coolingwatertech'] - prescribed_nonRSC_energy = prescribed_nonRSC_energy.groupby(['t','r','i']).value.sum().reset_index() - prescribed_nonRSC_energy.columns = ['t','r','i','value'] - else: - # Group by [year, region, tech] - rets = rets.groupby(['t','r','i']).value.sum().reset_index() - rets.columns = ['t','r','i','value'] - - capnonrsc = capnonrsc.groupby(['i','r']).value.sum().reset_index() - capnonrsc.columns = ['i','r','value'] - - prescribed_nonRSC = prescribed_nonRSC.groupby(['t','r','i']).value.sum().reset_index() - prescribed_nonRSC.columns = ['t','r','i','value'] - - prescribed_nonRSC_energy = prescribed_nonRSC_energy.groupby(['t','r','i']).value.sum().reset_index() - prescribed_nonRSC_energy.columns = ['t','r','i','value'] - - # Final Groupby step for capacity groupings not affected by GSw_WaterMain: - caprsc = caprsc.groupby(['i','r']).value.sum().reset_index() - prescribed_rsc = prescribed_rsc.groupby(['t','i','r']).value.sum().reset_index() - - - #%%---------------------------------------------------------------------------- - ################################ - # -- Canadian Imports -- # - ################################ - - can_imports_year_mwh = pd.read_csv(os.path.join(inputs_case,'can_imports.csv'), - index_col='r').dropna() - # Filter to regions in function call - can_imports_year_mwh = can_imports_year_mwh[can_imports_year_mwh.index.isin(regions)] - can_imports_year_mwh.columns = can_imports_year_mwh.columns.astype(int) - can_imports_year_mwh = can_imports_year_mwh.reindex(years, axis=1).dropna(axis=1) - - ## Get hours per quarter - year = sw['GSw_HourlyWeatherYears'].split('_')[0] - timestamps = pd.Series(index=pd.date_range(f'{year}-01-01', periods=8760, freq='H')) - - month2quarter = pd.read_csv( - os.path.join(inputs_case, 'month2quarter.csv'), - index_col='month', - ).squeeze(1) - - quarterhours = timestamps.index.month.map(month2quarter).value_counts() - quarterhours.index = quarterhours.index.map(lambda x: quartershorten.get(x,x)).rename('szn') - - can_imports_quarter_frac = pd.read_csv(os.path.join(inputs_case,'can_imports_quarter_frac.csv'), - header=0, names=['szn','frac'], index_col='szn' - ).squeeze(1) - can_imports_capacity = ( - ## Start with annual imports in MWh - pd.concat({szn: can_imports_year_mwh for szn in quartershorten.values()}, axis=0, names=['szn','r']) - ## Multiply by season frac to get MWh per season - .multiply(can_imports_quarter_frac, axis=0, level='szn') - ## Divide by hours per season to get average MW by season - .divide(quarterhours, axis=0, level='szn') - ## Keep the max value across seasons - .groupby('r', axis=0).max() - ## Reshape for GAMS - .stack().rename_axis(['*r','t']).rename('MW').round(3) - ) - - - #%%---------------------------------------------------------------------------- - ############################## - # -- Data Write-Out -- # - ############################## - - #Round outputs before writing out - for df in [rets, rets_energy, capnonrsc, capnonrsc_energy, prescribed_nonRSC, prescribed_nonRSC_energy, - caprsc, prescribed_rsc, h2_existing_smr_cap]: - df['value'] = df['value'].round(6) - # Set all years to integer datatype - if 't' in df.columns: - df['t'] = df.t.astype(float).round().astype(int) - - #%% - # Return - files_out = {'capnonrsc' : capnonrsc[['i','r','value']], - 'capnonrsc_energy' : capnonrsc_energy[['i','r','value']], - 'rets' : rets[['t','r','i','value']], - 'rets_energy' : rets_energy[['t','r','i','value']], - 'prescribed_nonRSC' : prescribed_nonRSC[['t','i','r','value']], - 'prescribed_nonRSC_energy' : prescribed_nonRSC_energy[['t','i','r','value']], - 'caprsc' :caprsc[['i','r','value']], - 'prescribed_rsc' : prescribed_rsc[['t','i','r','value']], - 'wind_rets' : wind_rets, - 'h2_existing_smr_cap' : h2_existing_smr_cap[['*r','t','value']], - 'geo_retirements' : geo_retirements, - 'poi_cap_init' : poi_cap_init, - 'cap_cspns': cap_cspns, - 'rsc_wsc':rsc_wsc, - 'hydcapadj_ccszn' : hydcapadj_ccszn[['*i','ccseason','r','value']], - 'can_imports_capacity' : can_imports_capacity, - 'geoexist' : geoexist, - 'h2_ba_share': h2_ba_share_out - } - - return files_out - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if __name__ == '__main__': - ### Time the operation of this script - tic = datetime.datetime.now() - - ### Parse arguments - parser = argparse.ArgumentParser(description="""This file processes plant cost data by tech""") - parser.add_argument("reeds_path", help="ReEDS directory") - parser.add_argument("inputs_case", help="path to runs/{case}/inputs_case") - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - print('Starting writecapdat.py') - - - # Use agglevel_variables function to obtain spatial resolution variables - agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) - - # For mixed resolution runs the main function of writecapdat needs to be executed separately for each desired resolution - # Then the data from each resolution are combined and written to the inputs_case folder - if agglevel_variables['lvl'] == 'mult': - for resolution in agglevel_variables['agglevel']: - if resolution == 'aggreg': - aggreg_data = main(reeds_path, inputs_case, agglevel=resolution, - regions=agglevel_variables['ba_regions'] ) - if resolution == 'ba': - ba_data = main(reeds_path, inputs_case, agglevel=resolution, - regions=agglevel_variables['ba_regions']) - if resolution == 'county': - county_data = main(reeds_path, inputs_case, agglevel=resolution, - regions=agglevel_variables['county_regions'],) - - # Combine and write mixed resolution data - # ReEDS only supports county-BA, county-aggreg combinations - combined_data = {} - if 'ba' in agglevel_variables['agglevel']: - for key in ba_data.keys() : - if county_data[key].empty: - combined_data[key] = ba_data[key] - elif ba_data[key].empty: - combined_data[key] = county_data[key] - else: - combined_data[key] = pd.concat([ba_data[key], county_data[key]]) - - if 'aggreg' in agglevel_variables['agglevel']: - for key in aggreg_data.keys() : - if county_data[key].empty: - combined_data[key] = aggreg_data[key] - elif aggreg_data[key].empty: - combined_data[key] = county_data[key] - else: - combined_data[key] = pd.concat([aggreg_data[key], county_data[key]]) - - data = combined_data - - # Single Resolution Procedure - else: - agglevel = agglevel_variables['agglevel'] - regions = pd.read_csv(os.path.join(inputs_case,f'val_{agglevel}.csv'),header=None).squeeze(1).values - data = main(reeds_path, inputs_case,agglevel, regions) - - # Write it - print('Writing out capacity data') - outname = { - 'rets': 'retirements', - 'rets_energy': 'retirements_energy', - 'wind_rets': 'wind_retirements', - 'hydcapadj_ccszn': 'cap_hyd_ccseason_adj', - } - keep_index = { - 'poi_cap_init': True, - 'cap_cspns': True, - 'can_imports_capacity': True, - } - for key, df in data.items(): - df.to_csv( - os.path.join(inputs_case, f'{outname.get(key, key)}.csv'), - index=keep_index.get(key, False), - ) - - reeds.log.toc(tic=tic, year=0, process='input_processing/writecapdat.py', - path=os.path.join(inputs_case,'..')) - - print('Finished writecapdat.py') diff --git a/input_processing/writedrshift.py b/input_processing/writedrshift.py deleted file mode 100644 index 106528f7..00000000 --- a/input_processing/writedrshift.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Code to process allowed shifting hours for demand response into -fraction of hours that can be shifted into each time slice -At some point, it may be nice to instead read in the actual DR -shifting potential and change to fraction of load that can be shifted -but haven't done that yet. - -Created on Feb 24 2021 -@author: bstoll -""" - -# %% =========================================================================== -### --- IMPORTS --- -### =========================================================================== -import os -import sys -import argparse -import pandas as pd -import datetime -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -# Time the operation of this script -tic = datetime.datetime.now() - -# %%################# -### FIXED INPUTS ### - -decimals = 4 - - -#%% =========================================================================== -### --- MAIN FUNCTION --- -### =========================================================================== - -if __name__ == "__main__": - ### Parse arguments - parser = argparse.ArgumentParser( - description="This file produces the DR shiftability inputs" - ) - parser.add_argument("reeds_path", help="ReEDS directory") - parser.add_argument("inputs_case", help="output directory") - - args = parser.parse_args() - inputs_case = args.inputs_case - reeds_path = args.reeds_path - - # Settings for testing - # reeds_path = os.getcwd() - # inputs_case = os.path.join(reeds_path,'runs','dr1_Pacific','inputs_case') - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - print('Starting writedrshift.py') - - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - - val_r = ( - pd.read_csv(os.path.join(inputs_case, "val_r.csv"), header=None) - .squeeze(1) - .tolist() - ) - - ### Create empty EVMC data files if GSw_EVMC == 0: - evmc_files = [ - "evmc_shape_profile_decrease", - "evmc_shape_profile_increase", - "evmc_storage_profile_decrease", - "evmc_storage_profile_increase", - "evmc_storage_energy", - ] - for file in evmc_files: - if int(sw["GSw_EVMC"]): - pass - else: - # Overwrite empty dataframes created in copy_files.py - df = pd.DataFrame(columns=["i", "hour", "year"] + val_r) - df.to_csv(os.path.join(inputs_case, file + ".csv"), index=False) - - reeds.log.toc(tic=tic, year=0, process='input_processing/writedrshift.py', - path=os.path.join(inputs_case,'..')) - print('Finished writedrshift.py') diff --git a/input_processing/writesupplycurves.py b/input_processing/writesupplycurves.py deleted file mode 100644 index 52b80a44..00000000 --- a/input_processing/writesupplycurves.py +++ /dev/null @@ -1,1138 +0,0 @@ -""" -This script gathers supply curve data for on/offshore wind, upv, csp, hydro, and -psh into a single inputs_case file, rsc_combined.csv. - -This script contains additional procedures for gathering geothermal supply curve data -and demand response supply curve data, and spurline supply curve data -into various separate inputs_case files. -""" - -# %% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import argparse -import numpy as np -import os -import sys -import h5py -import datetime -import pandas as pd -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds -reeds_path = reeds.io.reeds_path - -# %%################# -### FIXED INPUTS ### - -### Number of bins used for everything other than wind and PV -numbins_other = 5 -### Rounding precision -decimals = 7 -### spur_cutoff [$/MW]: Cutoff for spur line costs; clip cost for sites with larger costs -spur_cutoff = 1e7 - -# %% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== -def wm(df): - """Make a function to take the capacity-weighted average in a .groupby() call""" - def _wm(x): - weights = df.loc[x.index, 'capacity'] - if (weights < 0).any(): - raise ValueError( - "Negative capacity encountered during supply curve aggregation. " - "Check input supply curve data for invalid capacity values." - ) - # Return 0 if the group has zero total capacity. - if weights.sum() == 0: - return 0 - return np.average(x, weights=weights) - return _wm - - -def get_exog_cap(inputs_case, tech, dfsc): - """Get exogenous capacity by class, region, rscbin, and year""" - dfexog = ( - pd.read_csv(os.path.join(inputs_case, f'exog_cap_{tech}.csv')) - .merge( - dfsc.explode('sc_point_gid').reset_index()[['sc_point_gid','bin']], - on='sc_point_gid', - ) - .rename(columns={'capacity':'MW'}) - ) - dfexog['rscbin'] = dfexog['bin'].map('bin{}'.format) - dfexog = dfexog.groupby(['*tech', 'region', 'rscbin', 'year']).MW.sum() - return dfexog - - -def agg_supplycurve( - scpath, - inputs_case, - numbins_tech, - agglevel, - AggregateRegions, - bin_method='equal_cap_cut', - bin_col='supply_curve_cost_per_mw', - spur_cutoff=1e7, - agglevel_variables=None, - deflate=None, - sw=None, - write=False, -): - """ - """ - ### Get inputs - dfin = reeds.io.assemble_supplycurve( - scfile=scpath, - case=os.path.dirname(os.path.normpath(inputs_case)), - agg=AggregateRegions, - ## TEMPORARY 20260402 - **({'GSw_ZoneSet': 'z134'} if not AggregateRegions else {}), - ).reset_index().drop(columns=['FIPS','cf'], errors='ignore') - ## Convert dollar year and recalculate total cost - transcost_cols = [c for c in dfin if 'cost' in c] - dfin.loc[:, transcost_cols] *= deflate['interconnection'] - deflate_scen = os.path.splitext(os.path.basename(scpath))[0] - dfin['capital_adder_per_mw'] *= deflate[deflate_scen] - dfin['supply_curve_cost_per_mw'] = dfin[ - ['capital_adder_per_mw', 'cost_total_trans_usd_per_mw'] - ].sum(axis=1) - ### Define the aggregation settings - ## Cost and distance are weighted averages, with capacity as the weighting factor - aggs = {'capacity': 'sum', 'sc_point_gid': list} - index_cols = ['region', 'class', 'bin'] - aggs = { - col: aggs.get(col, wm(dfin)) for col in dfin - if col not in index_cols - } - - ### Assign bins - if dfin.empty: - dfin['bin'] = [] - else: - dfin = ( - dfin - .groupby(['region','class'], sort=False, group_keys=True) - .apply(reeds.inputs.get_bin, numbins_tech, bin_method, bin_col) - .reset_index(drop=True) - .sort_values('sc_point_gid') - ) - ### Aggregate it - dfout = dfin.groupby(index_cols).agg(aggs) - ### Clip negative costs and costs above cutoff - dfout.supply_curve_cost_per_mw = dfout.supply_curve_cost_per_mw.clip(lower=0, upper=spur_cutoff) - - return dfin, dfout - - -# %% ============================================================================ -### --- MAIN FUNCTION --- -### ============================================================================ - - -def main( - reeds_path, inputs_case, AggregateRegions=1, rsc_wsc_dat=None, write=True, **kwargs -): - # #%% Settings for testing - # reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # inputs_case = os.path.join(reeds_path,'runs','v20251209_scM0_Pacific','inputs_case') - # AggregateRegions = 1 - # rsc_wsc_dat = None - # write = True - # kwargs = {} - - #%% Inputs from switches - sw = reeds.io.get_switches(inputs_case) - ### Overwrite switches with keyword arguments - for kw, arg in kwargs.items(): - sw[kw] = arg - endyear = int(sw.endyear) - startyear = int(sw.startyear) - geohydrosupplycurve = sw.geohydrosupplycurve - egssupplycurve = sw.egssupplycurve - egsnearfieldsupplycurve = sw.egsnearfieldsupplycurve - pshsupplycurve = sw.pshsupplycurve - numbins = { - "upv": int(sw.numbins_upv), - "wind-ons": int(sw.numbins_windons), - "wind-ofs": int(sw.numbins_windofs), - "csp": int(sw.numbins_csp), - "geohydro": int(sw.numbins_geohydro_allkm), - "egs": int(sw.numbins_egs_allkm), - } - - # Use agglevel_variables function to obtain spatial resolution variables - agglevel_variables = reeds.spatial.get_agglevel_variables(reeds_path, inputs_case) - agglevel = agglevel_variables['agglevel'] - - val_r_all = pd.read_csv( - os.path.join(inputs_case,'val_r_all.csv'), header=None).squeeze(1).tolist() - # Read in tech-subset-table.csv to determine number of csp configurations - tech_subset_table = pd.read_csv(os.path.join(inputs_case, "tech-subset-table.csv")) - csp_configs = tech_subset_table.loc[ - (tech_subset_table.CSP == "YES") & (tech_subset_table.STORAGE == "YES") - ].shape[0] - - # Read in dollar year conversions for RSC data - dollaryear = pd.read_csv( - os.path.join(inputs_case, 'dollaryear_sc.csv'), index_col='Scenario', - ).squeeze(1) - ## Interconnection cost dollar year is stored as metadata - fpath_interconnection = os.path.join( - reeds_path, 'inputs', 'supply_curve', 'interconnection_land.h5' - ) - with h5py.File(fpath_interconnection, 'r') as f: - dollaryear['interconnection'] = f['data'].attrs['dollaryear'] - deflator = pd.read_csv( - os.path.join(inputs_case, 'deflator.csv'), index_col='*Dollar.Year', - ).squeeze(1) - deflate = dollaryear.map(deflator).rename('Deflator') - - #%% Load the existing RSC capacity (PV plants, wind, and CSP) if not provided in main function call - if rsc_wsc_dat is None: - # writesupplycurves.py is being run as a main input processing script - rsc_wsc = pd.read_csv(os.path.join(inputs_case, "rsc_wsc.csv")) - else: - # writesupplycurves.py is being passed rsc_wsc data from an aggregate_regions.py call - rsc_wsc = rsc_wsc_dat.copy() - - # Group CSP tech - rsc_wsc.loc[rsc_wsc['i']=='csp-ws', 'i'] = 'csp' - rsc_wsc = rsc_wsc.groupby(["r", "i"]).sum().reset_index() - rsc_wsc.i = rsc_wsc.i.str.lower() - - if len(rsc_wsc.columns) < 3: - rsc_wsc["value"] = "" - rsc_wsc.columns = ["r", "tech", "exist"] - - else: - rsc_wsc.columns = ["r", "tech", "exist"] - - ### Change the units - rsc_wsc.exist /= 1000 - tout = rsc_wsc.copy() - - # %% Load supply curve files --------------------------------------------------------- - - alloutcap_list = [] - alloutcost_list = [] - spurout_list = [] - - # %%################# - # -- Wind -- # - #################### - - windin, wind = {}, {} - cost_components_wind_list = [] - wind_types = ["ons"] - if int(sw["GSw_OfsWind"]): - wind_types.append("ofs") - - for s in wind_types: - windin[s], wind[s] = agg_supplycurve( - scpath=os.path.join(inputs_case,f'supplycurve_wind-{s}.csv'), - inputs_case=inputs_case, - agglevel=agglevel, AggregateRegions=AggregateRegions, - numbins_tech=numbins[f'wind-{s}'], spur_cutoff=spur_cutoff, - agglevel_variables=agglevel_variables, deflate=deflate, - sw=sw, write=write - ) - - cost_components = ( - wind[s][["cost_total_trans_usd_per_mw", "capital_adder_per_mw"]] - .round(2) - .reset_index() - .rename( - columns={ - "region": "r", - "class": "*i", - "bin": "rscbin", - "cost_total_trans_usd_per_mw": "cost_trans", - "capital_adder_per_mw": "cost_cap", - } - ) - ) - - cost_components["*i"] = f"wind-{s}_" + cost_components[ - "*i" - ].astype(str) - cost_components["rscbin"] = "bin" + cost_components[ - "rscbin" - ].astype(str) - cost_components = pd.melt( - cost_components, - id_vars=["*i", "r", "rscbin"], - var_name="sc_cat", - value_name="value", - ) - cost_components_wind_list.append(cost_components) - - spurout_list.append( - wind[s] - .reset_index() - .assign(i=f"wind-{s}_" + wind[s].reset_index()["class"].astype(str)) - .assign(rscbin="bin" + wind[s].reset_index()["bin"].astype(str)) - .rename(columns={"region": "r"}) - ) - - cost_components_wind = pd.concat(cost_components_wind_list) - windall = ( - pd.concat(wind, axis=0) - .reset_index(level=0) - .rename(columns={"level_0": "tech"}) - .reset_index() - ) - ### Normalize formatting - windall["tech"] = "wind-" + windall["tech"] - windall.supply_curve_cost_per_mw = windall.supply_curve_cost_per_mw.round(2) - windall["class"] = "class" + windall["class"].astype(str) - windall["bin"] = "wsc" + windall["bin"].astype(str) - ### Pivot, with bins in long format - bins_wind = list(range(1, max(numbins["wind-ons"], numbins["wind-ofs"]) + 1)) - windcost = ( - windall.pivot( - index=["region", "class", "tech"], - columns="bin", - values="supply_curve_cost_per_mw", - ) - .fillna(0) - .reset_index() - ) - windcost.rename( - columns={"wsc{}".format(i): "bin{}".format(i) for i in bins_wind}, - inplace=True, - ) - alloutcost_list.append(windcost) - - windcap = ( - windall.pivot( - index=["region", "class", "tech"], columns="bin", values="capacity" - ) - .fillna(0) - .reset_index() - ) - windcap.rename( - columns={"wsc{}".format(i): "bin{}".format(i) for i in bins_wind}, - inplace=True, - ) - alloutcap_list.append(windcap) - - if write: - ## Exogenous wind capacity - dfwindexog = get_exog_cap(inputs_case, tech='wind-ons', dfsc=wind['ons']) - dfwindexog.round(3).to_csv(os.path.join(inputs_case, "exog_wind_ons_rsc.csv")) - - # %%############### - # -- PV -- # - ################## - - upvin, upv = agg_supplycurve( - scpath=os.path.join(inputs_case, 'supplycurve_upv.csv'), - inputs_case=inputs_case, - agglevel=agglevel, AggregateRegions=AggregateRegions, - numbins_tech=numbins['upv'], spur_cutoff=spur_cutoff, - agglevel_variables=agglevel_variables, deflate=deflate, - sw=sw, write=write - ) - - # Similar to wind, save the trans vs cap components and then concatenate them below just - # before outputting rsc_combined.csv - cost_components_upv = ( - upv[["cost_total_trans_usd_per_mw", "capital_adder_per_mw"]].round(2).reset_index() - ) - cost_components_upv = cost_components_upv.rename( - columns={ - "region": "r", - "class": "*i", - "bin": "rscbin", - "cost_total_trans_usd_per_mw": "cost_trans", - "capital_adder_per_mw": "cost_cap", - } - ) - cost_components_upv["*i"] = "upv_" + cost_components_upv["*i"].astype(str) - cost_components_upv["rscbin"] = "bin" + cost_components_upv["rscbin"].astype(str) - cost_components_upv = pd.melt( - cost_components_upv, - id_vars=["*i", "r", "rscbin"], - var_name="sc_cat", - value_name="value", - ) - - if write: - ## Exogenous UPV capacity - dfupvexog = get_exog_cap(inputs_case, tech='upv', dfsc=upv) - dfupvexog.round(3).to_csv(os.path.join(inputs_case, "exog_upv_rsc.csv")) - - ### Normalize formatting - upv = upv.reset_index() - upv["class"] = "class" + upv["class"].astype(str) - upv["bin"] = "upvsc" + upv["bin"].astype(str) - - spurout_list.append( - upv.assign(i="upv_" + upv["class"].astype(str).str.strip("class")) - .assign(rscbin="bin" + upv["bin"].str.strip("upvsc")) - .rename(columns={"region": "r"}) - ) - - ### Pivot, with bins in long format - bins_upv = list(range(1, numbins["upv"] + 1)) - upvcost = ( - upv.pivot( - columns="bin", values="supply_curve_cost_per_mw", index=["region", "class"] - ).fillna(0) - ### reV spur line and reinforcement costs are now in per MW-AC terms, so removing the - ### correction term that was applied. - .assign(tech="upv") - ).reset_index() - upvcost.rename( - columns={"upvsc{}".format(i): "bin{}".format(i) for i in bins_upv}, - inplace=True, - ) - alloutcost_list.append(upvcost) - - upvcap = ( - upv.pivot(columns="bin", values="capacity", index=["region", "class"]) - .fillna(0) - .reset_index() - .assign(tech="upv") - ) - upvcap.rename( - columns={"upvsc{}".format(i): "bin{}".format(i) for i in bins_upv}, - inplace=True, - ) - alloutcap_list.append(upvcap) - - # %%################ - # -- CSP -- # - ################### - - if int(sw["GSw_CSP"]): - cspin, csp = agg_supplycurve( - scpath=os.path.join(inputs_case, 'supplycurve_csp.csv'), - inputs_case=inputs_case, - agglevel=agglevel, AggregateRegions=AggregateRegions, - numbins_tech=numbins['csp'], spur_cutoff=spur_cutoff, - agglevel_variables=agglevel_variables, deflate=deflate, - sw=sw, write=False - ) - - ### Normalize formatting - csp = csp.reset_index() - csp["class"] = "class" + csp["class"].astype(str) - csp["bin"] = "cspsc" + csp["bin"].astype(str) - - spurout_list.append( - csp.assign(i="csp_" + csp["class"].astype(str).str.strip("class")) - .assign(rscbin="bin" + csp["bin"].str.strip("cspsc")) - .rename(columns={"region": "r"}) - ) - - ### Pivot, with bins in long format - cspcost = ( - csp.pivot( - columns="bin", values="supply_curve_cost_per_mw", index=["region", "class"] - ).fillna(0) - ).reset_index() - cspcap = ( - csp.pivot(columns="bin", values="capacity", index=["region", "class"]) - .fillna(0) - .reset_index() - ) - - ## Duplicate the CSP supply curve for each CSP configuration - bins_csp = list(range(1, numbins["csp"] + 1)) - cspcap = ( - pd.concat( - {"csp{}".format(i): cspcap for i in range(1, csp_configs + 1)}, axis=0 - ) - .reset_index(level=0) - .rename(columns={"level_0": "tech"}) - .reset_index(drop=True) - ) - cspcap.rename( - columns={"cspsc{}".format(i): "bin{}".format(i) for i in bins_csp}, - inplace=True, - ) - alloutcap_list.append(cspcap) - - cspcost = ( - pd.concat( - {"csp{}".format(i): cspcost for i in range(1, csp_configs + 1)}, axis=0 - ) - .reset_index(level=0) - .rename(columns={"level_0": "tech"}) - .reset_index(drop=True) - ) - cspcost.rename( - columns={"cspsc{}".format(i): "bin{}".format(i) for i in bins_csp}, - inplace=True, - ) - alloutcost_list.append(cspcost) - - # %% Geothermal - if int(sw["GSw_Geothermal"]): - use_geohydro_rev_sc = (geohydrosupplycurve == "reV") - use_egs_rev_sc = (egssupplycurve == "reV") - else: - use_geohydro_rev_sc = False - use_egs_rev_sc = False - - ## reV supply curves - if use_geohydro_rev_sc or use_egs_rev_sc: - geoin, geo = {}, {} - rev_geo_types = [] - if use_geohydro_rev_sc: - rev_geo_types.append("geohydro") - if use_egs_rev_sc: - rev_geo_types.append("egs") - for s in rev_geo_types: - geoin[s], geo[s] = agg_supplycurve( - scpath=os.path.join( - inputs_case, - f'supplycurve_{s}.csv'), - numbins_tech=numbins[s], inputs_case=inputs_case, - agglevel=agglevel, AggregateRegions=AggregateRegions, - spur_cutoff=spur_cutoff,agglevel_variables=agglevel_variables, deflate=deflate, - sw=sw, write=False - ) - spurout_list.append( - geo[s] - .reset_index() - .assign(i=f"{s}_allkm_" + geo[s].reset_index()["class"].astype(str)) - .assign(rscbin="bin" + geo[s].reset_index()["bin"].astype(str)) - .rename(columns={"region": "r"}) - ) - - geoall = ( - pd.concat(geo, axis=0) - .reset_index(level=0) - .rename(columns={"level_0": "type"}) - .reset_index() - ) - geoall["type"] = geoall["type"] + "_allkm" - geoall.supply_curve_cost_per_mw = geoall.supply_curve_cost_per_mw.round(2) - geoall["class"] = "class" + geoall["class"].astype(str) - geoall["bin"] = "geosc" + geoall["bin"].astype(str) - ### Pivot, with bins in long format - geocost = ( - geoall.pivot( - index=["region", "class", "type"], - columns="bin", - values="supply_curve_cost_per_mw", - ) - .fillna(0) - .reset_index() - ) - geocap = ( - geoall.pivot( - index=["region", "class", "type"], columns="bin", values="capacity" - ) - .fillna(0) - .reset_index() - ) - - ### Geothermal bins (flexible) - bins_geo = (range(1, max(numbins['geohydro']*use_geohydro_rev_sc, numbins['egs']*use_egs_rev_sc) + 1)) - geocap.rename( - columns={ - **{ - "type": "tech", - "Unnamed: 0": "region", - "Unnamed: 1": "class", - "Unnamed 2": "tech", - }, - **{"geosc{}".format(i): "bin{}".format(i) for i in bins_geo}, - }, - inplace=True, - ) - alloutcap_list.append(geocap) - - geocost.rename( - columns={ - **{ - "type": "tech", - "Unnamed: 0": "region", - "Unnamed: 1": "class", - "Unnamed 2": "tech", - }, - **{"geosc{}".format(i): "bin{}".format(i) for i in bins_geo}, - }, - inplace=True, - ) - alloutcost_list.append(geocost) - - if write: - ## Geothermal discovery rates - geo_disc_rate = pd.read_csv(os.path.join(inputs_case, "geo_discovery_rate.csv")) - geo_disc_rate.round(decimals).to_csv( - os.path.join(inputs_case, "geo_discovery_rate.csv"), index=False - ) - geo_discovery_factor = pd.read_csv( - os.path.join(inputs_case, "geo_discovery_factor.csv") - ) - geo_discovery_factor = geo_discovery_factor.loc[ - geo_discovery_factor.r.isin(val_r_all)].copy() - geo_discovery_factor.round(decimals).to_csv( - os.path.join(inputs_case, "geo_discovery_factor.csv"), index=False - ) - - if use_geohydro_rev_sc: - ## Exogenous geohydro capacity - dfgeohydroexog = get_exog_cap(inputs_case, tech='geohydro', dfsc=geo['geohydro']) - dfgeohydroexog.round(3).to_csv( - os.path.join(inputs_case, "exog_geohydro_allkm_rsc.csv") - ) - - # %% Get supply-curve data for postprocessing - spurcols = [ - 'i', - 'r', - 'rscbin', - 'capacity', - 'dist_spur_km', - 'dist_reinforcement_km', - 'supply_curve_cost_per_mw', - ] - spurout = pd.concat(spurout_list)[spurcols].round(2) - if write: - ## Spurline and reinforcement distances and costs - spurout.to_csv(os.path.join(inputs_case, "spur_parameters.csv"), index=False) - - ### Get spur-line and reinforcement distances if using in annual trans investment limit - poi_distance = spurout.copy() - ## Duplicate CSP entries for each CSP system design - poi_distance_csp = poi_distance.loc[poi_distance.i.str.startswith("csp")].copy() - poi_distance_csp_broadcasted = pd.concat( - [ - poi_distance_csp.assign( - i=poi_distance_csp.i.str.replace("csp_", f"csp{i}_") - ) - for i in range(1, csp_configs + 1) - ], - axis=0, - ) - poi_distance_out = ( - pd.concat( - [ - poi_distance.loc[~poi_distance.i.str.startswith("csp")], - poi_distance_csp_broadcasted, - ], - axis=0, - ) - ## Reformat to save for GAMS - .rename(columns={"i": "*i"}) - .set_index(["*i", "r", "rscbin"]) - ) - ## Convert to miles - distance_spur = (poi_distance_out.dist_spur_km.rename("miles") / 1.609).round(3) - if write: - distance_spur.to_csv(os.path.join(inputs_case, "distance_spur.csv")) - - distance_reinforcement = ( - poi_distance_out.dist_reinforcement_km.rename("miles") / 1.609 - ).round(3) - if write: - distance_reinforcement.to_csv( - os.path.join(inputs_case, "distance_reinforcement.csv") - ) - - # %%################################### - # -- Supply Curve Data -- # - ###################################### - # %% Combine the supply curves - alloutcap = ( - pd.concat(alloutcap_list) - .rename(columns={"region": "r"}) - .assign(var="cap") - ) - alloutcap["class"] = alloutcap["class"].map(lambda x: x.lstrip("cspclass")) - alloutcap["class"] = ( - "class" + alloutcap["class"].map(lambda x: x.lstrip("class")).astype(str) - ) - - t1 = alloutcap.pivot( - index=["r", "tech", "var"], - columns="class", - values=[c for c in alloutcap.columns if c.startswith("bin")], - ).reset_index() - ### Concat the multi-level column names to a single level - t1.columns = ["_".join(i).strip("_") for i in t1.columns.tolist()] - - t2 = t1.merge(tout, on=["r", "tech"], how="outer").fillna(0) - - ### Subset to single-tech curves - wndonst2 = t2.loc[t2.tech == "wind-ons"].copy() - wndofst2 = t2.loc[t2.tech == "wind-ofs"].copy() - cspt2 = t2.loc[t2.tech.isin(["csp{}".format(i) for i in range(1, csp_configs + 1)])] - upvt2 = t2.loc[t2.tech == "upv"].copy() - geohydrot2 = t2.loc[t2.tech == "geohydro_allkm"].copy() - egst2 = t2.loc[t2.tech == "egs_allkm"].copy() - - ### Get the combined outputs - outcap = pd.concat([wndonst2, wndofst2, upvt2, cspt2, geohydrot2, egst2]) - - moutcap = pd.melt(outcap, id_vars=["r", "tech", "var"]) - moutcap = moutcap.loc[~moutcap.variable.isin(["exist", "temp"])].copy() - - moutcap["bin"] = moutcap.variable.map(lambda x: x.split("_")[0]) - moutcap["class"] = moutcap.variable.map(lambda x: x.split("_")[1].lstrip("class")) - outcols = ["r", "tech", "var", "bin", "class", "value"] - moutcap = moutcap.loc[moutcap.value != 0, outcols].copy() - - outcapfin = ( - moutcap.pivot( - index=["r", "tech", "var", "class"], columns="bin", values="value" - ) - .fillna(0) - .reset_index() - ) - - alloutcost = ( - pd.concat(alloutcost_list) - .rename(columns={"region": "r"}) - .set_index(["r", "class", "tech"]) - .reset_index() - .assign(var="cost") - ) - alloutcost["class"] = alloutcost["class"].map(lambda x: x.lstrip("cspclass")) - alloutcost["class"] = alloutcost["class"].map(lambda x: x.lstrip("class")) - - allout = pd.concat([outcapfin, alloutcost]) - allout["tech"] = allout["tech"] + "_" + allout["class"].astype(str) - alloutm = pd.melt(allout, id_vars=["r", "tech", "var"]) - alloutm.rename(columns={"bin":"variable"}, inplace=True) - alloutm = alloutm.loc[alloutm.variable != "class"].copy() - allout_list = [alloutm] - - # %%---------------------------------------------------------------------------------- - ########################## - # -- Hydropower -- # - ########################## - """ - Adding hydro costs and capacity separate as it does not - require the calculations to reduce capacity by existing amounts. - - Goal here is to acquire a data frame that matches the format - of alloutm so that we can simply stack the two. - """ - hydcap = pd.read_csv(os.path.join(inputs_case, "hydcap.csv")) - hydcost = pd.read_csv(os.path.join(inputs_case, "hydcost.csv")) - - hydcap = ( - pd.melt(hydcap, id_vars=["tech", "class"]) - .set_index(["tech", "class", "variable"]) - .sort_index() - ) - hydcap = hydcap.reset_index() - hydcost = pd.melt(hydcost, id_vars=["tech", "class"]) - - # Convert dollar year - hydcost[hydcost.select_dtypes(include=["number"]).columns] *= deflate["hydcost"] - - hydcap["var"] = "cap" - hydcost["var"] = "cost" - - hyddat = pd.concat([hydcap, hydcost]) - hyddat["bin"] = hyddat["class"].map(lambda x: x.replace("hydclass", "bin")) - hyddat["class"] = hyddat["class"].map(lambda x: x.replace("hydclass", "")) - - hyddat.rename(columns={"variable": "r", "bin": "variable"}, inplace=True) - hyddat = hyddat[["tech", "r", "value", "var", "variable"]].fillna(0) - allout_list.append(hyddat) - - ######################################### - # -- Pumped Storage Hydropower -- # - ######################################### - - if int(sw["GSw_Storage"]): - # Input processing currently assumes that cost data in CSV file is in 2004$ - psh_cap = pd.read_csv(os.path.join(inputs_case, "psh_supply_curves_capacity.csv")) - psh_cost = pd.read_csv(os.path.join(inputs_case, "psh_supply_curves_cost.csv")) - psh_durs = pd.read_csv( - os.path.join(inputs_case, "psh_supply_curves_duration.csv"), header=0 - ) - - psh_cap = pd.melt(psh_cap, id_vars=["r"]) - psh_cost = pd.melt(psh_cost, id_vars=["r"]) - - # Convert dollar year - psh_cost[psh_cost.select_dtypes(include=["number"]).columns] *= deflate["PSHcostn"] - - psh_cap["var"] = "cap" - psh_cost["var"] = "cost" - - psh_out = pd.concat([psh_cap, psh_cost]).fillna(0) - psh_out["tech"] = "pumped-hydro" - psh_out["variable"] = psh_out.variable.map(lambda x: x.replace("pshclass", "bin")) - psh_out = psh_out[hyddat.columns].copy() - allout_list.append(psh_out) - - if write: - # Select storage duration correponding to the supply curve - psh_dur_out = psh_durs[psh_durs["pshsupplycurve"] == pshsupplycurve]["duration"] - psh_dur_out.to_csv( - os.path.join(inputs_case, "psh_sc_duration.csv"), index=False, header=False - ) - - write_storage_duration = int(sw["GSw_HydroPSHDurData"]) - write_storinmaxfrac = sw["GSw_HydroStorInMaxFrac"] == "data" - if write_storage_duration or write_storinmaxfrac: - cap_existing_psh = pd.read_csv( - os.path.join(inputs_case, 'cap_existing_psh.csv'), - index_col=['*i', 'v', 'r'] - ) - - if write_storage_duration: - # Calculate capacity-weighted storage duration in - # hours for each region with an existing psh fleet - existing_psh_duration_data = cap_existing_psh.copy() - existing_psh_duration_data['hours'] = ( - existing_psh_duration_data['max_energy_MWh'] / - existing_psh_duration_data['operational_capacity_MW'] - ) - existing_psh_duration_data[['hours']].round(1).to_csv( - os.path.join(inputs_case, 'storage_duration_pshdata.csv') - ) - - if write_storinmaxfrac: - # Calculate max storage_in as a fraction of psh - # capacity for each region with an existing psh fleet - existing_psh_stor_in_data = cap_existing_psh.copy() - existing_psh_stor_in_data['frac'] = ( - existing_psh_stor_in_data['pump_capacity_MW'] / - existing_psh_stor_in_data['operational_capacity_MW'] - ) - existing_psh_stor_in_data[['frac']].round(2).to_csv( - os.path.join(inputs_case, 'storinmaxfrac.csv') - ) - - - ####################################################### - # -- Demand Response -- # - ####################################################### - # Use capacity and cost to add DR Shed to rsc_combined - disagg_data = pd.read_csv(os.path.join(inputs_case,'disagg_state_lpf.csv')) - state2r = disagg_data.groupby('state')['r'].unique().apply(list).to_dict() - if int(sw["GSw_DRShed"]) and write: - # DR Shed input data at state-level, need to assign to model resolution - # State cost are uniformaly distributed across model regions within the state - # State capacity is distributed based on load - - # Calculate state-to-region aggregation/disaggregation factors - state_region_factors = ( - disagg_data.groupby(['state', 'r'], as_index=False) - ['state_frac'] - .sum() - .pivot(index='state', columns='r', values='state_frac') - .rename_axis(None, axis=1) - .fillna(0) - ) - dr_shed_cap_state = pd.read_csv(os.path.join(inputs_case,'dr_shed_cap.csv')) - dr_shed_cap_reg = dr_shed_cap_state.copy() - state_region_factors = state_region_factors.loc[state_region_factors.index.intersection(dr_shed_cap_reg.columns), :] - - # Multiply the hourly state load profiles by the state-to-region factors - dr_shed_cap_reg = ( - dr_shed_cap_reg[state_region_factors.index] - .dot(state_region_factors) - ) - dr_shed_cap_reg.insert(0, 'tech', dr_shed_cap_state['tech']) - - dr_shed_cost_state = pd.read_csv(os.path.join(inputs_case,'dr_shed_cost.csv')) - dr_shed_cost_reg = dr_shed_cost_state.copy() - for col in dr_shed_cost_state.columns[1:]: - # Copy state columns to each model region - for r in state2r[col]: - dr_shed_cost_reg[r] = dr_shed_cost_state[col] - dr_shed_cost_reg.drop(columns=col, inplace=True) - - # Define rsc class using tech - dr_shed_cap = dr_shed_cap_reg.copy() - dr_shed_cap['class'] = dr_shed_cap['tech'] - dr_shed_cost = dr_shed_cost_reg.copy() - dr_shed_cost['class'] = dr_shed_cost['tech'] - - dr_shed_cap = (pd.melt(dr_shed_cap, id_vars=['tech','class']) - .set_index(['tech','class','variable']) - .sort_index()) - dr_shed_cap = dr_shed_cap.reset_index() - dr_shed_cost = pd.melt(dr_shed_cost, id_vars=['tech','class']) - - # Convert dollar year - dr_shed_cost[dr_shed_cost.select_dtypes(include=['number']).columns] *= deflate['dr_shed'] - - # Assign rsc cat - dr_shed_cap['var'] = 'cap' - dr_shed_cost['var'] = 'cost' - - # Combined cost and capacity - dr_shed_dat = pd.concat([dr_shed_cap, dr_shed_cost]) - dr_shed_dat['bin'] = dr_shed_dat['class'].map(lambda x: x.replace('dr_shed_','bin')) - dr_shed_dat['class'] = dr_shed_dat['class'].map(lambda x: x.replace('dr_shed_','')) - - dr_shed_dat.rename(columns={'variable':'r','bin':'variable'}, inplace=True) - dr_shed_dat = dr_shed_dat[['tech','r','value','var','variable']].fillna(0) - allout_list.append(dr_shed_dat) - - if write: - # Update supply curve capacity multiplier from state-level to region-level - dr_shed_capacity_scalar_state = pd.read_csv(os.path.join(inputs_case,'dr_shed_capacity_scalar.csv')) - # Could be empty if regions included in run do not have DR data - if dr_shed_capacity_scalar_state.empty: - pass - else: - dr_shed_capacity_scalar_reg ={} - for st in dr_shed_capacity_scalar_state['r'].unique(): - scalar_df = {} - for r in state2r[st]: - scalar_ba= dr_shed_capacity_scalar_state[dr_shed_capacity_scalar_state['r'] == st].copy() - scalar_ba['r'] = r - scalar_df[r] = scalar_ba - dr_shed_capacity_scalar_reg[st] = pd.concat(scalar_df) - dr_shed_capacity_scalar_reg = pd.concat(dr_shed_capacity_scalar_reg.values(), ignore_index=True) - dr_shed_capacity_scalar_reg.to_csv(os.path.join(inputs_case,'dr_shed_capacity_scalar.csv'), index=False) - - # %%---------------------------------------------------------------------------------- - ################################## - # -- Combine everything -- # - ################################## - - ### Combine, then drop the (cap,cost) entries with nan cost - alloutm = ( - pd.concat(allout_list) - .pivot( - index=["r", "tech", "variable"], columns=["var"], values=["value"] - ) - .dropna()["value"] - .reset_index() - .melt(id_vars=["r", "tech", "variable"])[ - ["tech", "r", "var", "variable", "value"] - ] - ### Rename the first column so GAMS reads the header as a comment - .rename(columns={"tech": "*i", "var": "sc_cat", "variable": "rscbin"}) - .astype({"value": float}) - ### Drop 0 values - .replace({"value": {0.0: np.nan}}) - .dropna() - .round(5) - ) - - ## Merge geothermal non-reV supply curves if applicable - if int(sw["GSw_Geothermal"]): - if not use_geohydro_rev_sc: - geohydro_rsc = pd.read_csv(os.path.join(inputs_case, "geo_rsc.csv"), header=0) - geohydro_rsc = geohydro_rsc.loc[ - geohydro_rsc["*i"].str.startswith("geohydro_allkm") - ] - # Filter by valid regions - geohydro_rsc = geohydro_rsc.loc[geohydro_rsc["r"].isin(val_r_all)] - # Convert dollar year - geohydro_rsc.sc_cat = geohydro_rsc.sc_cat.str.lower() - geohydro_rsc.loc[geohydro_rsc.sc_cat == "cost", "value"] *= deflate[ - "geo_rsc_{}".format(geohydrosupplycurve) - ] - geohydro_rsc["rscbin"] = "bin1" - alloutm = pd.concat([alloutm, geohydro_rsc]) - - if not use_egs_rev_sc: - egs_rsc = pd.read_csv(os.path.join(inputs_case, "geo_rsc.csv"), header=0) - egs_rsc = egs_rsc.loc[egs_rsc["*i"].str.startswith("egs_allkm")] - # Filter by valid regions - egs_rsc = egs_rsc.loc[egs_rsc["r"].isin(val_r_all)] - # Convert dollar year - egs_rsc.sc_cat = egs_rsc.sc_cat.str.lower() - egs_rsc.loc[egs_rsc.sc_cat == "cost", "value"] *= deflate[ - "geo_rsc_{}".format(egssupplycurve) - ] - egs_rsc["rscbin"] = "bin1" - alloutm = pd.concat([alloutm, egs_rsc]) - - egsnearfield_rsc = pd.read_csv(os.path.join(inputs_case, "geo_rsc.csv"), header=0) - egsnearfield_rsc = egsnearfield_rsc.loc[ - egsnearfield_rsc["*i"].str.startswith("egs_nearfield") - ] - # Filter by valid regions - egsnearfield_rsc = egsnearfield_rsc.loc[egsnearfield_rsc["r"].isin(val_r_all)] - # Convert dollar year - egsnearfield_rsc.sc_cat = egsnearfield_rsc.sc_cat.str.lower() - egsnearfield_rsc.loc[egsnearfield_rsc.sc_cat == "cost", "value"] *= deflate[ - "geo_rsc_{}".format(egsnearfieldsupplycurve) - ] - egsnearfield_rsc["rscbin"] = "bin1" - alloutm = pd.concat([alloutm, egsnearfield_rsc]) - - ### Combine with cost components - alloutm = pd.concat([alloutm, cost_components_upv, cost_components_wind]) - if write: - alloutm.to_csv( - os.path.join(inputs_case, "rsc_combined.csv"), index=False, header=True - ) - - #%%---------------------------------------------------------------------------------- - ####################### - # -- Biomass -- # - ####################### - """ - Biomass is currently being handled directly in b_inputs.gms - """ - - # %%---------------------------------------------------------------------------------- - ########################################## - # -- Spur lines (disaggregated) -- # - ########################################## - ### Get interconnection cost for reV sites within modeled area - interconnection_cost = reeds.io.assemble_supplycurve() - sitemap = reeds.io.get_sitemap() - county2zone = reeds.io.get_county2zone(os.path.dirname(os.path.normpath(inputs_case))) - interconnection_cost['r'] = interconnection_cost.index.map(sitemap.FIPS).map(county2zone) - val_r = pd.read_csv( - os.path.join(inputs_case, 'val_r.csv'), - header=None, - ).squeeze(1).values - spursites = interconnection_cost.loc[interconnection_cost.r.isin(val_r)].copy() - spursites['x'] = 'i' + spursites.index.astype(str) - if write: - spursites[["x", "cost_total_trans_usd_per_mw"]].rename(columns={"x": "*x"}).to_csv( - os.path.join(inputs_case, "spurline_cost.csv"), index=False - ) - spursites["x"].to_csv( - os.path.join(inputs_case, "x.csv"), index=False, header=False - ) - spursites[["x", "r"]].rename( - columns={"x": "*x"} - ).drop_duplicates().to_csv(os.path.join(inputs_case, "x_r.csv"), index=False) - - ###### Site maps - ### UPV - sitemap_upv = ( - upvin.assign(i="upv_" + upvin["class"].astype(str)) - .assign(rscbin="bin" + upvin["bin"].astype(str)) - .assign(x="i" + upvin["sc_point_gid"].astype(str)) - ) - sitemap_upv = ( - sitemap_upv - ### Assign rb's based on the no-exclusions transmission table - .assign(r=sitemap_upv.x.map(spursites.set_index("x").r))[ - ["i", "r", "rscbin", "x"] - ].rename(columns={"i": "*i"}) - ) - ### wind-ons - sitemap_windons = ( - windin["ons"] - .assign(i="wind-ons_" + windin["ons"]["class"].astype(str)) - .assign(rscbin="bin" + windin["ons"]["bin"].astype(str)) - .assign(x="i" + windin["ons"]["sc_point_gid"].astype(str)) - ) - sitemap_windons = ( - sitemap_windons - ### Assign r's based on the no-exclusions transmission table - .assign(r=sitemap_windons.x.map(spursites.set_index("x").r))[ - ["i", "r", "rscbin", "x"] - ].rename(columns={"i": "*i"}) - ) - - ### Combine, then only keep sites that show up in both supply curve and spur-line cost tables - spurline_sitemap_list = [sitemap_upv, sitemap_windons] - ### geohydro_allkm - if use_geohydro_rev_sc: - sitemap_geohydro = ( - geoin["geohydro"] - .assign(i="geohydro_allkm_" + geoin["geohydro"]["class"].astype(str)) - .assign(rscbin="bin" + geoin["geohydro"]["bin"].astype(str)) - .assign(x="i" + geoin["geohydro"]["sc_point_gid"].astype(str)) - ) - sitemap_geohydro = ( - sitemap_geohydro - ### Assign rb's based on the no-exclusions transmission table - .assign(r=sitemap_geohydro.x.map(spursites.set_index("x").r))[ - ["i", "r", "rscbin", "x"] - ].rename(columns={"i": "*i"}) - ) - spurline_sitemap_list.append(sitemap_geohydro) - ### egs_allkm - if use_egs_rev_sc: - sitemap_egs = ( - geoin["egs"] - .assign(i="egs_allkm_" + geoin["egs"]["class"].astype(str)) - .assign(rscbin="bin" + geoin["egs"]["bin"].astype(str)) - .assign(x="i" + geoin["egs"]["sc_point_gid"].astype(str)) - ) - sitemap_egs = ( - sitemap_egs - ### Assign rb's based on the no-exclusions transmission table - .assign(r=sitemap_egs.x.map(spursites.set_index("x").r))[ - ["i", "r", "rscbin", "x"] - ].rename(columns={"i": "*i"}) - ) - spurline_sitemap_list.append(sitemap_egs) - - spurline_sitemap = pd.concat(spurline_sitemap_list, ignore_index=True) - spurline_sitemap = spurline_sitemap.loc[ - spurline_sitemap.x.isin(spursites.x.values) - ].copy() - if write: - spurline_sitemap.to_csv( - os.path.join(inputs_case, "spurline_sitemap.csv"), index=False - ) - - ### Add mapping from sc_point_gid to bin for reeds_to_rev.py - site_bin_map_list = [ - upvin.assign(tech="upv")[["tech", "sc_point_gid", "bin"]] - ] - for wind_type, dfin in windin.items(): - site_bin_map_list.append( - dfin.assign(tech=f"wind-{wind_type}")[ - ["tech", "sc_point_gid", "bin"] - ] - ) - if use_geohydro_rev_sc: - site_bin_map_list.append( - geoin["geohydro"].assign(tech="geohydro_allkm")[ - ["tech", "sc_point_gid", "bin"] - ] - ) - if use_egs_rev_sc: - site_bin_map_list.append( - geoin["egs"].assign(tech="egs_allkm")[["tech", "sc_point_gid", "bin"]] - ) - site_bin_map = pd.concat(site_bin_map_list, ignore_index=True) - if write: - site_bin_map.to_csv(os.path.join(inputs_case, "site_bin_map.csv"), index=False) - - return alloutm - - -# %% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -if __name__ == "__main__": - ### Time the operation of this script - tic = datetime.datetime.now() - - ### Parse arguments - parser = argparse.ArgumentParser(description="Format and supply curves") - parser.add_argument("reeds_path", help="path to ReEDS directory") - parser.add_argument("inputs_case", help="path to inputs_case directory") - - args = parser.parse_args() - reeds_path = args.reeds_path - inputs_case = args.inputs_case - - #%% Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(inputs_case,'..','gamslog.txt'), - ) - - # %% Run it - print("Starting writesupplycurves.py") - - main(reeds_path=reeds_path, inputs_case=inputs_case) - - reeds.log.toc( - tic=tic, year=0, process='input_processing/writesupplycurves.py', - path=os.path.join(inputs_case,'..')) - - print('Finished writesupplycurves.py') diff --git a/interim_report.py b/interim_report.py deleted file mode 100644 index d4bce155..00000000 --- a/interim_report.py +++ /dev/null @@ -1,60 +0,0 @@ -#%%### Imports -import os -import sys -import subprocess -import argparse -import pandas as pd -from glob import glob - - -#%%### Arugment inputs -parser = argparse.ArgumentParser( - description='Run reporting scripts on latest completed year', - formatter_class=argparse.ArgumentDefaultsHelpFormatter, -) -parser.add_argument( - 'casepath', type=str, nargs='?', default='', - help='path to ReEDS case', -) -parser.add_argument( - '--only', '-o', type=str, default='', - help=',-delimited list of strings; only run reports that contain provided strings' -) -args = parser.parse_args() -casepath = args.casepath -only = [i for i in args.only.split(',') if len(i)] -if not len(casepath): - ### If the user provides a case path, switch to it; otherwise assume we're already there - casepath = os.path.dirname(os.path.abspath(__file__)) - - -#%%### Procedure -### Move to casepath -os.chdir(casepath) - -### Get model execution file -files = glob('*') -callfile = [c for c in files if os.path.basename(c).startswith('call')][0] -### Get final year_iteration that the model plans to run -final_year = int(pd.read_csv(os.path.join('inputs_case','modeledyears.csv')).columns[-1]) -final_year_iteration = f'{final_year}i0' - -#%% Get the lines to run -commands = [] -start_copying = 0 -with open(callfile, 'r') as f: - for line in f: - if ('# Output processing' in line) or start_copying: - start_copying = 1 - if (line.strip() != '') and not (line.strip().startswith('#')): - commands.append(line.strip()) - -### If "only" strings are specified, only include them (and cd lines) -if len(only): - commands = [cmd for cmd in commands if any([s in cmd for s in only+['cd ']])] - -#%% Run it -result = subprocess.run( - '\n'.join(commands), shell=True, - stderr=sys.stderr, stdout=sys.stdout, -) diff --git a/interim_report_batch.py b/interim_report_batch.py deleted file mode 100644 index 95a62e0e..00000000 --- a/interim_report_batch.py +++ /dev/null @@ -1,70 +0,0 @@ -''' -This script allows for batch re-running of just the reporting -functions (e_report.gms and e_report_dump.py) for a set of jobs -with a common batch prefix, specified as a command line argument. -It will make new copies of e_report.gms/e_report_dump.py to -each run folder, and can thus be useful to re-run reporting -for a large set of existing runs using new report scripts. - -It calls `interim_report.py` so can also be useful for reporting -results from runs that have not completed all years. - -author: bsergi -''' - -import os -import subprocess -import shutil -from glob import glob -import argparse -import sys - -parser = argparse.ArgumentParser() -parser.add_argument('--batch_name', '-b', type=str, default='', help='Prefix for batch of runs') -args = parser.parse_args() -# check if on hpc -hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False - -# get list of cases with matching batch name -case_list = glob(os.path.join('runs', '*')) -case_list = [c for c in case_list if args.batch_name in os.path.basename(c)] -if len(case_list) == 0: - sys.exit(f"No cases found with {args.batch_name} prefix.") -# list of new report files to copy -report_files = ["e_report.gms", "e_report_dump.py"] - -# loop over cases to copy files and run 'interim_report.py' for each one -for case in case_list: - # copy new report scripts into run folder - for f in report_files: - shutil.copy(f, os.path.join(case, f)) - # call runs - case_name = os.path.basename(case) - print(f"Running interim_report.py for {case_name}") - interim_report = os.path.join(case, "interim_report.py") - if os.name=='posix': - if hpc: - shutil.copy("srun_template.sh", os.path.join(case, "interim_report.sh")) - with open(os.path.join(case, "interim_report.sh"), 'a') as SPATH: - #add the name for easy tracking of the case - SPATH.writelines("\n#SBATCH --job-name=" + case_name + "_interim_report" + "\n\n") - - # load environments - SPATH.writelines("\nmodule purge\n") - SPATH.writelines("module load conda\n") - SPATH.writelines("conda activate reeds2\n") - SPATH.writelines("module load gams\n\n\n") - - #add the call to the python file to run the report - SPATH.writelines("python " + os.path.join(case, "interim_report.py")) - #close the file - SPATH.close() - #call that file - batchcom = "sbatch " + os.path.join(case, "interim_report.sh") - subprocess.Popen(batchcom.split()) - else: - os.chmod(interim_report, 0o777) - shellscript = subprocess.Popen(["python", interim_report]) - shellscript.wait() - else: - os.system('start /wait cmd /c ' + interim_report) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index debef993..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[tool.black] -include = ''' -( - hourlize/tests/.*\.py?$ - | hourlize/reeds_to_rev\.py?$ -) -''' - -[project] -name = "ReEDS" -version = "2024.0.0" - -[project.optional-dependencies] -docs = [ - "sphinx", - "myst-parser", -] - - - diff --git a/raw_value_streams.py b/raw_value_streams.py deleted file mode 100644 index d2f95961..00000000 --- a/raw_value_streams.py +++ /dev/null @@ -1,253 +0,0 @@ -''' -This file can be used to combine a coefficient matrix file (non-pre-solved) and associated marginals -from GAMS gdx solution file to produce value streams for the variables of the model. -''' - -import pandas as pd -import gdxpds -import subprocess -from datetime import datetime -import logging -logger = logging.getLogger('') - -def get_value_streams(solution_file, problem_file, var_list=None, con_list=None, prob_file_type='jacobian'): - ''' - Create dataframe of value streams for each variable of interest based on variable coefficients - in a coefficient matrix file and constraint and variable marginals in the associated GAMS gdx solution file. - Note that all strings are lowercased because GAMS is case insensitive. - Args: - solution_file (string): Full path to GAMS gdx solution file. - problem_file (string): Full path to the file of the model associated with the solution file. - var_list (list of strings): List of lowercased variable names that are of interest. If no list is given, - value streams will be created for all variables. If a list is given, variables not on the list will be - filtered out. - con_list (list of strings): List of lowercased constraint names that are of interest. If no list is given, - value streams will be created for all constraints. If a list is given, constraints not on the list will be - filtered out. - prob_file_type (string): Either 'jacobian' or 'mps'. - Returns: - df (pandas dataframe): Value streams of variables with these columns: - var_name (string): Name of variable. - var_set (string): Period-seperated sets of the variable. - con_name (string): Constraint name or '_obj' for objective coefficients. - con_set (string): Period-seperated sets of the constraint. - coeff (float): Coefficient of the variable in the constraint or objective. - var_level (float): Level of the variable in the solution. - var_marginal (float): Marginal of the variable in the solution. - con_level (float): Level of the constraint in the solution. - con_marginal (float): Marginal of the constraint in the solution or -1 for the objective (costs are negative). - value_per_unit (float): Value per unit of the variable from that constraint (equal to coeff * var_marginal). - value (float): Value that is produced by the variable from the constraint (equal to var_level * value_per_unit). - ''' - if prob_file_type == 'jacobian': - df_prob = get_df_jacobian(problem_file, var_list, con_list) - elif prob_file_type == 'mps': - df_prob = get_df_mps(problem_file, var_list, con_list) - var_list_prob = df_prob['var_name'].unique() - con_list_prob = df_prob['con_name'].unique() - dfs_solution = get_df_solution(solution_file, var_list_prob, con_list_prob) - df = pd.merge(left=df_prob, right=dfs_solution['vars'], how='left', on=['var_name', 'var_set'], sort=False) - df = pd.merge(left=df, right=dfs_solution['cons'], how='left', on=['con_name', 'con_set'], sort=False) - #The objective essentially has a con_marginal of -1 - df.loc[df['con_name']=='_obj','con_marginal'] = -1 - #When variable has a marginal, this marginal must be added as a new row - df_var_marg = df[df['var_marginal'] != 0].copy() - df_var_marg.drop_duplicates(inplace=True, subset=['var_name','var_set']) - df_var_marg['con_name'] = 'var' - df_var_marg['coeff'] = 1 - df_var_marg['con_marginal'] = df_var_marg['var_marginal'] - #concatenate back into original dataframe - df = pd.concat([df, df_var_marg], ignore_index=True) - #Calculate value streams - df['value_per_unit'] = df['coeff']*df['con_marginal'] - df['value'] = df['value_per_unit']*df['var_level'] - return df - -def get_df_mps(mps_file, var_list=None, con_list=None): - ''' - Create dataframe of coefficients for each variable in each constraint and objective function. Note that all - strings are lowercased because GAMS is case insensitive. - Args: - mps_file (string): Full path to the non-presolved mps file of the model associated with the solution file. - var_list (list of strings): List of lowercased variable names that are of interest. If no list is given, - value streams will be created for all variables. If a list is given, variables not on the list will be - filtered out. - con_list (list of strings): List of lowercased constraint names that are of interest. If no list is given, - value streams will be created for all constraints. If a list is given, constraints not on the list will be - filtered out. - Returns: - df (pandas dataframe): Value streams of variables with these columns: - var_name (string): Name of variable. - var_set (string): Period-seperated sets of the variable. - con_name (string): Constraint name or '_obj' for objective coefficients. - con_set (string): Period-seperated sets of the constraint. - coeff (float): Coefficient of the variable in the constraint or objective. - ''' - - #First, gather all rows of mps between 'COLUMNS' and 'RHS' into a dataframe, - #separating the set elements from the variable and constraint names, and separating - #the coefficients into their own column - start = datetime.now() - mps_ls = [] - columns = False - with open(mps_file) as mpsfile: - for line in mpsfile: - if columns: - if line[:3] == 'RHS': - break - if line[:8] == ' MARK': - continue - #split on whitespace - ls = line.split() - if len(ls) > 3: - #This means there was whitespace in one of the set elements. We need to recombine list elements. - i = 0 - while i < len(ls): - if '(' in ls[i] and ')' not in ls[i]: - j = i - while ')' not in ls[j]: - j = j + 1 - ls[i:j+1] = [' '.join(ls[i:j+1])] - i = i + 1 - var_ls = ls[0].split('(') - con_ls = ls[1].split('(') - if (var_list == None or var_ls[0].lower() in var_list) and (con_list == None or con_ls[0].lower() in con_list + ['_obj']): - if len(var_ls) == 1: - var_ls.append('') - else: - var_ls[1] = var_ls[1][:-1].replace('"','').replace("'",'') - if len(con_ls) == 1: - con_ls.append('') - else: - con_ls[1] = con_ls[1][:-1].replace('"','').replace("'",'') - mps_ls.append(var_ls + con_ls + [float(ls[2])]) - elif line[:7] == 'COLUMNS': - columns = True - df_mps = pd.DataFrame(mps_ls) - df_mps.columns = ['var_name','var_set','con_name','con_set', 'coeff'] - for col in ['var_name','var_set','con_name','con_set']: - df_mps[col] = df_mps[col].str.lower() - logger.info('mps read: ' + str(datetime.now() - start)) - return df_mps - -def get_df_jacobian(jacobian_file, var_list=None, con_list=None): - ''' - Create dataframe of coefficients for each variable in each constraint and objective function. Note that all - strings are lowercased because GAMS is case insensitive. - Args: - jacobian_file (string): Full path to the non-presolved jacobian gdx file of the model associated with the solution file. - var_list (list of strings): List of lowercased variable names that are of interest. If no list is given, - value streams will be created for all variables. If a list is given, variables not on the list will be - filtered out. - con_list (list of strings): List of lowercased constraint names that are of interest. If no list is given, - value streams will be created for all constraints. If a list is given, constraints not on the list will be - filtered out. - Returns: - df (pandas dataframe): Value streams of variables with these columns: - var_name (string): Name of variable. - var_set (string): Period-seperated sets of the variable. - con_name (string): Constraint name or '_obj' for objective coefficients. - con_set (string): Period-seperated sets of the constraint. - coeff (float): Coefficient of the variable in the constraint or objective. - ''' - start = datetime.now() - df_A = gdxpds.to_dataframe(jacobian_file, 'A', old_interface=False) - for x in ['j','i']: - #For i (equation) and j (variable) sets, I need to dump to csv to get the Text column, ugh - x_file = jacobian_file.replace('.gdx',f'_{x}.csv') - subprocess.Popen(f'gdxdump "{jacobian_file}" format=csv epsout=0 symb={x} output="{x_file}" CSVSetText').wait() - df_x = pd.read_csv(x_file) - df_x[['name','set']] = df_x['Text'].str.rstrip(')').str.replace(',','.').str.lower().str.split('(', expand=True, n=1) - if x == 'j': - #'j' means variable - name_col = 'var_name' - set_col = 'var_set' - lst = var_list - else: - #'i' means equation - name_col = 'con_name' - set_col = 'con_set' - lst = con_list - df_x = df_x.rename(columns={'Dim1':x, 'name':name_col, 'set':set_col}) - df_x = df_x.drop(columns=['Text']) - if lst != None: - df_x = df_x[df_x[name_col].isin(lst)].copy() - #inner merge with df_A (note that map may be much faster) - df_A = df_A.merge(df_x, on=x, how='inner') - df_A = df_A.rename(columns={'Value':'coeff'}) - df_A = df_A[['var_name','var_set','con_name','con_set','coeff']].copy() - logger.info('jacobian read: ' + str(datetime.now() - start)) - return df_A - -def get_df_solution(solution_file, var_list_prob, con_list_prob): - ''' - Create dataframes of marginals and levels of variables and constraints of interest. - Note that all strings are lowercased because GAMS is case insensitive. - Args: - solution_file (string): Full path to GAMS gdx solution file. - var_list_prob (list of strings): List of lowercased variable names that are of interest. - con_list_prob (list of strings): List of lowercased constraint names that are of interest. - Returns: - dict of two pandas dataframes, one for variables ('vars'), and one for constraints ('cons'). - Columns in the 'vars' dataframe: - var_name (string): Name of variable. - var_set (string): Period-seperated sets of the variable. - var_level (float): Level of the variable in the solution. - var_marginal (float): Marginal of the variable in the solution. - Columns in the 'cons' dataframe - con_name (string): Constraint name. - con_set (string): Period-seperated sets of the constraint. - con_level (float): Level of the constraint in the solution. - con_marginal (float): Marginal of the constraint in the solution. - ''' - start = datetime.now() - dfs = gdxpds.to_dataframes(solution_file) - logger.info('solution read: ' + str(datetime.now() - start)) - start = datetime.now() - dfs = {k.lower(): v for k, v in list(dfs.items())} - df_vars = get_df_symbols(dfs, var_list_prob) - df_vars = df_vars.rename(columns={"Level": "var_level", "Marginal": "var_marginal", 'sym_name':'var_name', 'sym_set': 'var_set'}) - df_cons = get_df_symbols(dfs, con_list_prob) - df_cons = df_cons.rename(columns={"Level": "con_level", "Marginal": "con_marginal", 'sym_name':'con_name', 'sym_set': 'con_set'}) - logger.info('solution reformatted: ' + str(datetime.now() - start)) - return {'vars':df_vars, 'cons':df_cons} - -def get_df_symbols(dfs, symbols): - ''' - Create dataframes of marginals and levels of symbols of interest. - Note that all strings are lowercased because GAMS is case insensitive. - Args: - dfs (dict of pandas dataframes): A result of gdxpds.to_dataframes(), with keys lowercased. - Dataframes of gdxpds.to_dataframes() always have separate columns for each set, - followed by Level, Marginal, Lower, Upper, and Scale columns. - symbols (list of strings): List of lowercased symbol names that are of interest. - Returns: - df_syms (pandas dataframe): dataframe of symbol levels and marginals with these columns: - sym_name (string): Name of symbol, lowercased - sym_set (string): Period-seperated sets of the symbol, lowercased - Level (float): Level of the symbol in the solution. - Marginal (float): Marginal of the symbol in the solution. - ''' - df_syms = [] - for sym_name in symbols: - if sym_name not in dfs: - continue - df_sym = dfs[sym_name] - df_sym['sym_name'] = sym_name - #concatenate all the set columns into one column - level_col = df_sym.columns.get_loc('Level') - df_sym['sym_set'] = '' - for s in range(level_col): - set_col = df_sym.iloc[:,s] - if set_col.str.contains(r'[.\'"()]| ').any(): - logger.info('Warning: Invalid character (dot, quote, parens, double space) found in column #' + str(s) + ' of ' + sym_name) - df_sym['sym_set'] = df_sym['sym_set'] + set_col - if s < level_col - 1: - df_sym['sym_set'] = df_sym['sym_set'] + '.' - #reduce to only the columns of interest - df_sym = df_sym[['sym_name','sym_set','Level','Marginal']] - df_syms.append(df_sym) - df_syms = pd.concat(df_syms).reset_index(drop=True) - for col in ['sym_name','sym_set']: - df_syms[col] = df_syms[col].str.lower() - return df_syms \ No newline at end of file diff --git a/reeds/inputs.py b/reeds/inputs.py deleted file mode 100644 index 45dfc002..00000000 --- a/reeds/inputs.py +++ /dev/null @@ -1,784 +0,0 @@ -### Imports -import os -import re -import sys -import yaml -import hashlib -import shapely -import numpy as np -import pandas as pd -import sklearn.cluster -import geopandas as gpd -from pathlib import Path -from warnings import warn -sys.path.append(str(Path(__file__).parent.parent)) -import reeds -from input_processing import mcs_sampler - -### Functions -def parse_regions(case_or_string, case=None): - """ - Inputs - ------ - case_or_string: path to a ReEDS case or a parseable string in the format of GSw_Region - case: path to a ReEDS case. Only used if case_or_string is not a ReEDS case. Should be - used if you want to select a subset of model zones from a ReEDS case that used - region aggregation. - - Returns - ------- - np.array of zone names - - If case_or_string is a case, return the regions modeled in the run - - If case_or_string is a parseable string in the format of GSw_Region, return - the regions that obey that string - - Examples - -------- - parse_regions('transreg/NYISO') -> ['p127', 'p128'] - parse_regions('st/PA') -> ['p115', 'p119', 'p120', 'p122'] - parse_regions('st/PA', 'path/to/case/using/region/aggregation') -> ['p115', 'p120', 'z122'] - """ - if os.path.exists(case_or_string): - sw = reeds.io.get_switches(case_or_string) - hierarchy = reeds.io.get_hierarchy(case_or_string) - GSw_Region = sw['GSw_Region'] - ## Provide case argument if using aggregated regions - elif os.path.exists(str(case)): - hierarchy = reeds.io.get_hierarchy(case) - GSw_Region = case_or_string - else: - hierarchy = reeds.io.get_hierarchy() - GSw_Region = case_or_string - - level, regions = GSw_Region.split('/') - regions = regions.split('.') - if level in ['r', 'ba']: - rs = [r for r in hierarchy.index if r in regions] - else: - rs = hierarchy.loc[hierarchy[level].isin(regions)].index - return rs - - -def parse_yearset(yearset:str) -> list: - """ - Parses a ReEDS-formatted yearset and returns a list of integer years. - - Args: - yearset (str): _-delimited list of individual years OR bash-formatted year ranges - - Returns: - list of integer years (sorted) - - Examples: - '2010' -> [2010] - '2010_2015_2020' -> [2010, 2015, 2020] - '2010..2020..5' -> [2010, 2015, 2020] - '2010_2015_2020..2050..3' -> [ - 2010, 2015, - 2020, 2023, 2026, 2029, 2032, 2035, 2038, 2041, 2044, 2047, 2050 - ] - '2010..2035..5_2040..2100..10' -> [ - 2010, 2015, 2020, 2025, 2030, 2035, - 2040, 2050, 2060, 2070, 2080, 2090, 2100 - ] - """ - pattern = r'^2\d{3}(\.\.2\d{3}(\.\.\d+)?)?(_2\d{3}(\.\.2\d{3}(\.\.\d+)?)?)*$' - helper = ( - "For formatting notes and examples, run the following commands:\n" - "$ python\n" - ">>> import reeds\n" - ">>> help(reeds.inputs.parse_yearset)" - ) - if not re.match(pattern, yearset): - err = f"Invalid yearset ({yearset}); must match {pattern}. {helper}" - raise ValueError(err) - yearstrings = yearset.split('_') - years = [] - for y in yearstrings: - subyears = [int(i) for i in y.split('..')] - if len(subyears) == 1: - years.append(subyears[0]) - elif len(subyears) == 2: - years.extend(range(subyears[0], subyears[1]+1)) - elif len(subyears) == 3: - years.extend(range(subyears[0], subyears[1]+1, subyears[2])) - else: - err = f"Invalid subyears ({subyears}) in yearset {yearset}. {helper}" - raise ValueError(err) - out = sorted(set(years)) - return out - - -def add_intermediate_switches(dfcases:pd.DataFrame) -> pd.DataFrame: - """Determine some switch settings from other switches""" - ignore_columns = ['Choices', 'Description', 'Default Value'] - cases = [i for i in dfcases if i not in ignore_columns] - new_switches = {} - for case in cases: - sw = dfcases[case] - new_switches[case] = {} - ### TEMPORARY 20260402: The GSw_RegionResolution switch is deprecated; - ### for now, hardcode its value for the region resolutions that use it - match sw['GSw_ZoneSet']: - case 'z134': - GSw_RegionResolution = 'ba' - case 'z3109': - GSw_RegionResolution = 'county' - case 'PJMcounty' | 'UTcounty': - GSw_RegionResolution = 'mixed' - case _: - GSw_RegionResolution = 'aggreg' - new_switches[case]['GSw_RegionResolution'] = GSw_RegionResolution - ### TEMPORARY 20260402: Turn off itlgrp constraint until it's fixed - # new_switches[case]['GSw_itlgrpConstraint'] = str(int( - # sw['GSw_RegionResolution'] in ['county', 'mixed'] - # )) - new_switches[case]['GSw_itlgrpConstraint'] = '0' - ## 'meshed' offshore files are only used when offshore zones are turned on - new_switches[case]['GSw_OffshoreFiles'] = ( - 'meshed' if int(sw['GSw_OffshoreZones']) else 'radial' - ) - ## Load site region level (GSw_LoadSiteReg) is embedded in GSw_LoadSiteTrajectory - new_switches[case]['GSw_LoadSiteReg'] = sw['GSw_LoadSiteTrajectory'].split('_')[0] - ## Get numbins from the max of individual technology bins - new_switches[case]['numbins'] = str(max( - int(sw['numbins_windons']), - int(sw['numbins_windofs']), - int(sw['numbins_upv']), - 15, - )) - dfcases_out = pd.concat([dfcases, pd.DataFrame(new_switches)]) - return dfcases_out - - -def parse_cases( - cases_filename:str='cases_test.csv', - single:str='', - skip_checks:bool=False, -) -> pd.DataFrame: - """ - Read a ReEDS cases file, look up empty switch values from "Default Value" or cases.csv, - and return a dataframe of all switches and values. - - Args: - cases_filename (str): 'cases_{something}.csv' or 'cases.csv' - single (str): If not '', specifies a single column to keep from cases_filename - skip_checks (bool): Skip case validation (not recommended) - - Returns: - pd.DataFrame - """ - dfcases = pd.read_csv( - os.path.join(reeds.io.reeds_path, 'cases.csv'), dtype=object, index_col=0) - - # If we have a case suffix, use cases_[suffix].csv for cases. - if cases_filename != 'cases.csv': - dfcases = dfcases[['Choices', 'Default Value']] - dfcases_suf = pd.read_csv( - os.path.join(reeds.io.reeds_path, cases_filename), dtype=object, index_col=0) - # Replace periods and spaces in case names with _ - dfcases_suf.columns = [ - c.replace(' ','_').replace('.','_') if c != 'Default Value' else c - for c in dfcases_suf.columns] - - # Check to make sure user-specified cases file has up-to-date switches - missing_switches = [s for s in dfcases_suf.index if s not in dfcases.index] - if len(missing_switches): - error = ( - "The following switches are in {} but have changed names or are no longer " - "supported by ReEDS:\n\n{} \n\nPlease update your cases file; " - "for the full list of available switches see cases.csv. " - "Note that switch names are case-sensitive." - ).format(cases_filename, '\n'.join(missing_switches)) - raise ValueError(error) - - # First use 'Default Value' from cases_[suffix].csv to fill missing switches - # Later, we will also use 'Default Value' from cases.csv to fill any remaining holes. - if 'Default Value' in dfcases_suf.columns: - case_i = dfcases_suf.columns.get_loc('Default Value') + 1 - casenames = dfcases_suf.columns[case_i:].tolist() - for case in casenames: - dfcases_suf[case] = dfcases_suf[case].fillna(dfcases_suf['Default Value']) - dfcases_suf.drop(['Choices','Default Value'], axis='columns',inplace=True, errors='ignore') - dfcases = dfcases.join(dfcases_suf, how='outer') - - casenames = [c for c in dfcases.columns if c not in ['Description','Default Value','Choices']] - # Get the list of switch choices - choices = dfcases.Choices.copy() - - for case in casenames: - # Fill any missing switches with the defaults in cases.csv - dfcases[case] = dfcases[case].fillna(dfcases['Default Value']) - - # If --single/-s was passed, only keep those cases (regardless of ignore) - # otherwise, drop any case marked ignore - if single: - if case not in single.split(','): - continue - else: - if int(dfcases.loc['ignore', case]) == 1: - continue - - # Check to make sure the switch setting is valid - for i, val in dfcases[case].items(): - if skip_checks: - continue - # check that the switch isn't duplicated - if isinstance(choices[i], pd.Series) and len(choices[i]) > 1: - error = ( - f'Duplicate entries for "{i}", delete one and restart.' - ) - raise ValueError(error) - ### Split choices by either '; ' or ',' - if choices[i] in ['N/A',None,np.nan]: - pass - elif choices[i].lower() in ['int','integer']: - try: - int(val) - except ValueError: - error = ( - f'Invalid entry for "{i}" for case "{case}".\n' - f'Entered "{val}" but must be an integer.' - ) - raise ValueError(error) - elif choices[i].lower() in ['float','numeric','number','num']: - try: - float(val) - except ValueError: - error = ( - f'Invalid entry for "{i}" for case "{case}".\n' - f'Entered "{val}" but must be a float (number).' - ) - raise ValueError(error) - else: - i_choices = [ - str(j).strip() for j in - np.ravel([i.split(',') for i in choices[i].split(';')]).tolist() - ] - matches = [re.match(choice, str(val)) for choice in i_choices] - if not any(matches): - error = ( - f'Invalid entry for "{i}" for case "{case}".\n' - f'Entered "{val}" but must match one of the following:\n> ' - + '\n> '.join(i_choices) - + f'\nOr, if "{val}" is intended, it must be added to the ' - '"Choices" column in cases.csv.' - ) - raise ValueError(error) - - # Check GSw_Region switch and ask user to correct if commas are used instead of - # periods to list multiple regions - if ',' in (dfcases[case].loc['GSw_Region']) : - print("Please change the delimeter in the GSw_Region switch from ',' to '.'") - quit() - - # If doing a Monte Carlo run, modify dfcases by adding new columns - # for each scenario run. Also validate the distribution file. - warned_about_cluster_alg = False - if 'MCS_runs' in dfcases.index: - for c in dfcases.columns: - if ( - c not in ['Description','Default Value','Choices'] - and (int(dfcases.loc['MCS_runs',c]) > 0) - and (not int(dfcases.loc['ignore',c])) - ): - # Warn user if the hourly clustering algorithm is not fixed for Monte Carlo runs - if ( - not dfcases.at['GSw_HourlyClusterAlgorithm', c].startswith('user') - and not warned_about_cluster_alg - ): - print(f"\n[Warning] Case Column: '{c}'") - print( - "You are attempting to run a Monte Carlo simulation with " - "`GSw_HourlyClusterAlgorithm` set to a value other than 'user'.\n" - "This may result in inconsistent representative days across MCS runs.\n\n" - "To ensure consistency, we strongly recommend setting " - "`GSw_HourlyClusterAlgorithm = user` in your switch configuration.\n" - "Do you want to proceed with the current setup?" - ) - user_input = input("Type 'yes' to proceed, or 'no' to exit: ").strip().lower() - if user_input not in ['yes', 'y']: - print("\nPlease update the `GSw_HourlyClusterAlgorithm` switch and restart.") - quit() - warned_about_cluster_alg = True - print() - - # Validate the distribution file - sw = dfcases[c].fillna(dfcases['Default Value']) - mcs_dist_path = os.path.join( - reeds.io.reeds_path, 'inputs', 'userinput', - 'mcs_distributions_{}.yaml'.format(sw.MCS_dist) - ) - mcs_sampler.general_mcs_dist_validation(reeds.io.reeds_path, mcs_dist_path, sw) - - # c (column) is a case with monte carlo runs. - # replicate this column N (NumMonteCarloRuns) times - NumMonteCarloRuns = int(dfcases.loc['MCS_runs',c]) - NewColumnNames = [ - f"{c}_MC{i:0>4}" - for i in range(1, NumMonteCarloRuns + 1) - ] - - # Each new column is a copy of the original column with name c_{MC1,MC2,...} - dfcases_MC = pd.DataFrame( - data=np.array([dfcases[c].values]*NumMonteCarloRuns).T, - index=dfcases.index, - columns=NewColumnNames, - ) - dfcases = pd.concat([dfcases, dfcases_MC], axis=1) - # drop the original column - dfcases.drop(c, axis=1, inplace=True) - - ## Add switches determined from other switches and remove unnecessary columns - dfcases_out = ( - add_intermediate_switches(dfcases) - .drop(columns=['Choices', 'Description', 'Default Value'], errors='ignore') - ) - - return dfcases_out - - -def get_bin( - df_in, - bin_num, - bin_method='equal_cap_cut', - bin_col='capacity_factor_ac', - bin_out_col='bin', - weight_col='capacity', -): - """ - bin supply curve points based on a specified bin column. Used in hourlize to create 'bins' - for the resource classes (typically using capacity factor) and then used by - writesupplycurves.py to create bins based on supply curve cost. - """ - df = df_in.copy() - ser = df[bin_col] - # If we have less than or equal unique points than bin_num, - # we simply group the points with the same values. - if ser.unique().size <= bin_num: - bin_ser = ser.rank(method='dense') - df[bin_out_col] = bin_ser.values - elif bin_method == 'kmeans': - nparr = ser.to_numpy().reshape(-1,1) - weights = df[weight_col].to_numpy() - kmeans = ( - sklearn.cluster.KMeans(n_clusters=bin_num, random_state=0, n_init=10) - .fit(nparr, sample_weight=weights) - ) - bin_ser = pd.Series(kmeans.labels_) - # but kmeans doesn't necessarily label in order of increasing value because it is 2D, - # so we replace labels with cluster centers, then rank - kmeans_map = pd.Series(kmeans.cluster_centers_.flatten()) - bin_ser = bin_ser.map(kmeans_map).rank(method='dense') - df[bin_out_col] = bin_ser.values - elif bin_method == 'equal_cap_man': - # using a manual method instead of pd.cut because i want the first bin to contain the - # first sc point regardless, even if its weight_col value is more than the capacity - # of the bin, and likewise for other bins, so i don't skip any bins. - orig_index = df.index - df.sort_values(by=[bin_col], inplace=True) - cumcaps = df[weight_col].cumsum().tolist() - totcap = df[weight_col].sum() - vals = df[bin_col].tolist() - bins = [] - curbin = 1 - for i, _v in enumerate(vals): - bins.append(curbin) - if cumcaps[i] >= totcap*curbin/bin_num: - curbin += 1 - df[bin_out_col] = bins - # we need the same index ordering for apply to work - df = df.reindex(index=orig_index) - elif bin_method == 'equal_cap_cut': - # Use pandas.cut with cumulative capacity in each class. This will assume equal capacity bins - # to bin the data. - orig_index = df.index - df.sort_values(by=[bin_col], inplace=True) - df['cum_cap'] = df[weight_col].cumsum() - bin_ser = pd.cut(df['cum_cap'], bin_num, labels=False) - bin_ser = bin_ser.rank(method='dense') - df[bin_out_col] = bin_ser.values - # we need the same index ordering for apply to work - df = df.reindex(index=orig_index) - df[bin_out_col] = df[bin_out_col].astype(int) - return df - - -def hash_string(string:str, hashfunc='md5') -> str: - """Return the hash of a string""" - _hashfunc = getattr(hashlib, hashfunc) - return _hashfunc(string.encode()).hexdigest() - - -def hash_counties(countylist, delim_county=',', hashfunc='md5') -> list: - """ - Takes a list of 5-digit county FIPS codes, sorts them, concatenates them into a string - delimited by `delim_county`, and returns a hash using the hashlib function - specified by `hashfunc`. - """ - ## Validate the inputs - invalid = [i for i in countylist if not re.match(r'^\d{5}$', i)] - if len(invalid): - err = ( - "The following entries in countylist do not look like 5-digit FIPS codes:\n" - ','.join(invalid) - ) - raise ValueError(err) - delim_string = delim_county.join(sorted(countylist)) - return hash_string(delim_string, hashfunc=hashfunc) - - -def get_itl_config() -> dict: - configpath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'itl_config.yaml') - with open(configpath, 'r') as f: - config = yaml.safe_load(f) - return config - - -def get_itl(r, rr, case=None, errors='raise', **kwargs) -> dict: - """ - Get the ITL for a single interface from zone `r` to `rr`. - The resolution can be provided by: - - Providing a path to a ReEDS run via `case` - - Providing `GSw_ZoneSet` as a keyword argument - """ - sw = reeds.io.get_switches(case, **kwargs) - config = get_itl_config() - hashfunc = config['hashfunc'] - county2zone = reeds.io.get_county2zone(case, **kwargs) - rs = county2zone.unique() - for _r, rlabel in [(r, 'r'), (rr, 'rr')]: - if _r not in rs: - err = f"{rlabel} = {_r} is not defined for GSw_ZoneSet = {sw.GSw_ZoneSet}" - raise KeyError(err) - ## Get the ITLs for all interfaces - itlspath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'itl_NARIS.csv') - itls = pd.read_csv(itlspath, index_col=[f'{hashfunc}_from', f'{hashfunc}_to']) - ## Look up the desired interface - rhash = hash_counties(county2zone.loc[county2zone==r].index.tolist()) - rrhash = hash_counties(county2zone.loc[county2zone==rr].index.tolist()) - try: - itl = itls.loc[rhash, rrhash].to_dict() - except KeyError: - try: - ## Check for the other direction. If it exists, reverse the definition of - ## 'forward' and 'reverse' to match the user-provided 'r' and 'rr'. - _itl = itls.loc[rrhash, rhash].to_dict() - itl = {'MW_forward': _itl['MW_reverse'], 'MW_reverse': _itl['MW_forward']} - except KeyError: - ## The requested interface is not in the table - itl = {'MW_forward':0, 'MW_reverse':0} - interfacepath = Path(reeds.io.reeds_path, 'inputs', 'zones', sw.GSw_ZoneSet) - err = ( - f"The interface defined by r = {r} and rr = {rr} with " - f"GSw_ZoneSet = {sw.GSw_ZoneSet} does not have an ITL in {itlspath}. " - "It either has not been calculated or the provided zones are not " - f"connected. Check {interfacepath} to see if a value is expected for " - "this interface." - ) - if errors == 'raise': - raise KeyError(err) - elif errors == 'warn': - warn(err) - return itl - - -def get_itls(case=None, level:str='r', errors='raise', **kwargs) -> pd.DataFrame: - """ - Get all the ITLs for the specified resolution. The resolution can be specified by: - - Providing a path to a ReEDS run via `case` - - Providing `GSw_ZoneSet` as a keyword argument - If neither `case` nor `GSw_ZoneSet` are provided, the default resolution from - `cases.csv` is used. - - Args: - level (str): 'r' or 'transgrp' - - Inputs for testing: - case = None - level = 'r' - kwargs = {} - errors = 'raise' - """ - sw = reeds.io.get_switches(case, **kwargs) - inputs = Path(reeds.io.reeds_path, 'inputs') - ## Get the ITL config settings - config = get_itl_config() - hashfunc = config['hashfunc'] - ## Get the ITLs for all interfaces - fpath = Path(inputs, 'transmission', 'itl_NARIS.csv') - itls = ( - pd.read_csv(fpath) - .rename(columns={ - f'{hashfunc}_from':f'{hashfunc}_r', - f'{hashfunc}_to':f'{hashfunc}_rr', - }) - ) - if itls.index.duplicated().sum(): - raise ValueError('Duplicate entries in ITL database') - ### Get the zone hashes - if level == 'r': - ## We save the zonehash for level == 'r' directly for peace of mind - zonehash = pd.read_csv( - Path(inputs, 'zones', sw.GSw_ZoneSet, 'zonehash.csv'), - index_col='r', - )[hashfunc] - else: - ## For other levels, we calculate the zonehash from the hierarchy - hierarchy = reeds.io.assemble_hierarchy(case, **kwargs).set_index('r') - county2zone = reeds.io.get_county2zone(case=None, **kwargs) - county2level = county2zone.map(hierarchy[level]).rename(level) - if county2level.isnull().sum(): - print(county2level.loc[county2level.isnull()]) - err = ( - "Model zones in county2zone.csv and hierarchy.csv " - f"for GSw_ZoneSet={sw.GSw_ZoneSet} do not match" - ) - raise ValueError(err) - zonehash = county2level.reset_index().groupby(level).FIPS.agg(hash_counties) - ### Get the ITLs - interfacepath = Path(inputs, 'zones', sw.GSw_ZoneSet, f'interfaces_{level}.csv') - dfout = pd.read_csv(interfacepath) - for i, r in enumerate(['r', 'rr']): - dfout[r] = dfout.interface.str.split(config['idelim']).str[i] - dfout[f'{hashfunc}_{r}'] = dfout[r].map(zonehash) - dfout = dfout.merge(itls, on=[f'{hashfunc}_r', f'{hashfunc}_rr'], how='left') - ### Make sure it worked - missing = dfout.loc[dfout.MW_forward.isnull() | dfout.MW_reverse.isnull()] - if len(missing): - print(missing) - err = f'Missing ITL for {len(missing)} interfaces' - if len(missing) <= 10: - err += ': ' + (' '.join(missing.interface)) - if errors == 'raise': - raise KeyError(err) - elif errors == 'warn': - warn(err) - return dfout.dropna() - - -def get_zones(case=None, crs='ESRI:102008', **kwargs) -> gpd.GeoDataFrame: - """ - Args: - case (str, Path, or None): Path to a ReEDS case. - If None, uses the default GSw_ZoneSet from cases.csv. - crs (str): Coordinate reference system - **kwargs: ReEDS switch:value pairs (overrides case argument) - """ - dfcounty = reeds.spatial.get_map('county', source='tiger', crs=crs) - dfstates = reeds.spatial.get_map('states', source='census', crs=crs) - country = dfstates.dissolve().geometry[0] - county2zone = reeds.io.get_county2zone(case, **kwargs) - - dfcounty['r'] = county2zone - - dfzones = dfcounty.dissolve('r') - dfzones.geometry = dfzones.intersection(country).buffer(0) - - return dfzones[['geometry']] - - -def _make_line(row): - return shapely.LineString([[row.from_lon, row.from_lat], [row.to_lon, row.to_lat]]) - - -def get_hvdc_lines(): - """Load data for individual HVDC lines""" - datapath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'hvdc_lines.csv') - dfdc = pd.read_csv(datapath) - dfdc['geometry'] = dfdc.apply(_make_line, axis=1) - dfdc = gpd.GeoDataFrame(dfdc, crs='EPSG:4326') - for i, side in enumerate(['from', 'to']): - dfdc[f'{side}_latlon'] = dfdc.geometry.map(lambda x: shapely.geometry.Point(x.coords[i])) - return dfdc - - -def map_hvdc_lines_to_interfaces(case=None, **kwargs) -> pd.DataFrame: - """ - Assign HVDC line capacity to interfaces by mapping start/end points to zones - - Inputs for testing: - case = None - kwargs = {'GSw_ZoneSet': 'z90'} - """ - dfzones = get_zones(case, **kwargs) - dfdc = get_hvdc_lines().to_crs(dfzones.crs) - for i, side in enumerate(['from', 'to']): - dfdc[f'zone_{side}'] = gpd.sjoin( - dfdc.set_geometry(f'{side}_latlon').set_crs('EPSG:4326').to_crs(dfzones.crs), - dfzones.reset_index(), - how='left', - )['r'] - - dfcap = ( - dfdc.loc[dfdc.zone_from != dfdc.zone_to].dropna() - .rename(columns={'zone_from':'r', 'zone_to':'rr'}) - ).copy() - ## Normalize from/to order and sum capacity for each interface - for index, row in dfcap.iterrows(): - for side, r in enumerate(['r', 'rr']): - dfcap.loc[index, r] = sorted(row[['r','rr']])[side] - dfout = dfcap.groupby(['r','rr'])[['name','MW']].agg({'MW':sum, 'name':list}) - return dfout - - -def get_b2b(case=None, **kwargs) -> pd.DataFrame: - """ - Get back-to-back (B2B) converter capacity for specified zone resolution. - Check it against the sum of known individual converter capacities. - - Inputs for testing: - case = None - kwargs = {} - """ - sw = reeds.io.get_switches(case, **kwargs) - b2bpath = Path(reeds.io.reeds_path, 'inputs', 'zones', sw.GSw_ZoneSet, 'b2b.csv') - b2b = pd.read_csv(b2bpath).drop(columns=['name', 'notes'], errors='ignore') - - ## Take the sum by interconnection for validation - hierarchy = reeds.io.assemble_hierarchy(case, **kwargs).set_index('r') - _b2b = b2b.copy() - for i, (r, side) in enumerate([('r', 'from'), ('rr', 'to')]): - _b2b[f'interconnect_{side}'] = _b2b[r].map(hierarchy.interconnect) - _b2b['interface'] = _b2b.apply( - lambda row: '~~'.join(sorted([row.interconnect_from, row.interconnect_to])), - axis=1 - ) - b2b_interconnect = _b2b.groupby('interface').MW.sum() - - ## Get data for individual converters - vpath = Path(reeds.io.reeds_path, 'inputs', 'transmission', 'b2b_converters.csv') - converters = pd.read_csv(vpath) - converters['interface'] = converters.apply( - lambda row: '~~'.join(sorted([row.interconnection_from, row.interconnection_to])), - axis=1 - ) - converters_interconnect = converters.groupby('interface').MW.sum() - - ## Interface capacity should match sum of individual converters - if (b2b_interconnect != converters_interconnect).any(): - err = ( - f"The B2B interface capacity in {b2bpath} does not match the sum of " - f"individual B2B converter capacity in {vpath}" - ) - raise ValueError(err) - - return b2b - - -def check_aggreg_unique(hierarchy): - """ - Make sure each aggreg is only assigned to a single transreg / transgrp / st / etc. - """ - testcols = [i for i in hierarchy.columns if i != 'aggreg'] - aggreg_errors = {} - for col in testcols: - unique_aggregs = ( - hierarchy[[col,'aggreg']] - .drop_duplicates() - .groupby('aggreg')[col].count() - ) - duplicated = unique_aggregs.loc[unique_aggregs>1] - if len(duplicated): - aggreg_errors[col] = hierarchy.loc[ - hierarchy.aggreg.isin(duplicated.index), - [col,'aggreg'] - ] - return aggreg_errors - - -def validate_zoneset(GSw_ZoneSet): - """ - Make sure all the required inputs are supplied for GSw_ZoneSet - - Test all options: - GSw_ZoneSets = [ - 'z48', - 'z54', - 'z69', - 'z90', - 'z132', - 'z134', - # 'z153', - # 'z1259', - # 'z2972', - 'z3109', - 'UTcounty', - 'PJMcounty', - ] - for GSw_ZoneSet in GSw_ZoneSets: - print(GSw_ZoneSet) - validate_zoneset(GSw_ZoneSet) - """ - zonepath = Path(reeds.io.reeds_path, 'inputs', 'zones', GSw_ZoneSet) - ## Do all the files exist? - required_files = [ - 'b2b.csv', - 'county2zone.csv', - 'hierarchy.csv', - 'interfaces_r.csv', - 'interfaces_transgrp.csv', - 'zonehash.csv', - ] - missing = [f for f in required_files if not Path(zonepath, f).is_file()] - if len(missing): - err = f'Missing these files from {zonepath}: ' + ' '.join(missing) - raise FileNotFoundError(err) - ## Are all/only the right counties included? - fpath_county2zone = Path(zonepath, 'county2zone.csv') - fpath_countystate = Path(reeds.io.reeds_path, 'inputs', 'zones', 'county_state.csv') - county2zone = pd.read_csv(fpath_county2zone, dtype=str) - county_state = pd.read_csv(fpath_countystate, dtype=str) - extra_fips = county2zone.loc[~county2zone.FIPS.isin(county_state.FIPS), 'FIPS'].values - missing_fips = county_state.loc[~county_state.FIPS.isin(county2zone.FIPS), 'FIPS'].values - if len(extra_fips): - raise ValueError( - f"{len(extra_fips)} counties should NOT be in {fpath_county2zone}: " - f"{', '.join(extra_fips)}" - ) - if len(missing_fips): - raise ValueError ( - f"{len(missing_fips)} counties are missing from {fpath_county2zone}: " - f"{', '.join(missing_fips)}" - ) - ## Do the zone definitions in county2zone.csv match zonehash.csv? - config = get_itl_config() - hashfunc = config['hashfunc'] - zonehash = pd.read_csv(Path(zonepath, 'zonehash.csv'), index_col='r')[hashfunc] - checkhash = county2zone.groupby('r').FIPS.agg(hash_counties) - if (zonehash != checkhash).any(): - _df = pd.concat({'zonehash.csv':zonehash, 'county2zone.csv':checkhash}, axis=1) - wrong = _df.loc[_df['zonehash.csv'] != _df['county2zone.csv']] - print(wrong) - raise ValueError( - f"zonehash.csv and county2zone.csv in inputs/zones/{GSw_ZoneSet} do not " - f"match for {len(wrong)} zones: {', '.join(wrong.index)}" - ) - ## Do all the zone interfaces have ITLs? - get_itls(GSw_ZoneSet=GSw_ZoneSet, errors='raise') - ## Do all the transgrp interfaces have ITLs? - get_itls(GSw_ZoneSet=GSw_ZoneSet, level='transgrp', errors='raise') - ## Do the hierarchy files have all the required columns? - required_levels = ['st', 'transreg', 'transgrp', 'nercr', 'interconnect'] - hierarchy = reeds.io.assemble_hierarchy(GSw_ZoneSet=GSw_ZoneSet).set_index('r') - missing = [i for i in required_levels if i not in hierarchy] - if len(missing): - hierarchypath = Path(zonepath, 'hierarchy.csv') - err = f'The following columns are missing from {hierarchypath}: ' + ' '.join(missing) - raise KeyError(err) - ## TEMPORARY 20260402: Is each aggreg only assigned to a single hierarchy level? - fpath_134 = Path(zonepath, 'hierarchy_from134.csv') - if fpath_134.is_file(): - hierarchy_134 = pd.read_csv(fpath_134, index_col='ba') - errors = check_aggreg_unique(hierarchy_134) - if len(errors): - for v in errors.values(): - print(v) - print() - err = ( - "There are aggreg values spanning multiple hierarchy levels for:\n > " - + '\n > '.join(errors.keys()) - + f"\nPlease modify {fpath_134}\n" - "to ensure each aggreg is only assigned to a single hierarchy level." - ) - raise ValueError(err) diff --git a/reeds/ra.py b/reeds/ra.py deleted file mode 100644 index 22d8ee35..00000000 --- a/reeds/ra.py +++ /dev/null @@ -1,125 +0,0 @@ -### Imports -import os -import sys -import numpy as np -import pandas as pd -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -### Functions -def get_pras_eue(case, t, iteration=0): - """ - """ - ### Get PRAS outputs - dfpras = reeds.io.read_pras_results( - os.path.join(case, 'ReEDS_Augur', 'PRAS', f"PRAS_{t}i{iteration}.h5") - ) - ### Create the time index - sw = reeds.io.get_switches(case) - dfpras.index = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) - - ### Keep the EUE columns by zone - eue_tail = '_EUE' - dfeue = dfpras[[ - c for c in dfpras - if (c.endswith(eue_tail) and not c.startswith('USA')) - ]].copy() - ## Drop the tailing _EUE - dfeue = dfeue.rename( - columns=dict(zip(dfeue.columns, [c[:-len(eue_tail)] for c in dfeue]))) - - return dfeue - - -def get_eue_periods( - case, t, iteration=0, - hierarchy_level='transgrp', - stress_metric='EUE', - period_agg_method='sum', - ): - """_summary_ - - Args: - sw (pd.series): ReEDS switches for this run. - t (int): Model solve year. - iteration (int, optional): Iteration number of this solve year. Defaults to 0. - hierarchy_level (str, optional): column of hierarchy.csv specifying the spatial - level over which to calculate stress_metric. Defaults to 'country'. - stress_metric (str, optional): 'EUE' or 'NEUE'. Defaults to 'EUE'. - period_agg_method (str, optional): 'sum' or 'max', indicating how to aggregate - over the hours in each period. Defaults to 'sum'. - - Raises: - NotImplementedError: if invalid value for stress_metric or GSw_PRM_StressModel - - Returns: - pd.DataFrame: Table of periods sorted in descending order by stress metric. - """ - sw = reeds.io.get_switches(case) - ### Get the region aggregator - rmap = reeds.io.get_rmap(case=case, hierarchy_level=hierarchy_level) - - ### Get EUE from PRAS - dfeue = get_pras_eue(case=case, t=t, iteration=iteration) - ## Aggregate to hierarchy_level - dfeue = ( - dfeue - .rename_axis('r', axis=1).rename_axis('h', axis=0) - .rename(columns=rmap).groupby(axis=1, level=0).sum() - ) - - ###### Calculate the stress metric by period - if stress_metric.upper() == 'EUE': - ### Aggregate according to period_agg_method - dfmetric_period = ( - dfeue - .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) - elif stress_metric.upper() == 'NEUE': - ### Get load at hierarchy_level - dfload = reeds.io.read_h5py_file( - os.path.join( - case,'ReEDS_Augur','augur_data',f'pras_load_{t}.h5') - ).rename(columns=rmap).groupby(level=0, axis=1).sum() - dfload.index = dfeue.index - - ### Recalculate NEUE [ppm] and aggregate appropriately - if period_agg_method == 'sum': - dfmetric_period = ( - dfeue - .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) / ( - dfload - .groupby([dfload.index.year, dfload.index.month, dfload.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) * 1e6 - elif period_agg_method == 'max': - dfmetric_period = ( - (dfeue / dfload) - .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) * 1e6 - - ### Sort and drop zeros and duplicates - dfmetric_top = ( - dfmetric_period.stack('r') - .sort_values(ascending=False) - .replace(0,np.nan).dropna() - .reset_index().drop_duplicates(['y','m','d'], keep='first') - .set_index(['y','m','d','r']).squeeze(1).rename(stress_metric) - .reset_index('r') - ) - ## Convert to timestamp, then to ReEDS period - dfmetric_top['actual_period'] = [ - reeds.timeseries.timestamp2h(pd.Timestamp(*d), sw['GSw_HourlyType']).split('h')[0] - for d in dfmetric_top.index.values - ] - - return dfmetric_top diff --git a/reeds2pras/Project.toml b/reeds2pras/Project.toml deleted file mode 100644 index ab41a55e..00000000 --- a/reeds2pras/Project.toml +++ /dev/null @@ -1,26 +0,0 @@ -name = "ReEDS2PRAS" -uuid = "c1db39dc-6a9d-11ed-0c9a-05c3ea2a1475" -version = "2025.2.0" - -[deps] -CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" -InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -PRAS = "05348d26-1c52-11e9-35e3-9d51842d34b9" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" - -[compat] -CSV = "~0.10" -DataFrames = "~1.7" -Dates = "1" -HDF5 = "~0.17" -InlineStrings = "~1.4" -JSON = "~0.21" -PRAS = "~0.7" -Statistics = "~1.11" -TimeZones = "~1.21" -julia = "^1.11" diff --git a/reeds2pras/R2P_Test_Summary.png b/reeds2pras/R2P_Test_Summary.png deleted file mode 100644 index 1a4d19b7..00000000 --- a/reeds2pras/R2P_Test_Summary.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9a0a46d1dc10b36da8e9891630d3699d7cbd9fd6c6f3ae8dae443fe9dc9c43e8 -size 148186 diff --git a/reeds2pras/README.md b/reeds2pras/README.md deleted file mode 100644 index c0ed00a9..00000000 --- a/reeds2pras/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# ReEDS2PRAS - -## Introduction - -The purpose of ReEDS2PRAS is to translate a ReEDS system into a PRAS system ready for probabilistic resource adequacy analysis. - -### Julia Installation - -[Juliaup](https://github.com/JuliaLang/juliaup) is a cross platform installer of the Julia programming language. -Detailed instructions to install Julia on different platforms are available from [Juliaup Installation Instructions](https://github.com/JuliaLang/juliaup?tab=readme-ov-file#installation). - -#### Mac/Linux - -Julia is included in the conda environment, so Julia does not need to be installed separately. -If you wish to install it for testing, you can run: - -```shell -curl -fsSL https://install.julialang.org | sh -``` - -#### Windows - -```shell -winget install --name Julia --id 9NJNWW8PVKMN -e -s msstore -``` - -Then, from the ReEDS-2.0 directory, run `julia --project=. instantiate.jl` to ensure proper installation of Julia and the ReEDS2PRAS environment. - -## Basic Usage - -If you have a completed ReEDS run and a REPL with ReEDS2PRAS (`using ReEDS2PRAS` after running `add ReEDS2PRAS` in the julia package manager), an example of running ReEDS2PRAS is provided below - -```julia -using ReEDS2PRAS - -reedscase = "/projects/ntps/llavin/ReEDS-2.0/runs/ntpsrerun_Xlim_DemHi_90by2035EarlyPhaseout__core" # path to completed ReEDS run -solve_year = 2035 #need ReEDS Augur data for the input solve year -weather_year = 2012 # must be 2007-2013 or 2016-2023 -timesteps = 8760 -user_descriptors = "your_user_descriptors_json_location_here" # Optional - if not passed uses default values - -# returns a parameterized PRAS system -pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, user_descriptors = user_descriptors) -``` - -This will save out a pras system to the variable `pras_system` from the ReEDS2PRAS run. The user can also save a PRAS system to a specific location using `PRAS.savemodel(pras_system, joinpath("MYPATH"*".pras")`. The saved PRAS system may then be read in by other tools like PRAS Analytics (`https://github.nrel.gov/PRAS/PRAS-Analytics`) for further analysis, post-processing, and plotting. - -## Multi-year usage - -ReEDS2PRAS can be run for multiple weather years of a completed ReEDS run by passing more than 8760 hourly timestamps. For example running all 7 weather years can be accomplished as in the below example - -```julia -using ReEDS2PRAS - -# path to completed ReEDS run -reedscase = "/projects/ntps/llavin/ReEDS-2.0/runs/ntpsrerun_Xlim_DemHi_90by2035EarlyPhaseout__core" -solve_year = 2035 #need ReEDS Augur data for the input solve year -weather_year = 2007 # must be 2007-2013 or 2016-2023 -timesteps = 61320 -user_descriptors = "your_user_descriptors_json_location_here" # Optional - if not passed uses default values - -# returns a parameterized PRAS system -pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, user_descriptors = user_descriptors) -``` - -Importantly, the timesteps count from the first hour of the first `weather_year`, so the user must input `2007` as the `weather_year` to run all 61320 hourly timesteps. - -## Testing - -When CI runners aren't available and ReEDS2PRAS isn't run automatically, it is good practice to run tests and ensure all of them pass before a PR is merged. -It is always good practice to include new tests if the current tests don't cover the functionality you've developed. -You can run ReEDS2PRAS tests by running: - -```shell -cd ReEDS-2.0/reeds2pras/test -julia --project runtests.jl -``` - -or - -```shell -cd ReEDS-2.0/reeds2pras/test -julia --project -``` - -```julia -include("runtests.jl") -``` - -The tests include PRAS SystemModel building tests and PRAS and ReEDS2PRAS benchmark tests. -If your changes increase the benchmark time by a significant amount, that might be worth investigating. - -Typical successful tests summary would look something like this: - -![Test Summary](R2P_Test_Summary.png) - -## Contributing Guidelines - -It is always a good practice to follow the Julia Style Guide (`https://docs.julialang.org/en/v1/manual/style-guide/`). -Please make sure you format your code to follow our guidelines using the snippet below before you open a PR: - -```shell -julia -e 'using Pkg; Pkg.add("JuliaFormatter"); using JuliaFormatter; include(".github/workflows/formatter-code.jl")' -``` - -**NOTE: You have to run the snippet above at the repo folder level. diff --git a/reeds2pras/src/ReEDS2PRAS.jl b/reeds2pras/src/ReEDS2PRAS.jl deleted file mode 100644 index 67a923a8..00000000 --- a/reeds2pras/src/ReEDS2PRAS.jl +++ /dev/null @@ -1,40 +0,0 @@ -module ReEDS2PRAS - -# Exports -export reeds_to_pras - -# Imports -import CSV -import DataFrames -import Dates -import HDF5 -import PRAS -import Statistics -import TimeZones -import InlineStrings -import JSON - -# Includes -# Models -include("models/Region.jl") -include("models/Storage.jl") -include("models/Battery.jl") -include("models/Gen_Storage.jl") -include("models/Generator.jl") -include("models/Thermal_Gen.jl") -include("models/Variable_Gen.jl") -include("models/Line.jl") -include("models/utils.jl") - -# Utils -include("utils/reeds_input_parsing.jl") -include("utils/runchecks.jl") -include("utils/reeds_data_parsing.jl") -#Main -include("main/parse_reeds_data.jl") -include("main/create_pras_system.jl") - -# Module -include("reeds_to_pras.jl") - -end diff --git a/reeds2pras/src/main/create_pras_system.jl b/reeds2pras/src/main/create_pras_system.jl deleted file mode 100644 index 386fe0a0..00000000 --- a/reeds2pras/src/main/create_pras_system.jl +++ /dev/null @@ -1,206 +0,0 @@ -""" - This function creates a PRAS (Probabilistic Resource Adequacy System) model - from a set of regions, lines, generators, storages, and generator-storages. - It takes in a vector of Region objects, a vector of Line objects, a vector - of Generator objects, a vector of Storage objects, a vector of Gen_Storage - objects, an integer timesteps representing the number of timesteps, and an - integer weather_year representing the year of the simulation. It then - creates a StepRange object for the timestamps, creates - PRAS lines and interfaces from the sorted lines and interface indices, - creates PRAS regions from the regions, creates PRAS generators from the - sorted generators and generator indices, creates PRAS storages from the - storages and storage indices, creates PRAS generator-storages from the - sorted generator-storages and generator-storage indices, and finally - returns a PRAS system model object. - - Parameters - ---------- - regions : Vector{Region} - Vector of Region objects. - lines : Vector{Line} - Vector of Line objects. - gens : Vector{<:Generator} - Vector of Generator objects. - storages : Vector{<:Storage} - Vector of Storage objects. - gen_stors : Vector{<:Gen_Storage} - Vector of Gen_Storage objects. - timesteps : Int - Number of timesteps. - weather_year : Int - Year of the simulation. - - Returns - ------- - PRAS.SystemModel - PRAS system model object. -""" -function create_pras_system( - regions::Vector{Region}, - lines::Vector{Line}, - gens::Vector{<:Generator}, - storages::Vector{<:Storage}, - gen_stors::Vector{<:Gen_Storage}, - timesteps::Int, - weather_year::Int, -) - first_ts = TimeZones.ZonedDateTime(weather_year, 01, 01, 00, TimeZones.tz"UTC") - last_ts = first_ts + Dates.Hour(timesteps - 1) - my_timestamps = StepRange(first_ts, Dates.Hour(1), last_ts) - - out = get_sorted_lines(lines, regions) - sorted_lines, interface_reg_idxs, interface_line_idxs = out - pras_lines, pras_interfaces = - make_pras_interfaces(sorted_lines, interface_reg_idxs, interface_line_idxs, regions) - pras_regions = PRAS.Regions{timesteps, PRAS.MW}( - get_name.(regions), - reduce(vcat, get_load.(regions)), - ) - ## - sorted_gens, gen_idxs = get_sorted_components(gens, get_name.(regions)) - capacity_matrix = reduce(vcat, get_capacity.(sorted_gens)) - λ_matrix = reduce(vcat, get_λ.(sorted_gens)) - μ_matrix = reduce(vcat, get_μ.(sorted_gens)) - pras_gens = PRAS.Generators{timesteps, 1, PRAS.Hour, PRAS.MW}( - get_name.(sorted_gens), - get_type.(sorted_gens), - capacity_matrix, - λ_matrix, - μ_matrix, - ) - ## - storages, stor_idxs = get_sorted_components(storages, regions) - - stor_names = isempty(get_name.(storages)) ? String[] : get_name.(storages) - stor_types = isempty(get_type.(storages)) ? String[] : get_type.(storages) - stor_charge_cap_array = reduce( - vcat, - get_charge_capacity.(storages), - init = Matrix{Int64}(undef, 0, timesteps), - ) - stor_discharge_cap_array = reduce( - vcat, - get_discharge_capacity.(storages), - init = Matrix{Int64}(undef, 0, timesteps), - ) - stor_energy_cap_array = reduce( - vcat, - get_energy_capacity.(storages), - init = Matrix{Int64}(undef, 0, timesteps), - ) - stor_chrg_eff_array = reduce( - vcat, - get_charge_efficiency.(storages), - init = Matrix{Float64}(undef, 0, timesteps), - ) - stor_dischrg_eff_array = reduce( - vcat, - get_discharge_efficiency.(storages), - init = Matrix{Float64}(undef, 0, timesteps), - ) - stor_cryovr_eff = reduce( - vcat, - get_carryover_efficiency.(storages), - init = Matrix{Float64}(undef, 0, timesteps), - ) - λ_stor = reduce(vcat, get_λ.(storages), init = Matrix{Float64}(undef, 0, timesteps)) - μ_stor = reduce(vcat, get_μ.(storages), init = Matrix{Float64}(undef, 0, timesteps)) - pras_storages = PRAS.Storages{timesteps, 1, PRAS.Hour, PRAS.MW, PRAS.MWh}( - stor_names, - stor_types, - stor_charge_cap_array, - stor_discharge_cap_array, - stor_energy_cap_array, - stor_chrg_eff_array, - stor_dischrg_eff_array, - stor_cryovr_eff, - λ_stor, - μ_stor, - ) - ## - sorted_gen_stors, genstor_idxs = get_sorted_components(gen_stors, regions) - - gen_stor_names = - isempty(get_name.(sorted_gen_stors)) ? String[] : get_name.(sorted_gen_stors) - gen_stor_cats = - isempty(get_type.(sorted_gen_stors)) ? String[] : get_type.(sorted_gen_stors) - gen_stor_cap_array = reduce( - vcat, - get_charge_capacity.(sorted_gen_stors), - init = Matrix{Int64}(undef, 0, timesteps), - ) - gen_stor_dis_cap_array = reduce( - vcat, - get_discharge_capacity.(sorted_gen_stors), - init = Matrix{Int64}(undef, 0, timesteps), - ) - gen_stor_enrgy_cap_array = reduce( - vcat, - get_energy_capacity.(sorted_gen_stors), - init = Matrix{Int64}(undef, 0, timesteps), - ) - gen_stor_chrg_eff_array = reduce( - vcat, - get_charge_efficiency.(sorted_gen_stors), - init = Matrix{Float64}(undef, 0, timesteps), - ) - gen_stor_dischrg_eff_array = reduce( - vcat, - get_discharge_efficiency.(sorted_gen_stors), - init = Matrix{Float64}(undef, 0, timesteps), - ) - gen_stor_carryovr_eff_array = reduce( - vcat, - get_carryover_efficiency.(sorted_gen_stors), - init = Matrix{Float64}(undef, 0, timesteps), - ) - gen_stor_inflow_array = reduce( - vcat, - get_inflow.(sorted_gen_stors), - init = Matrix{Int64}(undef, 0, timesteps), - ) - gen_stor_grid_withdrawl_array = reduce( - vcat, - get_grid_withdrawl_capacity.(sorted_gen_stors), - init = Matrix{Int64}(undef, 0, timesteps), - ) - gen_stor_grid_inj_array = reduce( - vcat, - get_grid_injection_capacity.(sorted_gen_stors), - init = Matrix{Int64}(undef, 0, timesteps), - ) - gen_stor_λ = - reduce(vcat, get_λ.(sorted_gen_stors), init = Matrix{Float64}(undef, 0, timesteps)) - gen_stor_μ = - reduce(vcat, get_μ.(sorted_gen_stors), init = Matrix{Float64}(undef, 0, timesteps)) - - gen_stors = PRAS.GeneratorStorages{timesteps, 1, PRAS.Hour, PRAS.MW, PRAS.MWh}( - gen_stor_names, - gen_stor_cats, - gen_stor_cap_array, - gen_stor_dis_cap_array, - gen_stor_enrgy_cap_array, - gen_stor_chrg_eff_array, - gen_stor_dischrg_eff_array, - gen_stor_carryovr_eff_array, - gen_stor_inflow_array, - gen_stor_grid_withdrawl_array, - gen_stor_grid_inj_array, - gen_stor_λ, - gen_stor_μ, - ) - - return PRAS.SystemModel( - pras_regions, - pras_interfaces, - pras_gens, - gen_idxs, - pras_storages, - stor_idxs, - gen_stors, - genstor_idxs, - pras_lines, - interface_line_idxs, - my_timestamps, - ) -end diff --git a/reeds2pras/src/main/parse_reeds_data.jl b/reeds2pras/src/main/parse_reeds_data.jl deleted file mode 100644 index 42daeb6e..00000000 --- a/reeds2pras/src/main/parse_reeds_data.jl +++ /dev/null @@ -1,133 +0,0 @@ -""" - This function creates PRAS objects based on data from the ReEDS - capacity expansion model. It processes regional load profiles, - interregional transmission lines, thermal generators, - storages, and GeneratorStorages into arrays suitable for creating - a PRAS system. - - Parameters - ---------- - ReEDS_data : ReEDSdatapaths - data paths with specific ReEDS file paths - timesteps : Int - Number of timesteps - solve_year : Int - ReEDS solve year - scheduled_outage : Bool, - Flag to read the scheduled_outage_hourly file (if available) - Returns - ------- - lines : Array{Line} - contains Line objects - regions : Array{Region} - contains Region objects - gens_array : Array{VegGen} - contains VG Gen objects - storage_array : Array{Storage} - contains Storage objects - genstor_array : Array{GeneratorStorage} - contains GeneratorStorage objects -""" -function parse_reeds_data( - ReEDS_data::ReEDSdatapaths, - timesteps::Int, - solve_year::Int; - hydro_energylim = false, - scheduled_outage = false, - pras_agg_ogs_lfillgas = false, - pras_existing_unit_size = true, - pras_max_unitsize_prm = true, -) - @info "Processing regions and associating load profiles..." - region_array = process_regions_and_load(ReEDS_data) - - @info "Processing lines and adding VSC-related regions, if applicable..." - lines = process_lines(ReEDS_data, get_name.(region_array), timesteps) - lines, regions = process_vsc_lines(lines, region_array) - - # Create Generator Objects - # **TODO: Should 0 MW generators be allowed after disaggregation? - # **TODO: is it important to also handle planned outages? - @info( - "splitting thermal, storage, variable and hydro generator types from installed " * - "ReEDS capacities..." - ) - thermal_builds, storage, hydro_disp_gens, hydro_non_disp_gens = - split_generator_types(ReEDS_data) - - @info "reading in ReEDS generator-type forced outage data..." - forced_outage_data = get_forced_outage_data(ReEDS_data) - FOR_dict = Dict(forced_outage_data[!, "ResourceType"] .=> forced_outage_data[!, "FOR"]) - - @info "reading hourly forced outage rates" - forcedoutage_hourly = get_hourly_forced_outage_data(ReEDS_data) - - @info "reading in ATB unit size data for use with disaggregation..." - unitsize_data = get_unitsize_mapping(ReEDS_data) - unitsize_dict = Dict(unitsize_data[!, "tech"] .=> unitsize_data[!, "MW"]) - - scheduled_outage_hourly = nothing - # read the scheduled_outage CSV file if scheduled_outage - if scheduled_outage - @info "reading hourly scheduled outage rates..." - scheduled_outage_hourly = get_hourly_scheduled_outage_data(ReEDS_data) - end - - @info "reading MTTR data" - mttr_dict = get_MTTR_data(ReEDS_data) - - @info "Processing conventional/thermal generators..." - thermal_gens = process_thermals_with_disaggregation( - ReEDS_data, - thermal_builds, - FOR_dict, - forcedoutage_hourly, - unitsize_dict, - timesteps, - solve_year, - mttr_dict, - scheduled_outage_hourly = scheduled_outage_hourly, - pras_agg_ogs_lfillgas = pras_agg_ogs_lfillgas, - pras_existing_unit_size = pras_existing_unit_size, - pras_max_unitsize_prm = pras_max_unitsize_prm, - ) - @info "Processing variable generation..." - gens_array = process_vg( - thermal_gens, - ReEDS_data, - ) - - @info "Processing Storages..." - @debug "storage is $(storage)" - storage_array = process_storages( - storage, - FOR_dict, - forcedoutage_hourly, - unitsize_dict, - ReEDS_data, - timesteps, - mttr_dict, - scheduled_outage_hourly = scheduled_outage_hourly, - ) - - @info "Processing hydroelectric generators..." - gens_array, genstor_array = process_hydro( - gens_array, - hydro_disp_gens, - hydro_non_disp_gens, - FOR_dict, - forcedoutage_hourly, - ReEDS_data, - solve_year, - timesteps, - mttr_dict, - unitsize_dict, - scheduled_outage_hourly = scheduled_outage_hourly, - hydro_energylim = hydro_energylim, - ) - - #@info "Processing GeneratorStorages" - #genstor_array = process_genstors(genstor_array, get_name.(regions), timesteps) - - return lines, regions, gens_array, storage_array, genstor_array -end diff --git a/reeds2pras/src/models/Battery.jl b/reeds2pras/src/models/Battery.jl deleted file mode 100644 index 0bb86a3e..00000000 --- a/reeds2pras/src/models/Battery.jl +++ /dev/null @@ -1,130 +0,0 @@ -""" - This code defines a struct called Battery which is a subtype of - Storage. The struct has 13 fields: name, timesteps, region_name, type, - charge_cap, discharge_cap, energy_cap, legacy, charge_eff, - discharge_eff, carryover_eff, FOR and MTTR. The code also includes an - inner constructor and checks to ensure that the values passed are - valid. The constructor checks that charge_cap and discharge_cap are greater than 0.0, - energy_cap is greater than 0.0, legacy is either "Existing" or "New", all of the - efficiency values are between 0.0 and 1.0 (inclusive), FOR is between 0.0 - and 1.0 (inclusive) and MTTR is greater than 0. If any of these checks fail - an error will be thrown. - - Parameters - ---------- - name : String - The name of the battery - timesteps : Int64 - Number of PRAS timesteps - region_name: String - Name of the region - type : String - Type of battery - charge_cap : Float64 - Charge capacity - discharge_cap : Float64 - Discharge capacity - energy_cap : Float64 - Energy capacity - legacy : String - Battery's legacy (existing or new) - charge_eff : Float64 - Charge efficiency - discharge_eff : Float64 - Discharge efficiency - carryover_eff : Float64 - Carryover efficiency - FOR : Float64 - Factor of restoration - SOR : Vector{Float32} - Scheduled Outage Rate - MTTR : Int64 - Mean time to restore - - Returns - ------- - Struct with properties related to the batter -""" -struct Battery <: Storage - name::String - timesteps::Int64 - region_name::String - type::String - charge_cap::Float64 - discharge_cap::Float64 - energy_cap::Vector{Float64} - legacy::String - charge_eff::Float64 - discharge_eff::Float64 - carryover_eff::Float64 - FOR::Vector{Float32} - SOR::Vector{Float32} - MTTR::Int64 - - # Inner Constructors & Checks - function Battery(; - name = "init_name", - timesteps = 8760, - region_name = "init_name", - type = "init_type", - charge_cap = 1.0, - discharge_cap = 1.0, - energy_cap = fill(4.0, timesteps), - legacy = "New", - charge_eff = 1.0, - discharge_eff = 1.0, - carryover_eff = 1.0, - FOR = zeros(Float32, timesteps), - SOR = zeros(Float32, timesteps), - MTTR = 24, - ) - @debug "cap_P = $(discharge_cap) MW and cap_E = $(energy_cap) MWh" - - charge_cap > 0.0 || error( - "Charge capacity passed is not allowed (should be > 0.0) : $(name) - $(charge_cap) MW", - ) - - discharge_cap > 0.0 || error( - "Discharge capacity passed is not allowed (should be > 0.0) : $(name) - $(discharge_cap) MW", - ) - - all(energy_cap .> 0.0) || error( - "Energy capacity passed is not allowed (should be > 0.0) : $(name) - $(energy_cap) MWh", - ) - - legacy in ["Existing", "New"] || - error("$(name) has legacy $(legacy) which is not in [Existing, New]") - - all(0.0 .<= [charge_eff, discharge_eff, carryover_eff] .<= 1.0) || - error("$(name) charge/discharge/carryover efficiency value is < 0.0 or > 1.0") - - all(0.0 .<= FOR .<= 1.0) || error("$(name) FOR value is < 0 or > 1") - - MTTR > 0 || error("$(name) MTTR value is <= 0") - - return new( - name, - timesteps, - region_name, - type, - charge_cap, - discharge_cap, - energy_cap, - legacy, - charge_eff, - discharge_eff, - carryover_eff, - FOR, - SOR, - MTTR, - ) - end -end - -# Getter Functions - -get_charge_capacity(stor::Battery) = fill(round(Int, stor.charge_cap), 1, stor.timesteps) - -get_discharge_capacity(stor::Battery) = - fill(round(Int, stor.discharge_cap), 1, stor.timesteps) - diff --git a/reeds2pras/src/models/Gen_Storage.jl b/reeds2pras/src/models/Gen_Storage.jl deleted file mode 100644 index 58144dde..00000000 --- a/reeds2pras/src/models/Gen_Storage.jl +++ /dev/null @@ -1,166 +0,0 @@ -""" - This code defines a struct called Gen_Storage which is a subtype of - Storage. The struct has 14 fields: name, timesteps, region_name, type, - charge_cap, discharge_cap, energy_cap, inflow, grid_withdrawl_cap, - grid_inj_cap, legacy, charge_eff, discharge_eff and carryover_eff. The - code also contains an inner constructor and checks to ensure that the - values passed are valid. Specifically, all capacity values must be - greater than or equal to 0.0,the legacy value must either be “Existing” - or “New”, all of the efficiency values must be between 0.0 and 1.0 (inclusive), - FOR must be between 0.0 and 1.0 (inclusive) and MTTR must be greater than 0. - Additionally, there is a commented out check that verifies that all of - the time series data is of the same size. Finally, if any of these - checks fail, an error will be thrown. - - Parameters - ---------- - name : string - Name of Gen_Storage - timesteps : integer - PRAS timesteps (timesteps) - region_name : string - Region name - type : string - Storage type - charge_cap : float - Charge capacity - discharge_cap : float - Discharge capacity - energy_cap : float - Energy capacity - inflow : float - Inflow time series data - grid_withdrawl_cap : float - Grid withdrawal capacity time series data - grid_inj_cap : floating point - Grid injection capacity time series data - legacy : string - Must be either "Existing" or "New" - charge_eff : float - Charge efficiency - discharge_eff : float - Discharge efficiency - carryover_eff : float - Carryover efficiency - FOR : float - Forced Outage Rate value - SOR : Vector{Float32} - Scheduled Outage Rate - MTTR : integer - Mean Time To Repair value - - Returns - ------- - A new instance of Gen_Storage. -""" -struct Gen_Storage <: Storage - name::String - timesteps::Int64 - region_name::String - type::String - charge_cap::Vector{Float64} - discharge_cap::Vector{Float64} - energy_cap::Vector{Float64} - inflow::Vector{Float64} - grid_withdrawl_cap::Vector{Float64} - grid_inj_cap::Vector{Float64} - legacy::String - charge_eff::Float64 - discharge_eff::Float64 - carryover_eff::Float64 - FOR::Vector{Float32} - SOR::Vector{Float32} - MTTR::Int64 - - # Inner Constructors & Checks - function Gen_Storage(; - name = "init_name", - timesteps = 8760, - region_name = "init_name", - type = "init_type", - charge_cap = zeros(Float64, timesteps), - discharge_cap = zeros(Float64, timesteps), - energy_cap = zeros(Float64, timesteps), - inflow = zeros(Float64, timesteps), - grid_withdrawl_cap = zeros(Float64, timesteps), - grid_inj_cap = zeros(Float64, timesteps), - legacy = "New", - charge_eff = 1.0, - discharge_eff = 1.0, - carryover_eff = 1.0, - FOR = zeros(Float32, timesteps), - SOR = zeros(Float32, timesteps), - MTTR = 24, - ) - all(charge_cap .>= 0.0) || error( - "Charge capacity passed is not allowed (should be >= 0.0) : $(name) - $(charge_cap) MW", - ) - - all(discharge_cap .>= 0.0) || error( - "Discharge capacity passed is not allowed (should be >= 0.0) : $(name) - $(discharge_cap) MW", - ) - - all(energy_cap .>= 0.0) || error( - "Energy capacity passed is not allowed (should be >= 0.0) : $(name) - $(energy_cap) MWh", - ) - - all(inflow .>= 0.0) || error( - "Inflow passed is not allowed (should be >= 0.0) : $(name) - $(inflow) MW", - ) - - all(grid_withdrawl_cap .>= 0.0) || error( - "Grid withdrawl capacity passed is not allowed (should be >= 0.0) : $(name) - $(grid_withdrawl_cap) MW", - ) - - all(grid_inj_cap .>= 0.0) || error( - "Grid injection capacity passed is not allowed (should be >= 0.0) : $(name) - $(grid_inj_cap) MW", - ) - - legacy in ["Existing", "New"] || - error("$(name) has legacy $(legacy) which is not in [Existing, New]") - - all(0.0 .<= [charge_eff, discharge_eff, carryover_eff] .<= 1.0) || - error("$(name) charge/discharge/carryover efficiency value is < 0.0 or > 1.0") - - all(0.0 .<= FOR .<= 1.0) || error("$(name) FOR value is < 0 or > 1") - - MTTR > 0 || error("$(name) MTTR value is <= 0") - - return new( - name, - timesteps, - region_name, - type, - charge_cap, - discharge_cap, - energy_cap, - inflow, - grid_withdrawl_cap, - grid_inj_cap, - legacy, - charge_eff, - discharge_eff, - carryover_eff, - FOR, - SOR, - MTTR, - ) - end -end - -# Getter Functions - -#TODO: is SOR for grid injection enough? Do we need to derate energy capacity too? - -get_charge_capacity(stor::Gen_Storage) = permutedims(round.(Int, stor.charge_cap)) - -get_discharge_capacity(stor::Gen_Storage) = permutedims(round.(Int, stor.discharge_cap)) - -get_inflow(stor::Gen_Storage) = permutedims(round.(Int, stor.inflow)) - -get_grid_withdrawl_capacity(stor::Gen_Storage) = - permutedims(round.(Int, stor.grid_withdrawl_cap)) - - -get_grid_injection_capacity(stor::Gen_Storage) = - permutedims(round.(Int, stor.grid_inj_cap .* (1 .-stor.SOR))) \ No newline at end of file diff --git a/reeds2pras/src/models/Generator.jl b/reeds2pras/src/models/Generator.jl deleted file mode 100644 index be3a2d75..00000000 --- a/reeds2pras/src/models/Generator.jl +++ /dev/null @@ -1,46 +0,0 @@ -abstract type Generator end - -# Getter Functions -get_name(gen::Generator) = gen.name - -get_legacy(gen::Generator) = gen.legacy - -# Helper Functions -get_outage_rate(gen::Generator) = outage_to_rate(gen.FOR, gen.MTTR) - -function get_λ(gen::Generator) - λ = getfield(get_outage_rate(gen), :λ) - if (isa(λ, Float64)) - out = fill(λ, 1, gen.timesteps) - else - out = reshape(λ, 1, :) - end - return out -end - -get_μ(gen::Generator) = fill(getfield(get_outage_rate(gen), :μ), 1, gen.timesteps) - -function get_generators_in_region(gens::Vector{<:Generator}, reg_name::String) - reg_gens = filter(gen -> gen.region_name == reg_name, gens) - if isempty(reg_gens) - @warn "No generators in region: $(reg_name)" - return Generator[] - else - return reg_gens - end -end - -get_generators_in_region(gens::Vector{<:Generator}, reg::Region) = - get_generators_in_region(gens, reg.name) - -function get_legacy_generators(gens::Vector{<:Generator}, leg::String) - leg in ["Existing", "New"] || error("Unidentified legacy passed") - - leg_gens = filter(gen -> gen.legacy == leg, gens) - if isempty(leg_gens) - @warn "No generators with legacy: $(leg)" - return Generator[] - else - return leg_gens - end -end diff --git a/reeds2pras/src/models/Interface.jl b/reeds2pras/src/models/Interface.jl deleted file mode 100644 index c73a7edb..00000000 --- a/reeds2pras/src/models/Interface.jl +++ /dev/null @@ -1,150 +0,0 @@ -""" - Constructs a model of a transmission interface (a group of lines). - - Parameters - ---------- - name : String - Name of interface object - timesteps : Int64 - Number of timeseries for the same identifier (assumed same for all - type of objects) - regions_from : String - Origin region of the line object - region_to : String - Destination region of the line object - forward_cap : Float64 - Capacity of the line object in forward direction - backward_cap : Float64 - Capacity of the line object in backward direction - legacy : String - Check whether it is existing or new i.e., RET or TEST - FOR : Float64 - Forced Outage Rate of the line object - MTTR : Int64 - Mean Time to Repair of the line object - VSC : Bool - Voltage Source Converter of the line object - converter_capacity : Dict{String, Float64} - Dictionary that stores the VSC capacities within Region_From and - Region_To - - Returns - ------- - Out: Line - A new instance of Line object as defined above - -""" -struct Line - name::String - timesteps::Int64 - category::String - region_from::String - region_to::String - forward_cap::Float64 - backward_cap::Float64 - legacy::String - FOR::Float64 - MTTR::Int64 - VSC::Bool - converter_capacity::Dict{String, Float64} - - # Inner Constructors & Checks - function Line(; - name = "init_name", - timesteps = 8760, - category = "AC", - region_from = "init_reg_from", - region_to = "init_reg_to", - forward_cap = 0.0, - backward_cap = 0.0, - legacy = "New", - FOR = 0.0, - MTTR = 24, - VSC = false, - converter_capacity = Dict(region_from => 0.0, region_to => 0.0), - ) - category in ReEDS_LINE_TYPES || - error("$(name) has category $(category) which is not in $(ReEDS_LINE_TYPES)") - - ~(region_from == region_to) || - error("Region_From and Region_To cannot be the same for $(name). PRAS only - considers inter-regional lines in zonal analysis") - - all([forward_cap, backward_cap] .>= 0.0) || - error("$(name) forward/backward capacity value < 0") - - legacy in ["Existing", "New"] || - error("$(name) has legacy $(legacy) which is not in [Existing, New]") - - 0.0 <= FOR <= 1.0 || error("$(name) FOR value is < 0 or > 1") - - MTTR > 0 || error("$(name) MTTR value is <= 0") - - all([region_from, region_to] .∈ Ref(keys(converter_capacity))) || - error("Check the keys of converter capacity dictionary for VSC DC - line: $(name)") - - return new( - name, - timesteps, - category, - region_from, - region_to, - forward_cap, - backward_cap, - legacy, - FOR, - MTTR, - VSC, - converter_capacity, - ) - end -end - -# Getter Functions - -get_name(ln::Line) = ln.name - -get_category(ln::Line) = ln.category - -get_forward_capacity(ln::Line) = fill(round(Int, ln.forward_cap), 1, ln.timesteps) - -get_backward_capacity(ln::Line) = fill(round(Int, ln.backward_cap), 1, ln.timesteps) - -get_region_from(ln::Line) = ln.region_from - -get_region_to(ln::Line) = ln.region_to - -#Helper functions - -get_outage_rate(ln::Line) = outage_to_rate(ln.FOR, ln.MTTR) - -get_λ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :λ), 1, ln.timesteps) - -get_μ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :μ), 1, ln.timesteps) - -""" - Return all lines with the specified legacy. - - Parameters - ---------- - lines : Vector{<:Line} - List of Lines to filter through. - leg : String - Legacy of Line, either 'Existing' or 'New'. - - Returns - ------- - Vector{<:Line} - List of all lines with the specified legacy. -""" -function get_legacy_lines(lines::Vector{Line}, leg::String) - leg in ["Existing", "New"] || error("Unidentified legacy passed") - - leg_lines = filter(ln -> ln.legacy == leg, lines) - if isempty(leg_lines) - # @warn "No lines with legacy: $(leg)" - else - return leg_lines - end -end diff --git a/reeds2pras/src/models/Line.jl b/reeds2pras/src/models/Line.jl deleted file mode 100644 index 37d97653..00000000 --- a/reeds2pras/src/models/Line.jl +++ /dev/null @@ -1,155 +0,0 @@ -const ReEDS_LINE_TYPES = ["AC", "B2B", "LCC", "VSC", "VSC DC-AC converter"] - -""" - Constructs a model of LINE. - - Parameters - ---------- - name : String - Name chose for a line object - timesteps : Int64 - Number of timeseries for the same identifier (assumed same for all - type of objects) - category : String - Type of Line object. Category can be one of AC/B2B/LCC/VSC/VSC - DC-AC converter - region_from : String - Origin region of the line object - region_to : String - Destination region of the line object - forward_cap : Float64 - Capacity of the line object in forward direction - backward_cap : Float64 - Capacity of the line object in backward direction - legacy : String - Check whether it is existing or new i.e., RET or TEST - FOR : Float64 - Forced Outage Rate of the line object - MTTR : Int64 - Mean Time to Repair of the line object - VSC : Bool - Voltage Source Converter of the line object - converter_capacity : Dict{String, Float64} - Dictionary that stores the VSC capacities within Region_From and - Region_To - - Returns - ------- - Out: Line - A new instance of Line object as defined above - -""" -struct Line - name::String - timesteps::Int64 - category::String - region_from::String - region_to::String - forward_cap::Float64 - backward_cap::Float64 - legacy::String - FOR::Float64 - MTTR::Int64 - VSC::Bool - converter_capacity::Dict{String, Float64} - - # Inner Constructors & Checks - function Line(; - name = "init_name", - timesteps = 8760, - category = "AC", - region_from = "init_reg_from", - region_to = "init_reg_to", - forward_cap = 0.0, - backward_cap = 0.0, - legacy = "New", - FOR = 0.0, - MTTR = 24, - VSC = false, - converter_capacity = Dict(region_from => 0.0, region_to => 0.0), - ) - category in ReEDS_LINE_TYPES || - error("$(name) has category $(category) which is not in $(ReEDS_LINE_TYPES)") - - ~(region_from == region_to) || - error("Region_From and Region_To cannot be the same for $(name). PRAS only - considers inter-regional lines in zonal analysis") - - all([forward_cap, backward_cap] .>= 0.0) || - error("$(name) forward/backward capacity value < 0") - - legacy in ["Existing", "New"] || - error("$(name) has legacy $(legacy) which is not in [Existing, New]") - - 0.0 <= FOR <= 1.0 || error("$(name) FOR value is < 0 or > 1") - - MTTR > 0 || error("$(name) MTTR value is <= 0") - - all([region_from, region_to] .∈ Ref(keys(converter_capacity))) || - error("Check the keys of converter capacity dictionary for VSC DC - line: $(name)") - - return new( - name, - timesteps, - category, - region_from, - region_to, - forward_cap, - backward_cap, - legacy, - FOR, - MTTR, - VSC, - converter_capacity, - ) - end -end - -# Getter Functions - -get_name(ln::Line) = ln.name - -get_category(ln::Line) = ln.category - -get_forward_capacity(ln::Line) = fill(round(Int, ln.forward_cap), 1, ln.timesteps) - -get_backward_capacity(ln::Line) = fill(round(Int, ln.backward_cap), 1, ln.timesteps) - -get_region_from(ln::Line) = ln.region_from - -get_region_to(ln::Line) = ln.region_to - -#Helper functions - -get_outage_rate(ln::Line) = outage_to_rate(ln.FOR, ln.MTTR) - -get_λ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :λ), 1, ln.timesteps) - -get_μ(ln::Line) = fill(getfield(outage_to_rate(ln.FOR, ln.MTTR), :μ), 1, ln.timesteps) - -""" - Return all lines with the specified legacy. - - Parameters - ---------- - lines : Vector{<:Line} - List of Lines to filter through. - leg : String - Legacy of Line, either 'Existing' or 'New'. - - Returns - ------- - Vector{<:Line} - List of all lines with the specified legacy. -""" -function get_legacy_lines(lines::Vector{Line}, leg::String) - leg in ["Existing", "New"] || error("Unidentified legacy passed") - - leg_lines = filter(ln -> ln.legacy == leg, lines) - if isempty(leg_lines) - # @warn "No lines with legacy: $(leg)" - else - return leg_lines - end -end diff --git a/reeds2pras/src/models/Region.jl b/reeds2pras/src/models/Region.jl deleted file mode 100644 index 883d092f..00000000 --- a/reeds2pras/src/models/Region.jl +++ /dev/null @@ -1,45 +0,0 @@ -""" - Constructs the ReEDS2PRAS Region type. Region objects have three main - attributes - name (String), timesteps (Int64) and load - (Vector{Float64}). The load attribute represents the region's total - power demand data over N intervals of measure given in MW, which must - always be greater than 0. - - Parameters - ---------- - name : String - Name to give the region. - timesteps : Int64 - Number of PRAS timesteps. - load : Vector{Float64} - Time series data for the region's total power demand must match N - in length. - - Returns - ------- - A new instance of the PRAS Region type. -""" -struct Region - name::String - timesteps::Int64 - load::Vector{Float64} - - # Inner Constructors & Checks - function Region(name, timesteps, load = zeros(Float64, timesteps)) - length(load) == timesteps || error( - "The length of the region $(name) load time series data is $(length(load)) but it should be - equal to PRAS timesteps ($(timesteps))", - ) - - all(load .>= 0.0) || - error("Check for negative values in region $(name) load time series data.") - - return new(name, timesteps, load) - end -end - -# Getter Functions - -get_name(reg::Region) = reg.name - -get_load(reg::Region) = permutedims(round.(Int, reg.load)) diff --git a/reeds2pras/src/models/Storage.jl b/reeds2pras/src/models/Storage.jl deleted file mode 100644 index cdbf7ced..00000000 --- a/reeds2pras/src/models/Storage.jl +++ /dev/null @@ -1,96 +0,0 @@ -abstract type Storage end - -# Getter Functions - -get_name(stor::Storage) = stor.name - -get_type(stor::Storage) = stor.type - -get_legacy(stor::Storage) = stor.legacy - -get_energy_capacity(stor::Storage) = permutedims(round.(Int, stor.energy_cap .* (1 .-stor.SOR))) - -get_charge_efficiency(stor::Storage) = fill(stor.charge_eff, 1, stor.timesteps) - -get_discharge_efficiency(stor::Storage) = fill(stor.discharge_eff, 1, stor.timesteps) - -get_carryover_efficiency(stor::Storage) = fill(stor.carryover_eff, 1, stor.timesteps) - -# Helper Functions -get_outage_rate(stor::Storage) = outage_to_rate(stor.FOR, stor.MTTR) - -function get_λ(stor::Storage) - λ = getfield(get_outage_rate(stor), :λ) - if (isa(λ, Float64)) - out = fill(λ, 1, stor.timesteps) - else - out = reshape(λ, 1, :) - end - return out -end - -get_μ(stor::Storage) = fill(getfield(get_outage_rate(stor), :μ), 1, stor.timesteps) - -get_category(stor::Storage) = "$(stor.legacy)|$(stor.type)" - -""" - This function searches an array stors of type Vector{<:Storage} for - storages located in a specific region reg_name. First, it filters the array - for storages with a region_name field equal to the region name given. If no - such storages exist, a warning is issued and an empty array of type - Storage[] is returned. Otherwise, an array containing all the storages from - this region is returned. - - Parameters - ---------- - stors : Vector{<:Storage} - An array of instances of type Storage. - reg_name : String - The name of the region to search for in storages. - - Returns - ------- - reg_stors : Vector{<:Storage} - An array of Storage instances found in the specified region reg_name. -""" -function get_storages_in_region(stors::Vector{<:Storage}, reg_name::String) - reg_stors = filter(stor -> stor.region_name == reg_name, stors) - if isempty(reg_stors) - @debug "No storages in region: $(reg_name)" - return Storage[] - else - return reg_stors - end -end - -get_storages_in_region(stors::Vector{<:Storage}, reg::Region) = - get_storages_in_region(stors, reg.name) - -""" - Get the storage objects which match a given legacy ('Existing' or 'New'). - - Parameters - ---------- - stors: Vector{<:Storage} - The array of storage objects. - leg: str - Legacy of the storage objects. Accepted values are 'Existing' and - 'New'. - - Returns - ------- - leg_stors: <:Storage - A subset of ``stors`` that has matching legacy. - Returns an empty array if there is no match. -""" -function get_legacy_storages(stors::Vector{<:Storage}, leg::String) - leg in ["Existing", "New"] || error("Unidentified legacy passed") - - leg_stors = filter(stor -> stor.legacy == leg, stors) - if isempty(leg_stors) - @debug "No storages with legacy: $(leg)" - return Storage[] - else - return leg_stors - end -end diff --git a/reeds2pras/src/models/Thermal_Gen.jl b/reeds2pras/src/models/Thermal_Gen.jl deleted file mode 100644 index 574336a0..00000000 --- a/reeds2pras/src/models/Thermal_Gen.jl +++ /dev/null @@ -1,82 +0,0 @@ -""" - This function is used to define a thermal generator in the model. It - contains one struct and an inner constructor to check if the inputs are - valid. - - Parameters - ---------- - name : String - The name of the generator. - timesteps : Int64 - Number of timesteps in the PRAS problem. - region_name : String - Name of the region associated with this generator. - capacity : Float64 - Capacity of the generator. - fuel : String - Fuel type of the generator (default "OT"). - legacy : String - Existing or New generator (default "New"). - FOR : Vector{Float32} - Forced Outage Rate (default 0.0). - SOR : Vector{Float32} - Scheduled Outage Rate (default 0.0). - MTTR : Int64 - Mean Time To Repair/Replace (default 24). - - Returns - ------- - An instance of a Thermal_Gen. -""" -struct Thermal_Gen <: Generator - name::String - timesteps::Int64 - region_name::String - capacity::Float64 - fuel::String - legacy::String - FOR::Vector{Float32} - SOR::Vector{Float32} - MTTR::Int64 - - # Inner Constructors & Checks - function Thermal_Gen(; - name = "init_name", - timesteps = 8760, - region_name = "init_name", - capacity = 10.0, - fuel = "OT", - legacy = "New", - FOR = zeros(Float32, timesteps), - SOR = zeros(Float32, timesteps), - MTTR = 24, - ) - capacity >= 0.0 || error("$(name) capacity value passed is < 0") - - legacy in ["Existing", "New"] || - error("$(name) has legacy $(legacy) which is not in [Existing, New]") - - length(FOR) == timesteps || - error("The length of the $(name) FOR time series data is $(length(FOR)) - but it should be should be equal to PRAS timesteps ($(timesteps))") - - if !isnothing(SOR) - length(SOR) == timesteps || - error("The length of the $(name) SOR time series data is $(length(SOR)) - but it should be should be equal to PRAS timesteps ($(timesteps))") - end - - MTTR > 0 || error("$(name) MTTR value is <= 0") - - return new(name, timesteps, region_name, capacity, fuel, legacy, FOR, SOR, MTTR) - end -end - -# Getter Functions -get_capacity(gen::Thermal_Gen) = round.(Int, gen.capacity * (1 .-gen.SOR)') - -get_fuel(gen::Thermal_Gen) = gen.fuel - -get_category(gen::Thermal_Gen) = "$(gen.legacy)_Thermal|$(gen.fuel)" - -get_type(gen::Thermal_Gen) = gen.fuel diff --git a/reeds2pras/src/models/Variable_Gen.jl b/reeds2pras/src/models/Variable_Gen.jl deleted file mode 100644 index f0f43e4f..00000000 --- a/reeds2pras/src/models/Variable_Gen.jl +++ /dev/null @@ -1,109 +0,0 @@ -""" - This function takes in the attributes of a variable generator (Variable_Gen) - and returns an object containing all its information. The input - parameters are: - - Parameters - ---------- - name : String - Name of the Variable Generator. - timesteps : Int64 - Number of timesteps for the PRAS model. - region_name : String - Name of the Region where Variable Generator's load area is present. - installed_capacity : Float64 - Installed Capacity of the Variable Generator. - capacity : Vector{Float64} - Capacity factor time series data ('forecasted capacity' / - 'nominal capacity') for the PRAS Model. - type : String - Type of Variable Generator being passed. - legacy : String - State of the Variable Generator, i.e., existing or new. - FOR : Float64 - Forced Outage Rate parameter of the Variable Generator. - SOR : Float64 - Scheduled Outage Rate parameter of the Variable Generator. - MTTR : Int64 - Mean Time To Repair parameter of the Variable Generator. - - Returns - ------- - Variable_Gen : Struct - Returns a struct with all the given attributes. -""" -struct Variable_Gen <: Generator - name::String - timesteps::Int64 - region_name::String - installed_capacity::Float64 - capacity::Vector{Float64} - type::String - legacy::String - FOR::Vector{Float32} - SOR::Vector{Float32} - MTTR::Int64 - - # Inner Constructors & Checks - function Variable_Gen(; - name = "init_name", - timesteps = 8760, - region_name = "init_name", - installed_capacity = 10.0, - capacity = zeros(Float64, timesteps), - type = "wind-ons_init_name", - legacy = "New", - FOR = zeros(Float32, timesteps), - SOR = zeros(Float32, timesteps), - MTTR = 24, - ) - all(0.0 .<= capacity .<= installed_capacity) || if ~(startswith(type, "hyd")) - # We do not need to ensure that capacity is < installed capacity - # for hydroelectric plants because we sometimes have - # capacity factors > 1 - error("$(name) time series has values < 0 or > installed capacity - ($(installed_capacity))") - end - - length(capacity) == timesteps || - error("The length of the $(name) capacity time series data is $(length(capacity)) - but it should be should be equal to PRAS timesteps ($(timesteps))") - - legacy in ["Existing", "New"] || - error("$(name) has legacy $(legacy) which is not in [Existing, New]") - - all(0.0 .<= FOR .<= 1.0) || error("$(name) FOR value is < 0 or > 1") - - if !isnothing(SOR) - length(SOR) == timesteps || - error("The length of the $(name) SOR time series data is $(length(SOR)) - but it should be should be equal to PRAS timesteps ($(timesteps))") - end - - MTTR > 0 || error("$(name) MTTR value is <= 0") - - return new( - name, - timesteps, - region_name, - installed_capacity, - capacity, - type, - legacy, - FOR, - SOR, - MTTR, - ) - end -end - -# Getter Functions - -get_capacity(gen::Variable_Gen) = - isnothing(gen.SOR) ? - round.(Int, gen.capacity)' : - round.(Int, gen.capacity .* (1 .-gen.SOR))' - -get_category(gen::Variable_Gen) = "$(gen.legacy)|$(gen.type)" - -get_type(gen::Variable_Gen) = gen.type diff --git a/reeds2pras/src/models/utils.jl b/reeds2pras/src/models/utils.jl deleted file mode 100644 index cbb20a69..00000000 --- a/reeds2pras/src/models/utils.jl +++ /dev/null @@ -1,371 +0,0 @@ -# Converting FOR and MTTR to λ and μ -""" - This function calculates the outage rate of a generator based on the - forced outage rate and its Mean Time To Repair (MTTR). - - Parameters - ---------- - for_gen: - forced outage rate (amount of time unit is out-of-service) - mttr: - Mean time to repair (in hours) - - Returns - ------- - (λ, μ): Tuple - λ is the probability of a unit being down, μ is probability - of recovery -""" -function outage_to_rate(for_gen, mttr) - μ = 1 / mttr - λ = (μ .* for_gen) ./ (1 .- for_gen) - return (λ = λ, μ = μ) -end - -emptyvec(::Vector{<:Storage}) = Storage[] - -get_components(comps::Vector{<:Storage}, region_name::String) = - get_storages_in_region(comps, region_name) - -emptyvec(::Vector{<:Generator}) = Generator[] - -get_components(comps::Vector{<:Generator}, region_name::String) = - get_generators_in_region(comps, region_name) -# Functions for processing ReEDS2PRAS generators and storages to prepare -# them for PRAS System generation -""" - Gets components in each region of the system and reorganizes them into - single sorted component vector and corresponding component index vector. - - Parameters - ---------- - comps : COMPONENTS - Vector containing components for each region - region_names : Vector{String} - Vector with names of regions in the system - - Returns - ------- - sorted_comps : COMPONENTS - Sorted vector of all components from every region - region_comp_idxs : UnitRange{Int64}, 1 - Index vector pointing to components belonging to each specified region -""" -function get_sorted_components( - comps::COMPONENTS, - region_names::Vector{String}, -) where {COMPONENTS <: Union{Vector{<:Generator}, Vector{<:Storage}}} - num_regions = length(region_names) - all_comps = [] - start_idx = Array{Int64}(undef, num_regions) - region_comp_idxs = Array{UnitRange{Int64}, 1}(undef, num_regions) - - for (idx, region_name) in enumerate(region_names) - region_comps = get_components(comps, region_name) - push!(all_comps, region_comps) - if idx == 1 - start_idx[idx] = 1 - else - prev_idx = start_idx[idx - 1] - prev_length = length(all_comps[idx - 1]) - start_idx[idx] = prev_idx + prev_length - end - region_comp_idxs[idx] = range(start_idx[idx], length = length(all_comps[idx])) - end - - sorted_comps = emptyvec(comps) - for idx in eachindex(all_comps) - if (length(all_comps[idx]) != 0) - append!(sorted_comps, all_comps[idx]) - end - end - return sorted_comps, region_comp_idxs -end - -get_sorted_components( - comps::COMPONENTS, - regions::Vector{Region}, -) where {COMPONENTS <: Union{Vector{<:Generator}, Vector{<:Storage}}} = - get_sorted_components(comps, get_name.(regions)) - -# Functions for processing ReEDS2PRAS lines (preparing PRAS lines) - -""" - Returns a list of tuples sorted by region name. - - Parameters - ---------- - lines : Vector{Line} - A list of lines containing the regions from and to. - - Returns - ------- - List[Tuple[str]] - A list of tuples sorted by region name. -""" -function get_sorted_region_tuples(lines::Vector{Line}, region_names::Vector{String}) - region_idxs = Dict(name => idx for (idx, name) in enumerate(region_names)) - - line_from_to_reg_idxs = similar(lines, Tuple{Int, Int}) - - for (l, line) in enumerate(lines) - from_name = get_region_from(line) - to_name = get_region_to(line) - - from_idx = region_idxs[from_name] - to_idx = region_idxs[to_name] - - line_from_to_reg_idxs[l] = - from_idx < to_idx ? (from_idx, to_idx) : (to_idx, from_idx) - end - - return line_from_to_reg_idxs -end - -function get_sorted_region_tuples(lines::Vector{Line}, regions::Vector{Region}) - get_sorted_region_tuples(lines, get_name.(regions)) -end - -function get_sorted_region_tuples(lines::Vector{Line}) - regions_from = get_region_from.(lines) - regions_to = get_region_to.(lines) - - region_names = unique(append!(regions_from, regions_to)) - - get_sorted_region_tuples(lines, region_names) -end - -""" - Returns a list of lines sorted - (acc. to sorted regions tuples and interface_region_idxs and interface_line_idxs for PRAS) - - Parameters - ---------- - lines : Vector{Line} - A Vector of ReEDS2PRAS Line objects - region_names : Vector{String} - Vector with names of regions in the system - - Returns - ------- - Vector{Line}, UnitRange{Int64,1}, UnitRange{Int64,1} - A list of sorted lines, Index vector pointing to interface region_from and to belonging to each specified region, - Index vector pointing to Lines belonging to an interface -""" - -function get_sorted_lines(lines::Vector{Line}, region_names::Vector{String}) - line_from_to_reg_idxs = get_sorted_region_tuples(lines, region_names) - line_ordering = sortperm(line_from_to_reg_idxs) - - sorted_lines = lines[line_ordering] - sorted_from_to_reg_idxs = line_from_to_reg_idxs[line_ordering] - interface_reg_idxs = unique(sorted_from_to_reg_idxs) - - # Ref tells Julia to use interfaces as Vector, only broadcasting over - # lines_sorted - interface_line_idxs = searchsorted.(Ref(sorted_from_to_reg_idxs), interface_reg_idxs) - - return sorted_lines, interface_reg_idxs, interface_line_idxs -end - -get_sorted_lines(lines::Vector{Line}, regions::Vector{Region}) = - get_sorted_lines(lines, get_name.(regions)) - -function check_if_line_exists(reg_from::String, reg_to::String, lines::Vector{Line}) - isnothing(findfirst(x -> (x.region_from == reg_from && x.region_to == reg_to),lines)) ? false : true -end - -""" - This code takes in a vector of Lines and a vector of Regions as input - parameters. It filters the Lines to find VSC (voltage source converter) - lines and non-VSC lines. Then, for each VSC line, it creates two new Line - objects representing direct current (DC) converter capacityfor the regional connections - to the VSC line. Finally, a vector of all lines and a vector of all - regions are returned as output. - - Parameters - ---------- - lines : Vector[Line] - Vector of Line objects that contain information about all the lines in - the system - regions : Vector[Region] - Vector of Region objects that contain information about all regions in - the system - - Returns - ------- - non_vsc_dc_lines : Vector[Line] - Vector of Line objects with non_vsc_dc_lines - regions : Vector[Region] - Vector of Region objects with added DC lines -""" -function process_vsc_lines(lines::Vector{Line}, regions::Vector{Region}) - timesteps = first(regions).timesteps - non_vsc_dc_lines = filter(line -> ~line.VSC, lines) - vsc_dc_lines = filter(line -> line.VSC, lines) - - for vsc_line in vsc_dc_lines - dc_region_from = "DC|$(vsc_line.region_from)" - dc_region_to = "DC|$(vsc_line.region_to)" - - for reg_name in [dc_region_from, dc_region_to] - if ~(reg_name in get_name.(regions)) - push!(regions, Region(reg_name, timesteps, zeros(Float64, timesteps))) - end - end - - push!( - non_vsc_dc_lines, - Line( - name = "$(vsc_line.name)|DC", - timesteps = vsc_line.timesteps, - category = vsc_line.category, - region_from = dc_region_from, - region_to = dc_region_to, - forward_cap = vsc_line.forward_cap, - backward_cap = vsc_line.backward_cap, - legacy = vsc_line.legacy, - FOR = vsc_line.FOR, - MTTR = vsc_line.MTTR, - ), - ) - if !(check_if_line_exists(dc_region_from, vsc_line.region_from, non_vsc_dc_lines)) - push!( - non_vsc_dc_lines, - Line( - name = "$(dc_region_from)_VSC", - timesteps = vsc_line.timesteps, - category = vsc_line.category, - region_from = dc_region_from, - region_to = vsc_line.region_from, - forward_cap = vsc_line.converter_capacity[vsc_line.region_from], - backward_cap = vsc_line.converter_capacity[vsc_line.region_from], - legacy = vsc_line.legacy, - FOR = vsc_line.FOR, - MTTR = vsc_line.MTTR, - ), - ) - end - if !(check_if_line_exists(dc_region_to, vsc_line.region_to, non_vsc_dc_lines)) - push!( - non_vsc_dc_lines, - Line( - name = "$(dc_region_to)_VSC", - timesteps = vsc_line.timesteps, - category = vsc_line.category, - region_from = dc_region_to, - region_to = vsc_line.region_to, - forward_cap = vsc_line.converter_capacity[vsc_line.region_to], - backward_cap = vsc_line.converter_capacity[vsc_line.region_to], - legacy = vsc_line.legacy, - FOR = vsc_line.FOR, - MTTR = vsc_line.MTTR, - ), - ) - end - end - return non_vsc_dc_lines, regions -end - -""" - This function creates interfaces between lines that are contained in - different regions. The get_name function then assigns the associated region - name to each interface. - - Parameters - ---------- - sorted_lines: Vector{Line} - A vector containing sorted line information - interface_reg_idxs: Vector{Tuple{Int64, Int64}} - A vector containing the index of the region for each interface - interface_line_idxs: Vector{UnitRange{Int64}} - A vector containing indices of the lines involved in the interface - regions: Vector{Region} - A vector containing the region information -""" -function make_pras_interfaces( - sorted_lines::Vector{Line}, - interface_reg_idxs::Vector{Tuple{Int64, Int64}}, - interface_line_idxs::Vector{UnitRange{Int64}}, - regions::Vector{Region}, -) - make_pras_interfaces( - sorted_lines, - interface_reg_idxs, - interface_line_idxs, - get_name.(regions), - ) -end - -""" - This function creates a Lines and Interfaces object from the given input - arguments. - - Parameters - ---------- - sorted_lines : Vector{Line} - A vector of sorted Line objects - interface_reg_idxs : Vector{Tuple{Int64, Int64}} - A vector of tuples, each tuple containing an interface region index - pair - interface_line_idxs : Vector{UnitRange{Int64}} - A vector of unit ranges indexing into the sorted_lines - region_names : Vector{String} - A vector of strings each denoting a region name - - Returns - ------- - new_lines : PRAS.Lines{timesteps, 1, PRAS.Hour, PRAS.MW} - A new Lines object created from the sorted_lines vector - new_interfaces : PRAS.Interfaces{, PRAS.MW} - A new interfaces object created from interface_reg_idxs and - interface_line_idxs -""" -function make_pras_interfaces( - sorted_lines::Vector{Line}, - interface_reg_idxs::Vector{Tuple{Int64, Int64}}, - interface_line_idxs::Vector{UnitRange{Int64}}, - region_names::Vector{String}, -) - num_interfaces = length(interface_reg_idxs) - interface_regions_from = first.(interface_reg_idxs) - interface_regions_to = last.(interface_reg_idxs) - - timesteps = first(sorted_lines).timesteps - - # Lines - line_names = get_name.(sorted_lines) - line_cats = get_category.(sorted_lines) - line_forward_cap = reduce(vcat, get_forward_capacity.(sorted_lines)) - line_backward_cap = reduce(vcat, get_backward_capacity.(sorted_lines)) - line_λ = reduce(vcat, get_λ.(sorted_lines)) - line_μ = reduce(vcat, get_μ.(sorted_lines)) - - new_lines = PRAS.Lines{timesteps, 1, PRAS.Hour, PRAS.MW}( - line_names, - line_cats, - line_forward_cap, - line_backward_cap, - line_λ, - line_μ, - ) - - interface_forward_capacity_array = Matrix{Int64}(undef, num_interfaces, timesteps) - interface_backward_capacity_array = Matrix{Int64}(undef, num_interfaces, timesteps) - - for i in 1:num_interfaces - fwd_sum = sum(line_forward_cap[interface_line_idxs[i], :], dims = 1) - interface_forward_capacity_array[i, :] = fwd_sum - back_sum = sum(line_backward_cap[interface_line_idxs[i], :], dims = 1) - interface_backward_capacity_array[i, :] = back_sum - end - - new_interfaces = PRAS.Interfaces{timesteps, PRAS.MW}( - interface_regions_from, - interface_regions_to, - interface_forward_capacity_array, - interface_backward_capacity_array, - ) - - return new_lines, new_interfaces -end diff --git a/reeds2pras/src/reeds_to_pras.jl b/reeds2pras/src/reeds_to_pras.jl deleted file mode 100644 index 897eacfe..00000000 --- a/reeds2pras/src/reeds_to_pras.jl +++ /dev/null @@ -1,73 +0,0 @@ -#runs ReEDS2PRAS -""" - Generates a PRAS system from data in ReEDSfilepath - - Parameters - ---------- - ReEDSfilepath : String - Location of ReEDS filepath where inputs, results, and outputs are - stored - year : Int64 - ReEDS solve year - timesteps : Int - Number of timesteps - weather_year : Int - The weather year for variable gen profiles and load - scheduled_outage : Bool - Flag for reading the scheduled_outage_hourly.h5 file for a monthly - scheduled outage rate values, similar to ReEDS. If false, a zero value - is assumed. Default is false. - hydro_energylim : Bool - If this is false we process hydro with fixed capacity based one - name plate from the max_cap file. If true, we process non-dispatchable - hydro as a VRE with varying capacity and dispatchable hydro as - a generator storage with monthly inflows - - Returns - ------- - PRAS.SystemModel - PRAS SystemModel struct with regions, interfaces, generators, - region_gen_idxs, storages, region_stor_idxs, generatorstorages, - region_genstor_idxs, lines, interface_line_idxs, timestamps - -""" -function reeds_to_pras( - reedscase::String, - solve_year::Int64, - timesteps::Int, - weather_year::Int, - scheduled_outage::Bool = false, - hydro_energylim::Bool = false, - pras_agg_ogs_lfillgas::Bool = false, - pras_existing_unit_size::Bool = true, - pras_max_unitsize_prm::Bool = true, -) - ReEDS_data = ReEDSdatapaths(reedscase, solve_year) - - @info "Running checks on input data..." - run_checks(ReEDS_data) - - @info "Parsing ReEDS data and creating ReEDS2PRAS objects..." - out = parse_reeds_data( - ReEDS_data, - timesteps, - solve_year, - hydro_energylim = hydro_energylim, - scheduled_outage = scheduled_outage, - pras_agg_ogs_lfillgas = pras_agg_ogs_lfillgas, - pras_existing_unit_size = pras_existing_unit_size, - pras_max_unitsize_prm = pras_max_unitsize_prm, - ) - lines, regions, gens, storages, genstors = out - - @info "ReEDS data successfully parsed, creating a PRAS system" - return create_pras_system( - regions, - lines, - gens, - storages, - genstors, - timesteps, - weather_year, - ) -end diff --git a/reeds2pras/src/utils/reeds_data_parsing.jl b/reeds2pras/src/utils/reeds_data_parsing.jl deleted file mode 100644 index ec1acc2d..00000000 --- a/reeds2pras/src/utils/reeds_data_parsing.jl +++ /dev/null @@ -1,1186 +0,0 @@ -""" - Processes ReEDS data and loads the specified weather year and number of - time steps. - - Parameters - ---------- - ReEDS_data : ReEDSData - ReEDSData object containing the load data. - weather_year : Int - The weather year to be loaded. - timesteps : Int - The number of time steps to be loaded. - - Returns - ------- - List - A list of Region objects containing the load data for the specified - weather year and number of time steps. -""" -function process_regions_and_load(ReEDS_data) - load_data = get_load_file(ReEDS_data) - regions = names(load_data) - - return [ - Region(r, size(load_data, 1), Int.(round.(load_data[!, r]))) - for r in regions - ] -end - -""" - This function takes in ReEDS data, a vector of regions, a year, and a - number of time steps, and returns an array of Line objects. It first gets - the line capacity data from the ReEDS data, then gets the converter - capacity data from the ReEDS data. It then adds 0 converter capacity for - regions that lack a converter. It then creates a system line naming - dataframe, which is a subset of the line capacity dataframe, and combines - the MW column by summing it. It then creates a Line object for each row in - the system line naming dataframe, and adds it to the lines_array. If the - line is a VSC line, it adds the converter capacity data to the Line object. - - Parameters - ---------- - ReEDS_data : DataFrame - DataFrame containing ReEDS line capacity data. - regions : Vector{<:AbstractString} - Vector of region names. - timesteps : Int - Number of timesteps. - - Returns - ------- - lines_array : Vector{Line} - Vector of Line objects. -""" -function process_lines( - ReEDS_data, - regions::Vector{<:AbstractString}, - timesteps::Int, -) - #it is assumed this has prm line capacity data - line_base_cap_data = get_line_capacity_data(ReEDS_data) - - converter_capacity_data = get_converter_capacity_data(ReEDS_data) - converter_capacity_dict = Dict( - convert.(String, converter_capacity_data[!, "r"]) .=> - converter_capacity_data[!, "MW"], - ) - - #add 0 converter capacity for regions that lack a converter - if length(keys(converter_capacity_dict)) > 0 - for reg in regions - if !(reg in keys(converter_capacity_dict)) - @info("$reg does not have VSC converter capacity, so adding" * " a 0") - converter_capacity_dict[reg] = 0.0 - end - end - end - - function keep_line(from_pca, to_pca) - from_idx = findfirst(x -> x == from_pca, regions) - to_idx = findfirst(x -> x == to_pca, regions) - return !isnothing(from_idx) && !isnothing(to_idx) && (from_idx < to_idx) - end - system_line_naming_data = - DataFrames.subset(line_base_cap_data, [:r, :rr] => DataFrames.ByRow(keep_line)) - # split-apply-combine b/c some lines have same name convention - system_line_naming_data = DataFrames.combine( - DataFrames.groupby(system_line_naming_data, ["r", "rr", "trtype"]), - :MW => sum, - ) - - lines_array = Line[] - for row in eachrow(system_line_naming_data) - forward_cap = sum( - line_base_cap_data[ - (line_base_cap_data.r .== row.r) .& (line_base_cap_data.rr .== row.rr) .& (line_base_cap_data.trtype .== row.trtype), - "MW", - ], - ) - backward_cap = sum( - line_base_cap_data[ - (line_base_cap_data.r .== row.rr) .& (line_base_cap_data.rr .== row.r) .& (line_base_cap_data.trtype .== row.trtype), - "MW", - ], - ) - - name = "$(row.r)|$(row.rr)|$(row.trtype)" - @debug( - "a line $name, with $forward_cap MW forward and $backward_cap" * - " backward in $(row.trtype)" - ) - if row.trtype != "VSC" - push!( - lines_array, - Line( - name = name, - timesteps = timesteps, - category = row.trtype, - region_from = row.r, - region_to = row.rr, - forward_cap = forward_cap, - backward_cap = backward_cap, - legacy = "Existing", - # We do not model outages for lines so just use filler values - FOR = 0.0, - MTTR = 24, - ), - ) - else - push!( - lines_array, - Line( - name = name, - timesteps = timesteps, - category = row.trtype, - region_from = row.r, - region_to = row.rr, - forward_cap = forward_cap, - backward_cap = backward_cap, - legacy = "Existing", - # We do not model outages for lines so just use filler values - FOR = 0.0, - MTTR = 24, - VSC = true, - converter_capacity = Dict( - row.r => converter_capacity_dict[string(row.r)], - row.rr => converter_capacity_dict[string(row.rr)], - ), - ), - ) - end - end - return lines_array -end - -""" - Split generator types into thermal, storage, and variable generation - resources - - Parameters - ---------- - ReEDS_data : dict - Raw ReEDS data as a dict - year : Int - Year of interest - - Returns - ------- - DataFrames - (thermal, storage, dispatchable hydro, nondispatchable hydro) capacity -""" -function split_generator_types(ReEDS_data::ReEDSdatapaths) - ## Read {case}/inputs_case/tech-subset-table.csv - tech_subset_table = get_technology_types(ReEDS_data) - @debug "tech_subset_table is $(tech_subset_table)" - ## Read {case}/ReEDS_Augur/augur_data/max_cap_{year}.csv - capacity_data = get_ICAP_data(ReEDS_data) - ## Read {case}/inputs_case/resources.csv - resources = get_valid_resources(ReEDS_data) - @debug "resources is $(resources)" - vg_types = unique(resources.i) - push!(vg_types, "vre") - @debug "vg_types is $(vg_types)" - - hyd_disp_types = - lowercase.(DataFrames.dropmissing(tech_subset_table, :HYDRO_D)[:, "Column1"]) - hyd_non_disp_types = - lowercase.(DataFrames.dropmissing(tech_subset_table, :HYDRO_ND)[:, "Column1"]) - - @debug "hd_types is $(union(hyd_disp_types,hyd_non_disp_types))" - - storage_types = - unique(DataFrames.dropmissing(tech_subset_table, :STORAGE_STANDALONE)[:, "Column1"]) - - @debug "storage type is $(storage_types)" - - # clean vg/storage capacity on a regex, though there might be a better way... - clean_names!(vg_types) - @debug "vg_types is $(vg_types)" - clean_names!(storage_types) - - storage_capacity = filter(x -> x.i in storage_types, capacity_data) - - hyd_disp_capacity = filter(x -> x.i in hyd_disp_types, capacity_data) - hyd_non_disp_capacity = filter(x -> x.i in hyd_non_disp_types, capacity_data) - - thermal_capacity = filter( - x -> ~(x.i in union(vg_types, storage_types, hyd_disp_types, hyd_non_disp_types)), - capacity_data, - ) - - @debug "thermal_capacity is $(thermal_capacity)" - return thermal_capacity, - storage_capacity, - hyd_disp_capacity, - hyd_non_disp_capacity -end - -""" - Process existing thermal capacities with disaggregation. - - Parameters - ---------- - ReEDS_data : object - The ReEDS data object - thermal_builds : DataFrames.DataFrame - Data frame containing the thermal build information - FOR_dict : dict - Dictionary of Forced Outage Rates (FORs) for each resource - timesteps : int - Number of slices to disaggregate the capacity - year : int - Year associated with the capacity - scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} - a dataframe of hourly scheduled outage rates - (if the scheduled_outage_rate file is read). If no file is read, a default - nothing is used. - Returns - ------- - all_generators : Generator[] - Array of Generator objects containing the disaggregated capacity for - each resource -""" -function process_thermals_with_disaggregation( - ReEDS_data, - thermal_builds::DataFrames.DataFrame, - FOR_dict::Dict, - forcedoutage_hourly::DataFrames.DataFrame, - unitsize_dict::Dict, - timesteps::Int, - year::Int, - mttr_dict::Dict; - scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame}, - pras_agg_ogs_lfillgas = false, - pras_existing_unit_size = true, - pras_max_unitsize_prm = true, -) - all_generators = Generator[] - # csp-ns is not a thermal; just drop in for now - thermal_builds = thermal_builds[(thermal_builds.i .!= "csp-ns"), :] - # split-apply-combine to handle differently vintaged entries - thermal_builds = - DataFrames.combine(DataFrames.groupby(thermal_builds, ["i", "r"]), :MW => sum) - unitdata = get_unitdata(ReEDS_data) - - # Get the PRM [MW] in case we use it to set the max unit size - if pras_max_unitsize_prm - max_unitsize = get_max_unitsize(ReEDS_data) - end - - # Get the FOR for each build/tech - for row in eachrow(thermal_builds) - tech = row.i - i_r = "$tech|$(row.r)" - - if (i_r in DataFrames.names(forcedoutage_hourly)) - gen_for = forcedoutage_hourly[!, i_r] - elseif lowercase(tech) in keys(FOR_dict) - gen_for = fill(Float32(FOR_dict[lowercase(tech)]), timesteps) - @info( - "$tech ($(row.r)) was not found in forcedoutage_hourly so using " * - "static value of $(FOR_dict[tech]) from outage_forced_static.csv" - ) - else - @error( - "$(tech) ($(row.r)) was not found in forcedoutage_hourly or outage_forced_static.csv" - ) - end - - gen_sor = zeros(Float32, timesteps) - if !isnothing(scheduled_outage_hourly) && tech in DataFrames.names(scheduled_outage_hourly) - gen_sor = scheduled_outage_hourly[!, tech] - end - - mttr = Int64(mttr_dict[tech]) - - generator_array = disagg_existing_capacity( - unitdata, - unitsize_dict, - Int(round(row.MW_sum)), - String(row.i), - String(row.r), - gen_for, - timesteps, - year, - mttr, - gen_sor, - pras_agg_ogs_lfillgas = pras_agg_ogs_lfillgas, - pras_existing_unit_size = pras_existing_unit_size, - # Use PRM MW if pras_max_unitsize_prm switch is on; otherwise ignore by setting to 0 - max_unit_mw = (pras_max_unitsize_prm ? max_unitsize[row.r] : 0), - ) - append!(all_generators, generator_array) - end - return all_generators -end - -""" - We use this function if we want to process hydroelectric generators as fixed capacities. - It is a replication of the process thermal functions to retain the older way of - handling hydro plants, where we do not use hydro capacity factors, and distinguish - dispatchable and non dispatchable hydroelectric generators. - - Parameters - ---------- - all_generators : Generator[] - Array of Generator objects containing the capacity for - each resource, and it is modified in place. - ReEDS_data : object - The ReEDS data object - hd_builds : DataFrames.DataFrame - Data frame containing the hydro plant information - FOR_dict : dict - Dictionary of Forced Outage Rates (FORs) for each resource - timesteps : int - Number of slices to disaggregate the capacity - year : int - Year associated with the capacity - scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} - a dataframe of hourly scheduled outage rates - (if the scheduled_outage_rate file is read). If no file is read, a default - nothing is used. - Returns - ------- -""" -function process_hd_as_generator!( - all_generators, - ReEDS_data, - hd_builds::DataFrames.DataFrame, - FOR_dict::Dict, - forcedoutage_hourly::DataFrames.DataFrame, - unitsize_dict::Dict, - timesteps::Int, - year::Int, - mttr_dict::Dict, - scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame}, -) - - # split-apply-combine to handle differently vintaged entries - hd_builds = DataFrames.combine(DataFrames.groupby(hd_builds, ["i", "r"]), :MW => sum) - unitdata = get_unitdata(ReEDS_data) - - # Get the FOR for each build/tech - for row in eachrow(hd_builds) - tech = row.i - i_r = "$tech|$(row.r)" - - if (i_r in DataFrames.names(forcedoutage_hourly)) - gen_for = forcedoutage_hourly[!, i_r] - elseif lowercase(tech) in keys(FOR_dict) - gen_for = fill(Float32(FOR_dict[lowercase(tech)]), timesteps) - @info( - "$tech ($(row.r)) was not found in forcedoutage_hourly so using " * - "static value of $(FOR_dict[tech]) from outage_forced_static.csv" - ) - else - @error( - "$(tech) ($(row.r)) was not found in forcedoutage_hourly or outage_forced_static.csv" - ) - end - - mttr = Int64(mttr_dict[tech]) - - gen_sor = zeros(Float32, timesteps) - if !isnothing(scheduled_outage_hourly) && tech in DataFrames.names(scheduled_outage_hourly) - gen_sor = scheduled_outage_hourly[!, tech] - end - - generator_array = disagg_existing_capacity( - unitdata, - unitsize_dict, - Int(round(row.MW_sum)), - String(row.i), - String(row.r), - gen_for, - timesteps, - year, - mttr, - gen_sor, - ) - append!(all_generators, generator_array) - end -end - -""" - Add generators for each tech/region in pras_vre_gen_{year}.h5. - Capacity is taken as the maximum of the hourly generation profile. - VRE outages are already included in VRE profiles, so we do not disaggregate - VRE or apply outages in PRAS. - - Parameters - ---------- - generators_array : Vector{<:ReEDS2PRAS.Generator} - Vector of ReEDS Generators - ReEDS_data : DataFrames.DataFrame - A dataset from the ReEDS Program - scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} - a dataframe of hourly scheduled outage rates - (if the scheduled_outage_rate file is read). If no file is read, a default - nothing is used. - Returns - ------- - generators_array : Vector{<:ReEDS2PRAS.Generator} - An array of the VG generators -""" -function process_vg( - generators_array::Vector{<:ReEDS2PRAS.Generator}, - ReEDS_data, -) - vg_profiles = get_vg_cf_data(ReEDS_data) - timesteps = size(vg_profiles, 1) - - for name in names(vg_profiles) - tech, region = split(name, "|") - profile = vg_profiles[!, name] - push!( - generators_array, - Variable_Gen( - name = name, - timesteps = timesteps, - region_name = region, - installed_capacity = maximum(profile), - capacity = profile, - type = tech, - legacy = "New", - FOR = zeros(Float32, timesteps), - MTTR = 24, - ), - ) - end - return generators_array -end - -""" - Parameters - ---------- - generators_array : Vector{<:Generator} - a vector of strings of distinct regions in the model - hydro_disp_capacities : DataFrame - a dataframe containing details of dispatchable (reservoir) - HD generators - hydro_non_disp_capacities : DataFrame - a dataframe containing details of non-dispatchable - (run-of-river) HD generators - FOR_dict : Dict - a dictionary of forced outage rates, not applied to HD - devices for now - ReEDS_data - input data from ReEDS - timesteps : Int - number of timesteps - year : Int64 - ReEDS target simulation year - scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} - a dataframe of hourly scheduled outage rates - (if the scheduled_outage_rate file is read). If no file is read, a default - nothing is used. - hydro_energylim : Bool - a flag which allows users to choose whether to model HD - devices as flat generators, or as variable generator for - run-of-river plants and generatorstorage for reservoir - plants - unitsize_dict = nothing, - Returns - ------- - generators_array : Vector{<:Generator} - Generators array updated with hydro generators, either all - generators as static capacity, or run-of-river generators - as variable generators - gen_stors : Gen_Storage - Generator_storages array returned as empty if we don't - process energy limits, or returned with reservoir hydro - plants (dispatchable type) -""" -function process_hydro( - generators_array::Vector{<:Generator}, - hydro_disp_capacities::DataFrames.DataFrame, - hydro_non_disp_capacities::DataFrames.DataFrame, - FOR_dict::Dict, - forcedoutage_hourly::DataFrames.DataFrame, - ReEDS_data, - year::Int64, - timesteps::Int, - mttr_dict::Dict, - unitsize_dict; - scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame}, - hydro_energylim = false, -) - - # If we do not impose energy limits on hydro and model it as fixed capacity - if !(hydro_energylim) - @info "Processing HD generators as generator with fixed capacities..." - - process_hd_as_generator!( - generators_array, - ReEDS_data, - hydro_disp_capacities, - FOR_dict, - forcedoutage_hourly, - unitsize_dict, - timesteps, - year, - mttr_dict, - scheduled_outage_hourly, - ) - - process_hd_as_generator!( - generators_array, - ReEDS_data, - hydro_non_disp_capacities, - FOR_dict, - forcedoutage_hourly, - unitsize_dict, - timesteps, - year, - mttr_dict, - scheduled_outage_hourly - ) - genstor_array = Gen_Storage[] - - return generators_array, genstor_array - end - - hydcf, hydcapadj = get_hydro_data(ReEDS_data) - monthhours = monhours() - - timesteps_year = 8760 - num_years = Int(timesteps // timesteps_year) - - # Combine all plant vintages for each region and plant type for dispatchable plants - disp_combined_caps = - DataFrames.combine(DataFrames.groupby(hydro_disp_capacities, [:i, :r]), :MW => sum) - - genstor_array = Gen_Storage[] - - # We need the dispatch limit for each hour from each asset and each month's energy budget - # applied as an exogenous limit using inflow. We have monthly capacity factors for energy budget - # and monthly capacity adjustment factor on the nameplate capacity for dispatch limits. - # For each month, (i) energy budget is calculated based on number of hours in the month - # and (ii) dispatch limit is calculated based on the month. - for (idx, row) in enumerate(DataFrames.eachrow(disp_combined_caps)) - reg_plant_subset = - filter(x -> (x.r == row.r && x.i == row.i), hydcf)[:, [:month, :value]] - monthly_energy = zeros(timesteps_year) - dispatch_limit = zeros(timesteps_year) - energy_cap = zeros(timesteps_year) - for monhr in monthhours - try - reqd_slice = monhr.slice - monthly_energy[first(reqd_slice)] = - (monhr.numhrs * filter(x -> (x.month == monhr.month), reg_plant_subset)[ - :, - :value, - ] * row.MW_sum)[1] - - energy_cap[reqd_slice] .= - (monhr.numhrs * filter(x -> (x.month == monhr.month), reg_plant_subset)[ - :, - :value, - ] * row.MW_sum)[1] - - capacity_adjust = hydcapadj[ - (hydcapadj.i .== row.i) .&& (hydcapadj.r .== row.r), - [:month, :value], - ] - dispatch_limit[reqd_slice] .= - (filter(x -> (x.month == monhr.month), capacity_adjust)[ - :, - :value, - ] * row.MW_sum)[1] - catch e - if isa(e, BoundsError) - @error "$(row.r),$(row.i),$(e)" - else - error() - end - end - end - - category = string(row.i) - name = "$(category)_$(string(row.r))" - region = string(row.r) - - gen_sor = zeros(Float32, timesteps) - if !isnothing(scheduled_outage_hourly) && category in DataFrames.names(scheduled_outage_hourly) - gen_sor = scheduled_outage_hourly[!, category] - end - - mttr = Int64(mttr_dict[category]) - - # - Charging to genstore is limited by charge_capacity whether from grid or from - # inflows. So, that charge_capacity should be equal to the genflow timeseries. - # - Powerflow into grid is limited by grid_injection, which can come from discharge - # and/or exogenous inflow. So discharge capacity can be the dispatch limit or - # arbitrarily high, while the grid_injection cap has to the dispatch limit. - # - Energy capacity can be arbitrarily high in the absense of reservoir limit to - # ensure month to month energy energy carryover. - push!( - genstor_array, - Gen_Storage( - name = name, - timesteps = timesteps, - region_name = region, - charge_cap = repeat(monthly_energy, num_years), - discharge_cap = repeat(dispatch_limit, num_years), - energy_cap = repeat(energy_cap, num_years), - inflow = repeat(monthly_energy, num_years), - grid_withdrawl_cap = zeros(timesteps), - grid_inj_cap = repeat(dispatch_limit, num_years), - type = category, - legacy = "New", - FOR = zeros(Float32, timesteps), - SOR = gen_sor, - MTTR = mttr, - ), - ) - end - - # Non-dispatchable hydro power plants - non_disp_combined_caps = DataFrames.combine( - DataFrames.groupby(hydro_non_disp_capacities, [:i, :r]), - :MW => sum, - ) - - # For non dispatchable hydro plants, the monthly capacity factor is used to determine - # hourly capacity time series in each month. - for (idx, row) in enumerate(DataFrames.eachrow(non_disp_combined_caps)) - reg_type = hydcf[ - findall(x -> (x.r == row.r && x.i == row.i), eachrow(hydcf)), - [:month, :value], - ] - hourly_capacity = zeros(timesteps_year) - for monhr in monthhours - try - reqd_slice = monhr.slice - hourly_capacity[reqd_slice] .= - (filter(x -> (x.month == monhr.month), reg_type)[ - :, - :value, - ] * row.MW_sum)[1] - catch e - @error "$(row.r),$(row.i),$(e)" - end - end - - category = string(row.i) - name = "$(category)_$(string(row.r))" - region = string(row.r) - gen_sor = zeros(Float32, timesteps) - if !isnothing(scheduled_outage_hourly) && category in DataFrames.names(scheduled_outage_hourly) - gen_sor = scheduled_outage_hourly[!, category] - end - mttr = Int64(mttr_dict[category]) - - push!( - generators_array, - Variable_Gen( - name = name, - timesteps = timesteps, - region_name = region, - installed_capacity = row.MW_sum, - capacity = repeat(hourly_capacity, num_years), - type = category, - legacy = "New", - FOR = zeros(Float32, timesteps), - SOR = gen_sor, - MTTR = mttr, - ), - ) - end - - return generators_array, genstor_array -end - -""" - Process data associated with the regional storage build for modeled time - period - - Parameters - ---------- - storage_builds : DataFrames.DataFrame - Data construct containing regional storage build information - FOR_dict : Dict - dictionary of Forced Outage Rates (FOR) associated with storage types - ReEDS_data - input data from ReEDS - timesteps : Int - Number of timesteps - year : Int64 - simulated time period - scheduled_outage_hourly: Union{Nothing, DataFrames.DataFrame} - a dataframe of hourly scheduled outage rates - (if the scheduled_outage_rate file is read). If no file is read, a default - nothing is used. - Returns - ------- - storages_array : Storage[] - array of modeled storages -""" -function process_storages( - storage_builds::DataFrames.DataFrame, - FOR_dict::Dict, - forcedoutage_hourly::DataFrames.DataFrame, - unitsize_dict::Dict, - ReEDS_data, - timesteps::Int, - mttr_dict::Dict; - scheduled_outage_hourly::Union{Nothing, DataFrames.DataFrame} -) - storage_energy_capacity_data = get_storage_energy_capacity_data(ReEDS_data) - @debug "storage_energy_capacity_data is $(storage_energy_capacity_data)" - # split-apply-combine to handle differently vintaged entries - energy_capacity_df = DataFrames.combine( - DataFrames.groupby(storage_energy_capacity_data, ["i", "r"]), - :MWh => sum, - ) - - efficiency_in = Dict( - polarity => DataFrames.DataFrame(CSV.File(joinpath( - ReEDS_data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", - "$(polarity)_eff_$(ReEDS_data.year).csv" - ))) - for polarity in ["charge", "discharge"] - ) - efficiency = Dict( - polarity => Dict(zip(efficiency_in[polarity][!,"i"], efficiency_in[polarity][!,"fraction"])) - for polarity in keys(efficiency_in) - ) - - ## Read {case}/inputs_case/tech-subset-table.csv - tech_subset_table = get_technology_types(ReEDS_data) - battery_types = DataFrames.dropmissing(tech_subset_table, :BATTERY)[:, "Column1"] - - storages_array = Storage[] - for (idx, row) in enumerate(eachrow(storage_builds)) - gen_sor = zeros(Float32, timesteps) - storage_type = string(row.i) - if !isnothing(scheduled_outage_hourly) && storage_type in DataFrames.names(scheduled_outage_hourly) - gen_sor = scheduled_outage_hourly[!, storage_type] - end - - name = "$(string(row.i))|$(string(row.r))" - - gen_for = nothing - if (name in DataFrames.names(forcedoutage_hourly)) - gen_for = forcedoutage_hourly[!, name] - else - gen_for = fill(Float32(FOR_dict[lowercase(storage_type)]), timesteps) - @info( - "$name was not found in forcedoutage_hourly so using " * - "static value of $(FOR_dict[storage_type]) from outage_forced_static.csv") - end - - name = "$(name)|"#append for later matching - mttr = Int64(mttr_dict[string(row.i)]) - - storage_duration = energy_capacity_df[idx, "MWh_sum"] / row.MW - if string(row.i) in battery_types - push!( - storages_array, - Battery( - name = name, - timesteps = timesteps, - region_name = string(row.r), - type = string(row.i), - charge_cap = row.MW, - discharge_cap = row.MW, - ## Battery FOR is applied to energy capacity, not power capacity - energy_cap = row.MW * storage_duration * (1 .- gen_for), - legacy = "New", - charge_eff = efficiency["charge"][string(row.i)], - discharge_eff = efficiency["discharge"][string(row.i)], - carryover_eff = 1.0, - FOR = zeros(Float32, timesteps), - SOR = gen_sor, - MTTR = mttr, - ), - ) - else - add_new_capacity!( - storages_array, - round(Int, row.MW), - storage_duration, - efficiency["charge"][string(row.i)], - efficiency["discharge"][string(row.i)], - # Always use the characteristic unit size - unitsize_dict[row.i], - row.i, - row.r, - gen_for, - timesteps, - mttr, - gen_sor, - ) - end - end - return storages_array -end - -""" - Disaggregates the existing capacity of a thermal generator given a certain - year, by taking into account its technology and associated balancing - authority. - - Parameters - ---------- - unitdata : DataFrames.DataFrame - DataFrame containing Expected Information Administration (EIA) data. - unitsize_dict: Dict - Map from techs to characteristic unit size [MW] - built_capacity : int - The current built capacity for the thermal generator. - tech : str - The technology for the thermal generator. - pca : str - The associated balancing authority (PCA). - gen_for : float - The forced outage rate associated with the generator. - gen_sor : float - The scheduled outage rate associated with the generator. - timesteps : Int - Number of timesteps. - year : int - The year. - mttr : int - mean time to repaire (mttr) - pras_agg_ogs_lfillgas : bool - If true, aggregate existing o-g-s and landfill gas using size for new units. - Applies to existing o-g-s and landfill gas capaity, so does not interact with - pras_existing_unit_size, which only affects new capacity. - pras_existing_unit_size : bool - If true, use average existing unit size by (tech,region) when disaggregating new - capacity. Applies to new capacity so does not interact with pras_agg_ogs_lfillgas. - max_unit_mw: int - If nonzero, caps the upper bound of disaggregated unit size - - Returns - ------- - generators_array : array - An array composed of thermal generator objects created with the - disaggregated existing capacities. -""" -function disagg_existing_capacity( - unitdata::DataFrames.DataFrame, - unitsize_dict::Dict, - built_capacity::Int, - tech::String, - pca::String, - gen_for::Vector{Float32}, - timesteps::Int, - year::Int, - mttr::Int, - gen_sor::Union{Nothing, Vector{Float32}} = nothing; - pras_agg_ogs_lfillgas = false, - pras_existing_unit_size = true, - max_unit_mw = 0, -) - if pras_agg_ogs_lfillgas == true - group_existing_techs = ["lfill-gas", "o-g-s"] - else - group_existing_techs = [] - end - - tech_ba_year_existing = DataFrames.subset( - unitdata, - :tech => DataFrames.ByRow(==(tech)), - :reeds_ba => DataFrames.ByRow(==(pca)), - :RetireYear => DataFrames.ByRow(>(year)), - :StartYear => DataFrames.ByRow(<=(year)), - ) - - generators_array = [] - # If there is no existing capacity, use the characteristic unit size - if size(tech_ba_year_existing, 1) == 0 && gen_for != 0.0 - add_new_capacity!( - generators_array, - built_capacity, - # If max_unit_mw is provided and is smaller than the characteristic unit size, - # use max_unit_mw; otherwise use the characteristic unit size from unitsize_dict - ((max_unit_mw > 0) ? min(max_unit_mw, unitsize_dict[tech]) : unitsize_dict[tech]), - tech, - pca, - gen_for, - timesteps, - mttr, - gen_sor, - ) - return generators_array - # If the FOR is zero, no need to disaggregate, so put all capacity in one unit - elseif size(tech_ba_year_existing, 1) == 0 && gen_for == 0.0 - return [ - Thermal_Gen( - name = "$(tech)|$(pca)|1", - timesteps = timesteps, - region_name = pca, - capacity = built_capacity, - fuel = tech, - legacy = "New", - FOR = gen_for, - SOR = gen_sor, - MTTR = mttr, - ), - ] - end - - remaining_capacity = built_capacity - if tech in group_existing_techs - existing_capacity_total = sum(tech_ba_year_existing[!, "summer_power_capacity_MW"]) - num_whole = Int(existing_capacity_total ÷ unitsize_dict[tech]) - remainder = existing_capacity_total % unitsize_dict[tech] - existing_capacity = vcat(ones(num_whole) * unitsize_dict[tech], remainder) - else - existing_capacity = tech_ba_year_existing[!, "summer_power_capacity_MW"] - end - - if pras_existing_unit_size == true - # If the average integer unit size rounds to 0 MW, use 1 MW - unit_capacity = max(Int(round(Statistics.mean(existing_capacity))), 1) - # If max_unit_mw is provided and is smaller than the mean, use max_unit_mw instead - if max_unit_mw > 0 - unit_capacity = min(unit_capacity, max_unit_mw) - end - else - unit_capacity = 0 - end - - @info "$tech $pca: unit capacity = $unit_capacity MW" - - for (idx, built_cap) in enumerate(existing_capacity) - int_built_cap = Int(round(built_cap)) - if int_built_cap < remaining_capacity - gen_cap = int_built_cap - remaining_capacity -= int_built_cap - else - gen_cap = remaining_capacity - remaining_capacity = 0 - end - gen = Thermal_Gen( - name = "$(tech)|$(pca)|$(idx)", - timesteps = timesteps, - region_name = pca, - capacity = gen_cap, - fuel = tech, - legacy = "Existing", - FOR = gen_for, - SOR = gen_sor, - MTTR = mttr, - ) - push!(generators_array, gen) - end - - #whatever remains, we want to build as new capacity - if remaining_capacity > 0 - add_new_capacity!( - generators_array, - remaining_capacity, - (if (unit_capacity > 0) unit_capacity else unitsize_dict[tech] end), - tech, - pca, - gen_for, - timesteps, - mttr, - gen_sor, - ) - end - - return generators_array -end - -""" - This function adds new capacity to an existing list of generators. The - unit_capacity parameter is used to determine how many generators - must be constructed to create the total new_capacity. If there are no - existing units, a single generator is built with all of the new_capacity. - For fixed unit_capacity values, the remaining capacity is divided - evenly among the new generators, adding additional capacity to each one, - then a small remainder may be created and added as a separate generator to - get the desired total new_capacity. The output of this function is the - updated generators_array containing all of the newly added generators. - - Parameters - ---------- - generators_array : Vector{<:Any} - a vector or list of existing generators - new_capacity : int - specified new capacity to be added - unit_capacity : int - target capacity for the units to be added - tech : string - type of technology used for the generator unit - pca : string - power control authority of the generator unit - gen_for : float - generation forecast - gen_sor : float - The scheduled outage rate associated with the generator. - timesteps : Int - number of timesteps - MTTR : int - mean time to repair for the generator unit - - Returns - ------- - generators_array: Vector{<:Any} - updated vector or list of generators containing the new capacity -""" -function add_new_capacity!( - generators_array::Vector{<:Any}, - new_capacity::Int, - unit_capacity::Int, - tech::AbstractString, - pca::AbstractString, - gen_for::Vector{Float32}, - timesteps::Int, - MTTR::Int, - gen_sor::Union{Nothing, Vector{Float32}} = nothing, -) - n_gens = floor(Int, new_capacity / unit_capacity) - if n_gens == 0 - return push!( - generators_array, - Thermal_Gen( - name = "$(tech)|$(pca)|new|1", - timesteps = timesteps, - region_name = pca, - capacity = new_capacity, - fuel = tech, - legacy = "New", - FOR = gen_for, - SOR = gen_sor, - MTTR = MTTR, - ), - ) - end - - for i in range(1, n_gens) - push!( - generators_array, - Thermal_Gen( - name = "$(tech)|$(pca)|new|$(i)", - timesteps = timesteps, - region_name = pca, - capacity = unit_capacity, - fuel = tech, - legacy = "New", - FOR = gen_for, - SOR = gen_sor, - MTTR = MTTR, - ), - ) - end - - remainder = new_capacity - (n_gens * unit_capacity) - if remainder > 0 - # integer remainder is made into a tiny gen - push!( - generators_array, - Thermal_Gen( - name = "$(tech)|$(pca)|new|$(n_gens+1)", - timesteps = timesteps, - region_name = pca, - capacity = remainder, - fuel = tech, - legacy = "New", - FOR = gen_for, - SOR = gen_sor, - MTTR = MTTR, - ), - ) - end - - return generators_array -end - -""" - This function is the same as above but takes an additional arg - new_duration, which will be picked up by multiple dispatch - and leading to Battery{} model handling -""" -function add_new_capacity!( - generators_array::Vector{<:Any}, - new_capacity::Int, - new_duration::Float64, - charge_eff::Float64, - discharge_eff::Float64, - unit_capacity::Int, - tech::AbstractString, - pca::AbstractString, - gen_for::Vector{Float32}, - timesteps::Int, - MTTR::Int, - gen_sor::Union{Nothing, Vector{Float32}} = nothing, -) - n_gens = floor(Int, new_capacity / unit_capacity) - if n_gens == 0 - return push!( - generators_array, - Battery( - name = "$(tech)|$(pca)|new|1", - timesteps = timesteps, - region_name = pca, - type = tech, - charge_cap = new_capacity, - discharge_cap = new_capacity, - energy_cap = fill(Float64(new_capacity * new_duration), timesteps), - legacy = "New", - charge_eff = charge_eff, - discharge_eff = discharge_eff, - carryover_eff = 1.0, - FOR = gen_for, - SOR = gen_sor, - MTTR = MTTR, - ), - ) - end - - for i in range(1, n_gens) - push!( - generators_array, - Battery( - name = "$(tech)|$(pca)|new|$(i)", - timesteps = timesteps, - region_name = pca, - type = tech, - charge_cap = unit_capacity, - discharge_cap = unit_capacity, - energy_cap = fill(Float64(unit_capacity * new_duration), timesteps), - legacy = "New", - charge_eff = charge_eff, - discharge_eff = discharge_eff, - carryover_eff = 1.0, - FOR = gen_for, - SOR = gen_sor, - MTTR = MTTR, - ), - ) - end - - remainder = new_capacity - (n_gens * unit_capacity) - if remainder > 0 - # integer remainder is made into a tiny gen - push!( - generators_array, - Battery( - name = "$(tech)|$(pca)|new|$(n_gens+1)", - timesteps = timesteps, - region_name = pca, - type = tech, - charge_cap = remainder, - discharge_cap = remainder, - energy_cap = fill(Float64(remainder * new_duration), timesteps), - legacy = "New", - charge_eff = charge_eff, - discharge_eff = discharge_eff, - carryover_eff = 1.0, - FOR = gen_for, - SOR = gen_sor, - MTTR = MTTR, - ), - ) - end - - return generators_array -end diff --git a/reeds2pras/src/utils/reeds_input_parsing.jl b/reeds2pras/src/utils/reeds_input_parsing.jl deleted file mode 100644 index 985936bc..00000000 --- a/reeds2pras/src/utils/reeds_input_parsing.jl +++ /dev/null @@ -1,497 +0,0 @@ -""" - Creates a datapath for a given ReEDS model year. Used as a parameter for other - functions in order to access correctly dated input files. - - Parameters - ---------- - x : String - Path to ReEDS case (e.g. "~/github/ReEDS-2.0/runs/name_of_reeds_case") - y : Int - ReEDS model year - - Returns - ------- - A new object with filepath and valid year parameters -""" -struct ReEDSdatapaths - ReEDSfilepath::String - year::Int - - function ReEDSdatapaths(x, y) - return new(x, y) - end -end - -"Includes functions used for loading ReEDS data" - -""" - Loop through the vector and replace any '*' present in the elements with - the text between the asterisk, excluding the '_'. - - Parameters - ---------- - input_vec : Vector{<:AbstractString} - Vector of strings that need to be parsed - - Returns - ------- - input_vec : Vector{<:AbstractString} - Vector of strings which has been cleaned of '*' -""" -function clean_names!(input_vec::Vector{<:AbstractString}) - for (idx, a) in enumerate(input_vec) - if occursin("*", a) - input_vec[idx] = match(r"\*([a-zA-Z]+-*[a-zA-Z]*)_*", a)[1] - end - end - return input_vec -end - -""" - Loads the EIA-NEMS Generator Database from a given ReEDS directory. - - Parameters - ---------- - - Returns - ------- - unitdata : DataFrame - A DataFrame containing the EIA-NEMS Generator Database data. -""" -function get_unitdata(data::ReEDSdatapaths) - filepath = joinpath(data.ReEDSfilepath, "inputs_case", "unitdata.csv") - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - Loads an .h5 file from the given file path, containing Electrical Demand - data from the indicated year. - - Parameters - ---------- - data : ReEDSdatapaths - An instance of ReEDSdatapaths with the necessary arguments set. - - Returns - ------- - HDF5.h5read(filepath, "data") - A readout of the Augur load h5 file associated with the given ReEDS - filepath and year. - -""" -function get_load_file(data::ReEDSdatapaths) - filepath = joinpath( - data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", - "pras_load_$(string(data.year)).h5", - ) - columns = HDF5.h5read(filepath, "columns") - data = HDF5.h5read(filepath, "data") - df = DataFrames.DataFrame(transpose(data), columns) - return df -end - -""" - This function reads a hdf5 file from the ReEDS Augur directory, based on - the year provided in the ReEDSdatapaths struct. - - Parameters - ---------- - data : ReEDSdatapaths - Struct containing the `ReEDSfilepath` and a year, indicating which .h5 - file should be read. - - Returns - ------- - The requested ``hdf5`` as a data frame. -""" -function get_vg_cf_data(data::ReEDSdatapaths) - filepath = joinpath( - data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", - "pras_vre_gen_$(string(data.year)).h5", - ) - columns = HDF5.h5read(filepath, "columns") - data = HDF5.h5read(filepath, "data") - df = DataFrames.DataFrame(transpose(data), columns) - return df -end - -""" - Get tech-dependent MTTR. - - Parameters - ---------- - data : ReEDSdatapaths - Struct containing relevant datapaths and year from which to extract - the data. - - Returns - ------- - Dictionary - tech => MTTR [hours] -""" -function get_MTTR_data(data::ReEDSdatapaths) - filepath = joinpath(data.ReEDSfilepath, "inputs_case", "mttr.csv") - df = DataFrames.DataFrame(CSV.File(filepath)) - return Dict(df[!, "tech"] .=> df[!, "hours"]) -end - - -""" - Get region-dependent max unit size [MW] as dictionary -""" -function get_max_unitsize(data::ReEDSdatapaths) - filepath = joinpath( - data.ReEDSfilepath, "ReEDS_Augur", "augur_data", - "max_unitsize_$(string(data.year)).csv" - ) - df = DataFrames.DataFrame(CSV.File(filepath)) - return Dict(df[!, "r"] .=> df[!, "mw"]) -end - - -""" - Get the forced outage data from the augur files. - - Parameters - ---------- - data : ReEDSdatapaths - Struct containing relevant datapaths and year from which to extract - the data. - - Returns - ------- - DataFrames.DataFrame - Dataframe containing the forced outage data. -""" -function get_forced_outage_data(data::ReEDSdatapaths) - filepath = joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_static.csv") - df = DataFrames.DataFrame(CSV.File(filepath, header = false)) - return DataFrames.rename!(df, ["ResourceType", "FOR"]) -end - -""" - Get the valid resources from {case}/inputs_case/resources.csv - - Parameters - ---------- - data : ReEDSdatapaths - Struct containing ReEDS filepaths and year - - Returns - ------- - DataFrames.DataFrame - A DataFrame containing the valid resources of the ReEDS case -""" -function get_valid_resources(data::ReEDSdatapaths) - filepath = joinpath(data.ReEDSfilepath, "inputs_case", "resources.csv") - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - This function gets the technology types from {case}/inputs_case/tech-subset-table.csv - - Arguments - --------- - data : ReEDSdatapaths - A struct containing paths and dates related to ReEDS analyses. - - Returns - ------- - DataFrames.DataFrame - The technology type table in the form of a DataFrames object. -""" -function get_technology_types(data::ReEDSdatapaths) - filepath = joinpath(data.ReEDSfilepath, "inputs_case", "tech-subset-table.csv") - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - Gets line capacity data for the given ReEDS database. - - Parameters - ---------- - data : ReEDSdatapaths - Contains the filepath of data and year of analysis - - Returns - ------- - DataFrame - A dataframe with transmission capacity data; assumes this file has been - formatted by ReEDS -""" -function get_line_capacity_data(data::ReEDSdatapaths) - #assumes this file has been formatted by ReEDS to be PRM line capacity data - filepath = joinpath( - data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", - "tran_cap_$(string(data.year)).csv", - ) - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - Get the converter capacity data associated with the given ReEDSdatapaths - object. - - Parameters - ---------- - data : ReEDSdatapaths) - A ReEDSdatapaths object containing the relevant file paths and year. - - Returns - ------- - DataFrames.DataFrame - The DataFrame of the converter capacity data for the given year. -""" -function get_converter_capacity_data(data::ReEDSdatapaths) - filepath = joinpath( - data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", - "cap_converter_$(string(data.year)).csv", - ) - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - Returns a DataFrame containing the Annual Technology Baseline - default unit size for the ReEDSdatapaths object. - - Parameters - ---------- - data : ReEDSdatapaths - An object containing the filepaths to the ReEDS input files. - - Returns - ------- - DataFrame - A DataFrame containing the default unit size mapping. - - Raises - ------ - Error - If no table of unit size mapping is found. - -""" -function get_unitsize_mapping(data::ReEDSdatapaths) - filepath = joinpath(data.ReEDSfilepath, "inputs_case", "unitsize.csv") - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - Returns a DataFrame containing the installed capacity of generators for a - given year, read from {case}/ReEDS_Augur/augur_data/max_cap_{year}.csv. - - Parameters - ---------- - data : ReEDSdatapaths - A ReEDSdatapaths object containing the year and filepath. - - Returns - ------- - DataFrame - A DataFrame containing the installed capacity data. - - Raises - ------ - Error - If the year does not have generator installed capacity data. -""" -function get_ICAP_data(data::ReEDSdatapaths) - filepath = joinpath( - data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", - "max_cap_$(string(data.year)).csv", - ) - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - Returns DataFrames containing hydroelectric plants capacity factors - - Parameters - ---------- - data : ReEDSdatapaths - A ReEDSdatapaths object containing the year and filepath. - - Returns - ------- - hydcf: DataFrame - A DataFrame containing the seasonal capacity factors for both - dispatchable and non-dispatchable hydroelectric plants, subsetted - to the required output year. - - hydcapadj: DataFrame - A DataFrame containing seasonal capacity adjustment factors for - dispatchable hydroelectric plants which limits the maximum hourly - dispatch (MW) in each season. - - Raises - ------ - Error - If the filepath for the for the two files do not exist. -""" -function get_hydro_data(data::ReEDSdatapaths) - filepath_cf = joinpath(data.ReEDSfilepath, "inputs_case", "hydcf.csv") - hydcf = DataFrames.DataFrame(CSV.File(filepath_cf)) - - # Rename plant types as techtypes and capacity are lowercase - DataFrames.rename!(hydcf, [:"*i"] .=> [:i]) - hydcf.i = lowercase.(hydcf.i) - # Subset to ReEDS model year - hydcf = filter(x -> x.t == data.year, hydcf) - - filepath_capadj = joinpath(data.ReEDSfilepath, "inputs_case", "hydcapadj.csv") - - hydcapadj = DataFrames.DataFrame(CSV.File(filepath_capadj)) - - # Rename plant types as techtypes and capacity are lowercase - DataFrames.rename!(hydcapadj, [:"*i"] .=> [:i]) - hydcapadj.i = lowercase.(hydcapadj.i) - - return hydcf, hydcapadj -end - -""" - Returns a DataFrame containing the installed storage energy capacity data - for the year specified in the ReEDSdatapaths object. - - Parameters - ---------- - data : ReEDSdatapaths - An object containing paths to ReEDS data. - - Returns - ------- - DataFrame - DataFrame containing storage energy capacity data - - Raises - ------ - Error - If the filepath for the specified year does not exist. -""" -function get_storage_energy_capacity_data(data::ReEDSdatapaths) - filepath = joinpath( - data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", - "energy_cap_$(string(data.year)).csv", - ) - return DataFrames.DataFrame(CSV.File(filepath)) -end - -""" - Returns a DataFrame containing the hourly planned outage data, - read from reeds2pras/test/reeds_cases/Pacific/inputs_case - - Parameters - ---------- - data : ReEDSdatapaths - An object containing paths to ReEDS data. - - Returns - ------- - DataFrame - DataFrame the hourly scheduled outages - - Raises - ------ - Error - If the filepath for the specified year does not exist. -""" -function get_hourly_scheduled_outage_data(data::ReEDSdatapaths) - filepath = joinpath( - data.ReEDSfilepath, - "inputs_case", - "outage_scheduled_hourly.h5", - ) - - return DataFrames.DataFrame( - HDF5.h5read(filepath, "data")', - HDF5.h5read(filepath, "columns"), - ) -end - -""" - Get the hourly forced outage data from the augur files. - - Parameters - ---------- - data : ReEDSdatapaths - Struct containing relevant datapaths and year from which to extract - the data. - - Returns - ------- - DataFrames.DataFrame - Dataframe containing the hourly forced outage data. -""" -function get_hourly_forced_outage_data(data::ReEDSdatapaths) - filepath = joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_hourly.h5") - forcedoutage_hourly = DataFrames.DataFrame( - HDF5.h5read(filepath, "data")', - HDF5.h5read(filepath, "columns"), - ) - return forcedoutage_hourly -end - -# Struct to define monthhour values -mutable struct monthhour - month::String - numhrs::Int64 - cumsum::Int64 - slice::UnitRange{Int64} - - # Inner Constructors & Checks - function monthhour( - month = "month", - numhrs = 10, - cumsum = 0, - slice = range(1, length = 10), - ) - return new(month, numhrs, cumsum, slice) - end -end - -# Functions to augment collection of monthour -function cumsum!(collection::Vector{monthhour}) - sum = 0 - for element in collection - sum = sum + element.numhrs - element.cumsum = sum - end -end - -function addslices!(collection::Vector{monthhour}) - for element in collection - element.slice = (element.cumsum - element.numhrs + 1):(element.cumsum) - end -end - -# Generating necessary data -function monhours() - monthours = monthhour[] - start_date = Dates.Date("2021-01", "yyyy-mm") - for i in range(0, length = 12) - new_date = start_date + Dates.Month(i) - push!( - monthours, - monthhour( - uppercase(Dates.monthabbr(i + 1)), - Dates.daysinmonth(new_date) * 24, - ), - ) - end - - cumsum!(monthours) - addslices!(monthours) - - return monthours -end diff --git a/reeds2pras/src/utils/runchecks.jl b/reeds2pras/src/utils/runchecks.jl deleted file mode 100644 index 9a5a88a6..00000000 --- a/reeds2pras/src/utils/runchecks.jl +++ /dev/null @@ -1,47 +0,0 @@ -# Check if you can open a file -function check_file(loc::String) - io = try - open(loc) - catch - nothing - end - - if (isnothing(io)) - return nothing, false - else - return io, isopen(io) - end -end - -function run_checks(data::ReEDSdatapaths) - augur_data_path = joinpath(data.ReEDSfilepath, "ReEDS_Augur", "augur_data") - filepaths = [ - joinpath(augur_data_path, "cap_converter_$(string(data.year)).csv"), - joinpath(augur_data_path, "charge_eff_$(string(data.year)).csv"), - joinpath(augur_data_path, "discharge_eff_$(string(data.year)).csv"), - joinpath(augur_data_path, "energy_cap_$(string(data.year)).csv"), - joinpath(augur_data_path, "max_cap_$(string(data.year)).csv"), - joinpath(augur_data_path, "max_unitsize_$(string(data.year)).csv"), - joinpath(augur_data_path, "pras_load_$(string(data.year)).h5"), - joinpath(augur_data_path, "pras_vre_gen_$(string(data.year)).h5"), - joinpath(augur_data_path, "tran_cap_$(string(data.year)).csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "hydcapadj.csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "hydcf.csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "mttr.csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_hourly.h5"), - joinpath(data.ReEDSfilepath, "inputs_case", "outage_forced_static.csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "outage_scheduled_hourly.h5"), - joinpath(data.ReEDSfilepath, "inputs_case", "resources.csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "tech-subset-table.csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "unitdata.csv"), - joinpath(data.ReEDSfilepath, "inputs_case", "unitsize.csv"), - ] - for filepath in filepaths - io, file_exists = check_file(filepath) - if (file_exists) - close(io) - else - error("Missing required file for ReEDS2PRAS: $filepath") - end - end -end diff --git a/reeds2pras/test/Project.toml b/reeds2pras/test/Project.toml deleted file mode 100644 index e62453f6..00000000 --- a/reeds2pras/test/Project.toml +++ /dev/null @@ -1,8 +0,0 @@ -[deps] -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -PRAS = "05348d26-1c52-11e9-35e3-9d51842d34b9" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - - diff --git a/reeds2pras/test/runtests.jl b/reeds2pras/test/runtests.jl deleted file mode 100644 index 6a6f505a..00000000 --- a/reeds2pras/test/runtests.jl +++ /dev/null @@ -1,38 +0,0 @@ -using ReEDS2PRAS -using BenchmarkTools -using Aqua -using DataFrames -using PRAS -using Test - -const R2P = ReEDS2PRAS -include("utils.jl") - -@testset verbose = true "Aqua.jl" begin - Aqua.test_unbound_args(ReEDS2PRAS) - Aqua.test_undefined_exports(ReEDS2PRAS) - Aqua.test_ambiguities(ReEDS2PRAS) - Aqua.test_stale_deps(ReEDS2PRAS) - Aqua.test_deps_compat(ReEDS2PRAS) -end - -#= -Don't add your tests to runtests.jl. Instead, create files named - - test-title-for-my-test.jl - -The file will be automatically included inside a `@testset` with title "Title For My Test". -=# -@testset verbose = true "ReEDS2PRAS tests" begin - for (root, dirs, files) in walkdir(@__DIR__) - for file in files - if isnothing(match(r"^test.*\.jl$", file)) - continue - end - title = titlecase(replace(splitext(file[6:end])[1], "-" => " ")) - @testset verbose = true "$title" begin - include(file) - end - end - end -end \ No newline at end of file diff --git a/reeds2pras/test/test-ReEDS2PRAS.jl b/reeds2pras/test/test-ReEDS2PRAS.jl deleted file mode 100644 index b346723d..00000000 --- a/reeds2pras/test/test-ReEDS2PRAS.jl +++ /dev/null @@ -1,71 +0,0 @@ -reedscase = joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035") -solve_year = 2035 -timesteps = 8760 -weather_year = 2007 - -# ReEDS2PRAS System Generation with no kwargs -pras_sys_1 = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = false, scheduled_outage = false); -path = R2P.ReEDSdatapaths(reedscase, solve_year) - -@testset verbose = true "SystemModel" begin - @test pras_sys_1 isa PRAS.SystemModel - - @testset "Load" begin - # Load - @test check_region_load_data(pras_sys_1) - end - - @testset "Lines" begin - # VSC Lines - @test check_DC_region_in_pras_system(pras_sys_1, path) - @test check_converter_capacity(pras_sys_1, path) - - # Other Lines - @test check_line_capacities(pras_sys_1, path) - end - @testset "Resource Capacity" begin - # Generators - @test check_generator_capacities(pras_sys_1, path) - @test check_storage_capacities(pras_sys_1, path) - end - - @testset "Transition Probabilities" begin - # Generators - @test check_generator_outage_probabilities(pras_sys_1, path, weather_year, timesteps) - - # Storages - @test check_storage_outage_probabilities(pras_sys_1, path, weather_year, timesteps) - @test check_storage_recovery_probabilities(pras_sys_1) - end - -end - -# ReEDS2PRAS System Generation with only scheduled outages -pras_sys_2 = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = false, scheduled_outage = true); - -@testset verbose = true "SystemModel-ScheduledOutage" begin - @testset "Generator Capacities" begin - @test check_scheduled_outage_generator_capacities(pras_sys_1, pras_sys_2) - end - - @testset "Storage Energy Capacities" begin - @test check_scheduled_outage_storage_capacities(pras_sys_1, pras_sys_2) - end - -end - -# ReEDS2PRAS System Generation with only hydro energy limits -pras_sys_3 = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = true, scheduled_outage = false); - -@testset verbose = true "SystemModel-HydroEnergyLimits" begin - @test length(pras_sys_3.generatorstorages.names) > 0 - @test length(pras_sys_3.generators.names) < length(pras_sys_1.generators.names) # Because we don't disaggregate and we have GeneratorStorages - - @testset "Inflow" begin - @test check_hydro_energy_limits(pras_sys_3, path) - end - @testset "Outage Probability" begin - @test check_generatorstorage_outage_probabilities(pras_sys_3, path, weather_year, timesteps) - end - #TODO : Should we check others (discharge_capacity, etc.?)? -end \ No newline at end of file diff --git a/reeds2pras/test/test-benchmark.jl b/reeds2pras/test/test-benchmark.jl deleted file mode 100644 index 8d72d42f..00000000 --- a/reeds2pras/test/test-benchmark.jl +++ /dev/null @@ -1,35 +0,0 @@ -@testset verbose = true "ReEDS2PRAS & PRAS Benchmark" begin - # Running this benchmark: - # Run this file first with the main branch and - # then the feature branch, record the reported mean time taken - # for both ReEDS2PRAS and PRAS, and the CONUS LOLE, nEUE output here - # while submitting pull request with the PR template - - # If making major changes to R2P model, increase number of MC samples used - - # Set up this test - reedscase = joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035") - solve_year = 2035 - timesteps = 8760 - weather_year = 2007 - samples = 10 - seed = 1 - - # ReEDS2PRAS Benchmarking - bm_r2p = @btimed R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = true, scheduled_outage = true) setup = (reedscase=joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035"); solve_year=2035; timesteps = 8760; weather_year = 2007); - - # PRAS Benchmarking - bm_pras = @btimed assess(pras_sys, simulation, Shortfall()) setup = (pras_sys=R2P.reeds_to_pras(joinpath(@__DIR__, "reeds_cases", "USA_VSC_2035"), 2035, 8760, 2007, hydro_energylim = true, scheduled_outage = true); simulation = SequentialMonteCarlo(samples = 10, seed = 1)); - - # Print Results - pras_sys = R2P.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, hydro_energylim = true, scheduled_outage = true); - simulation = SequentialMonteCarlo(samples = samples, seed = seed) - shortfall = assess(pras_sys, simulation, Shortfall()) - - LOLE = PRAS.LOLE(shortfall[1]).lole.estimate - EUE = PRAS.EUE(shortfall[1]).eue.estimate - nEUE = PRAS.NEUE(shortfall[1]).neue.estimate - - @show "ReEDS2PRAS Benchmark Time : time - $(bm_r2p.time), gctime - $(bm_r2p.gctime); PRAS Benchmark Time : time - $(bm_pras.time), gctime - $(bm_pras.gctime), LOLE: $(LOLE), EUE: $(EUE), NEUE :$(nEUE)" - -end \ No newline at end of file diff --git a/reeds2pras/test/utils.jl b/reeds2pras/test/utils.jl deleted file mode 100644 index 1732d32e..00000000 --- a/reeds2pras/test/utils.jl +++ /dev/null @@ -1,393 +0,0 @@ -function check_generator_capacities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) - # Currently doesn't check for weather_year VG profiles - # Just ensures 0 <= VG capacity time series <= Installed Capacity - # In some regions, distpv fails this tests, so skipping that category for now - capacity_data = R2P.get_ICAP_data(path) - vg_resource_types = R2P.get_valid_resources(path) - tech_list = R2P.get_technology_types(path) - vg_types = unique(vg_resource_types.i) - storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) - reg_cap = filter(x -> x.r == reg_name, capacity_data) - vg_counts = length(filter(x -> x ∈ vg_types, unique(reg_cap.i))) - vg_counts = ("distpv" in unique(reg_cap.i)) ? vg_counts - 1 : vg_counts # because some regions fail the distpv test - non_vg_counts = length(filter(x -> x ∉ union(vg_types,storage_types), unique(reg_cap.i))) - - reg_non_vg_count = 0 - reg_vg_count = 0 - for gen_cat in filter(x -> x ∉ storage_types, unique(reg_cap.i)) - pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.generators.categories[pras_sys.region_gen_idxs[reg_idx]]) - reeds_gen_cat_data = filter(x -> x.i == gen_cat, reg_cap) - if !(gen_cat in vg_types) - if (isapprox(sum(pras_sys.generators.capacity[pras_sys.region_gen_idxs[reg_idx]][pras_gen_cat_idx]), round(Int, sum(reeds_gen_cat_data.MW)), atol=1)) - reg_non_vg_count = reg_non_vg_count + 1 - end - else - if (gen_cat != "distpv") # because some regions fail the distpv test - if (all(0 .<= pras_sys.generators.capacity[pras_sys.region_gen_idxs[reg_idx],:][pras_gen_cat_idx,:] .<= round(Int, sum(reeds_gen_cat_data.MW)))) - reg_vg_count = reg_vg_count + 1 - end - end - end - end - - if ((reg_non_vg_count == non_vg_counts) && (reg_vg_count == vg_counts)) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys.regions.names)) - return true - else - return false - end -end - -function check_storage_capacities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) - capacity_data = R2P.get_ICAP_data(path) - tech_list = R2P.get_technology_types(path) - storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) - - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) - reg_cap = filter(x -> x.r == reg_name, capacity_data) - stor_cats = filter(x -> x ∈ storage_types, unique(reg_cap.i)) - stor_counts = length(stor_cats) - - reg_stor_count = 0 - for stor_cat in stor_cats - pras_stor_cat_idx = findall(x -> x == stor_cat, pras_sys.storages.categories[pras_sys.region_stor_idxs[reg_idx]]) - reeds_stor_cat_data = filter(x -> x.i == stor_cat, reg_cap) - - if (sum(pras_sys.storages.charge_capacity[pras_sys.region_stor_idxs[reg_idx]][pras_stor_cat_idx]) == round(Int, sum(reeds_stor_cat_data.MW))) - reg_stor_count = reg_stor_count + 1 - end - end - - if (reg_stor_count == stor_counts) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys.regions.names)) - return true - else - return false - end -end - -function check_line_capacities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) - # The capacities of interfaces between AC & DC regions are checked in a different function - line_data = R2P.get_line_capacity_data(path) - non_vsc_line_data = filter(x -> x.trtype != "VSC", line_data) - vsc_line_data = filter(x -> x.trtype == "VSC", line_data) - - ac_interfaces_count = length(findall(.~occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_from]) .&& .~occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_to]))) - dc_interfaces_count = length(findall(occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_from]) .&& occursin.("DC",pras_sys.regions.names[pras_sys.interfaces.regions_to]))) - ac_int_count = 0 - dc_int_count = 0 - - for i in 1:length(pras_sys.interfaces) - region_from = pras_sys.regions.names[pras_sys.interfaces.regions_from[i]] - region_to = pras_sys.regions.names[pras_sys.interfaces.regions_to[i]] - cap_forward_pras = pras_sys.interfaces.limit_forward[i] - cap_backward_pras = pras_sys.interfaces.limit_backward[i] - - if !((occursin("DC", region_from) && occursin("DC", region_to)) || (occursin("DC", region_from) || occursin("DC", region_to))) - to_from_lines = filter(x -> x.r == region_from && x.rr == region_to, non_vsc_line_data) - from_to_lines = filter(x -> x.r == region_to && x.rr == region_from, non_vsc_line_data) - - cap_forward_reeds = round(Int, sum(to_from_lines.MW)) - cap_backward_reeds = round(Int, sum(from_to_lines.MW)) - - if ((cap_forward_pras == cap_forward_reeds) && (cap_backward_pras == cap_backward_reeds)) - ac_int_count = ac_int_count + 1 - end - else - if ((occursin("DC", region_from) && occursin("DC", region_to))) - ac_reg_from = last(split(region_from, "|")) - ac_reg_to = last(split(region_to, "|")) - to_from_lines = filter(x -> x.r == ac_reg_from && x.rr == ac_reg_to, vsc_line_data) - from_to_lines = filter(x -> x.r == ac_reg_to && x.rr == ac_reg_from, vsc_line_data) - - cap_forward_reeds = round(Int, sum(to_from_lines.MW)) - cap_backward_reeds = round(Int, sum(from_to_lines.MW)) - - if ((cap_forward_pras == cap_forward_reeds) && (cap_backward_pras == cap_backward_reeds)) - dc_int_count = dc_int_count + 1 - end - end - end - end - - if ((ac_int_count == ac_interfaces_count) && (dc_int_count == dc_interfaces_count)) - return true - else - return false - end -end - -function check_region_load_data(pras_sys::PRAS.SystemModel) - dc_reg_idx = findall(occursin.("DC", pras_sys.regions.names)) - dc_flag = all(iszero.(pras_sys.regions.load[dc_reg_idx,:])) - ac_reg_idx = findall(.!(occursin.("DC", pras_sys.regions.names))) - ac_flag = all(pras_sys.regions.load[ac_reg_idx,:] .> 0) - - return (dc_flag && ac_flag) ? true : false -end - -function check_DC_region_in_pras_system(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) - line_base_cap_data = R2P.get_line_capacity_data(path) - vsc_data = filter(x -> x.trtype == "VSC", line_base_cap_data) - vsc_regions = union(Set(vsc_data.r), Set(vsc_data.rr)) - dc_region_names = "DC|".*vsc_regions - - if all(in.(dc_region_names, Ref(pras_sys.regions.names))) - return true - else - return false - end -end - -function check_converter_capacity(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths) - cap_converter_data = R2P.get_converter_capacity_data(path) - vsc_region_names = unique(filter(x -> x.MW > 0.0, cap_converter_data)[!,"r"]) - cap_mws = unique(filter(x -> x.MW > 0.0, cap_converter_data)[!,"MW"]) - dc_region_names = "DC|".* vsc_region_names - count = 0 - for (reg_ac, reg_dc, cap_mw) in zip(vsc_region_names, dc_region_names, cap_mws) - reg_ac_idx = findfirst(pras_sys.regions.names .== reg_ac) - reg_dc_idx = findfirst(pras_sys.regions.names .== reg_dc) - - interface_idx = findfirst((pras_sys.interfaces.regions_from .== reg_ac_idx .&& pras_sys.interfaces.regions_to .== reg_dc_idx) .|| - (pras_sys.interfaces.regions_to .== reg_ac_idx .&& pras_sys.interfaces.regions_from .== reg_dc_idx)) - - if all(pras_sys.interfaces.limit_forward[interface_idx,:] .== round(Int,cap_mw)) - count+=1 - end - end - - if (count == length(vsc_region_names)) - return true - else - return false - end -end - -function check_scheduled_outage_generator_capacities(pras_sys_no_derate::PRAS.SystemModel, pras_sys_with_derate::PRAS.SystemModel) - - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys_no_derate.regions.names) - - reg_gen_count = 0 - reg_gen_cats = unique(pras_sys_no_derate.generators.categories[pras_sys_no_derate.region_gen_idxs[reg_idx]]) - for gen_cat in reg_gen_cats - pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys_no_derate.generators.categories[pras_sys_no_derate.region_gen_idxs[reg_idx]]) - pras_derate_reg_idx = findfirst(pras_sys_with_derate.regions.names .== reg_name) - pras_derate_gen_cat_idx = findall(x -> x == gen_cat, pras_sys_with_derate.generators.categories[pras_sys_with_derate.region_gen_idxs[pras_derate_reg_idx]]) - - if all(pras_sys_with_derate.generators.capacity[pras_sys_with_derate.region_gen_idxs[pras_derate_reg_idx],:][pras_derate_gen_cat_idx,:] .<= - pras_sys_no_derate.generators.capacity[pras_sys_no_derate.region_gen_idxs[reg_idx],:][pras_gen_cat_idx,:]) - reg_gen_count = reg_gen_count + 1 - end - - end - - if (reg_gen_count == length(reg_gen_cats)) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys_no_derate.regions.names)) - return true - else - return false - end -end - -function check_scheduled_outage_storage_capacities(pras_sys_no_derate::PRAS.SystemModel, pras_sys_with_derate::PRAS.SystemModel) - - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys_no_derate.regions.names) - - reg_stor_count = 0 - reg_stor_cats = unique(pras_sys_no_derate.storages.categories[pras_sys_no_derate.region_stor_idxs[reg_idx]]) - for stor_cat in reg_stor_cats - pras_stor_cat_idx = findall(x -> x == stor_cat, pras_sys_no_derate.storages.categories[pras_sys_no_derate.region_stor_idxs[reg_idx]]) - pras_derate_reg_idx = findfirst(pras_sys_with_derate.regions.names .== reg_name) - pras_derate_stor_cat_idx = findall(x -> x == stor_cat, pras_sys_with_derate.storages.categories[pras_sys_with_derate.region_stor_idxs[pras_derate_reg_idx]]) - - if all(pras_sys_with_derate.storages.energy_capacity[pras_sys_with_derate.region_stor_idxs[pras_derate_reg_idx],:][pras_derate_stor_cat_idx,:] .<= - pras_sys_no_derate.storages.energy_capacity[pras_sys_no_derate.region_stor_idxs[reg_idx],:][pras_stor_cat_idx,:]) - reg_stor_count = reg_stor_count + 1 - end - - end - - if (reg_stor_count == length(reg_stor_cats)) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys_no_derate.regions.names)) - return true - else - return false - end -end - -function check_hydro_energy_limits(pras_sys::PRAS.SystemModel,path::R2P.ReEDSdatapaths) - tech_list = R2P.get_technology_types(path) - hyd_disp_types = - lowercase.(DataFrames.dropmissing(tech_list, :HYDRO_D)[:, "Column1"]) - - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) - hyd_disp_cats = filter(x -> x ∈ hyd_disp_types, pras_sys.generatorstorages.categories[pras_sys.region_genstor_idxs[reg_idx]]) - hyd_disp_counts = length(hyd_disp_cats) - - reg_hyd_disp_count = 0 - for gen_cat in hyd_disp_cats - pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.generatorstorages.categories[pras_sys.region_genstor_idxs[reg_idx]]) - - if (length(unique(pras_sys.generatorstorages.inflow[pras_sys.region_genstor_idxs[reg_idx],:][pras_gen_cat_idx,:])) > 1 || - all(iszero.(pras_sys.generatorstorages.inflow[pras_sys.region_genstor_idxs[reg_idx],:][pras_gen_cat_idx,:]))) - # Need to do this becuase some regions don't have inflow data (p80 in particular) - reg_hyd_disp_count = reg_hyd_disp_count + 1 - end - end - - if (reg_hyd_disp_count == length(hyd_disp_cats)) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys.regions.names)) - return true - else - return false - end -end - -function check_generator_outage_probabilities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths, weather_year::Int, timesteps::Int) - capacity_data = R2P.get_ICAP_data(path) - tech_list = R2P.get_technology_types(path) - storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) - hyd_disp_types = lowercase.(DataFrames.dropmissing(tech_list, :HYDRO_D)[:, "Column1"]) - hyd_non_disp_types = lowercase.(DataFrames.dropmissing(tech_list, :HYDRO_ND)[:, "Column1"]) - for_hourly = R2P.get_hourly_forced_outage_data(path) - - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) - reg_cap = filter(x -> x.r == reg_name, capacity_data) - non_stor_cats = filter(x -> x ∉ storage_types, unique(reg_cap.i)) - - upgraded_non_stor_cats = String[] - for cat in non_stor_cats - push!(upgraded_non_stor_cats, cat) - end - cat_idx = findall((upgraded_non_stor_cats .* "|" .* reg_name) .∈ Ref(names(for_hourly))) - hourly_for_cats = non_stor_cats[cat_idx] - upgraded_hourly_for_cats = upgraded_non_stor_cats[cat_idx] - hourly_for_cat_counts = length(hourly_for_cats) - - reg_hourly_for_cat_count = 0 - for (gen_cat, upgraded_gen_cat) in zip(hourly_for_cats, upgraded_hourly_for_cats) - pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.generators.categories[pras_sys.region_gen_idxs[reg_idx]]) - reeds_unique_for_count = length(unique(for_hourly[!,upgraded_gen_cat*"|"* reg_name])) - if (length(unique(sum(pras_sys.generators.λ[pras_sys.region_gen_idxs[reg_idx],:][pras_gen_cat_idx,:], dims = 1))) == reeds_unique_for_count) - reg_hourly_for_cat_count = reg_hourly_for_cat_count + 1 - end - - end - - if (reg_hourly_for_cat_count == hourly_for_cat_counts) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys.regions.names)) - return true - else - return false - end -end - -function check_storage_outage_probabilities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths, weather_year::Int, timesteps::Int) - # This test doesn't account for that fact that FOR for batteries is accounted for in the energy capacity. - # but passes because λ assigned is 0.0 - capacity_data = R2P.get_ICAP_data(path) - tech_list = R2P.get_technology_types(path) - storage_types = unique(DataFrames.dropmissing(tech_list, :STORAGE_STANDALONE)[:, "Column1"]) - - for_hourly = R2P.get_hourly_forced_outage_data(path) - - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) - reg_cap = filter(x -> x.r == reg_name, capacity_data) - stor_cats = filter(x -> x ∈ storage_types, unique(reg_cap.i)) - - cat_idx = findall((stor_cats .* "|" .* reg_name) .∈ Ref(names(for_hourly))) - hourly_for_cats = stor_cats[cat_idx] - hourly_for_cat_counts = length(hourly_for_cats) - - reg_hourly_for_cat_count = 0 - for gen_cat in hourly_for_cats - pras_gen_cat_idx = findall(x -> x == gen_cat, pras_sys.storages.categories[pras_sys.region_stor_idxs[reg_idx]]) - reeds_unique_for_count = length(unique(for_hourly[!,gen_cat*"|"* reg_name])) - if (length(unique(sum(pras_sys.storages.λ[pras_sys.region_stor_idxs[reg_idx],:][pras_gen_cat_idx,:], dims = 1))) == reeds_unique_for_count) - reg_hourly_for_cat_count = reg_hourly_for_cat_count + 1 - end - - end - - if (reg_hourly_for_cat_count == hourly_for_cat_counts) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys.regions.names)) - return true - else - return false - end -end - -function check_storage_recovery_probabilities(pras_sys::PRAS.SystemModel) - # Just check storages MTTR is parsed as a test - - reg_count = 0 - for (reg_idx,reg_name) in enumerate(pras_sys.regions.names) - if (all(pras_sys.storages.μ[pras_sys.region_stor_idxs[reg_idx]] .== (1/24))) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys.regions.names)) - return true - else - return false - end -end - -function check_generatorstorage_outage_probabilities(pras_sys::PRAS.SystemModel, path::R2P.ReEDSdatapaths, weather_year::Int, timesteps::Int) - for_hourly = R2P.get_hourly_forced_outage_data(path) - - reg_count = 0 - for (reg_idx, reg_name) in enumerate(pras_sys.regions.names) - reg_genstor_idx = pras_sys.region_genstor_idxs[reg_idx] - reg_genstor_cats = pras_sys.generatorstorages.categories[reg_genstor_idx] - reg_genstor_cat_count = 0 - for genstor_cat in unique(reg_genstor_cats) - pras_genstor_cat_idx = findall(x -> x == genstor_cat, pras_sys.generatorstorages.categories[reg_genstor_idx]) - tech = genstor_cat - reeds_unique_for_count = length(unique(for_hourly[!,tech*"|"* reg_name])) - if (length(unique(sum(pras_sys.generatorstorages.λ[reg_genstor_idx,:][pras_genstor_cat_idx,:], dims = 1))) == reeds_unique_for_count) - reg_genstor_cat_count = reg_genstor_cat_count + 1 - end - - end - - if (reg_genstor_cat_count == length(unique(reg_genstor_cats))) - reg_count = reg_count + 1 - end - end - if (reg_count == length(pras_sys.regions.names)) - return true - else - return false - end -end diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 21614b6e..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -pre-commit>=3.1.1,<3.2 -pylint>=2.17.0,<2.18 -pytest>=7.2.2,<7.3 diff --git a/restart_runs.py b/restart_runs.py deleted file mode 100644 index dee9a85c..00000000 --- a/restart_runs.py +++ /dev/null @@ -1,180 +0,0 @@ -#%% Imports -import os -import shutil -import subprocess -import argparse -import pandas as pd -from glob import glob -from runbatch import submit_slurm_parallel_jobs -from runstatus import get_run_status - -#%% Argument inputs -parser = argparse.ArgumentParser(description='Restart failed runs on the HPC') -parser.add_argument('batch_name', type=str, help='batch name (case prefix) to search for') -parser.add_argument('--copy_cplex', '-c', type=int, default=0, - help='Which cplex.opt file to copy (or 0 for none)') -parser.add_argument('--copy_srun_template', '-s', action='store_true', - help='Copy current srun_template.sh to sbatch file') -parser.add_argument('--force', '-f', action='store_true', - help='Proceed without double-checking') -parser.add_argument('--more_copyfiles', '-m', type=str, default='', - help=',-delimited list of additional files to copy from reeds_path') -parser.add_argument('--include_finished', '-i', action='store_true', - help='Also restart finished runs (e.g. to redo postprocessing)') - -args = parser.parse_args() -batch_name = args.batch_name -copy_cplex = args.copy_cplex -copy_srun_template = args.copy_srun_template -force = args.force -more_copyfiles = [i for i in args.more_copyfiles.split(',') if len(i)] -include_finished = args.include_finished - -# #%% Inputs for debugging -# batch_name = 'v20231113_yamM0' -# copy_cplex = 1 -# copy_srun_template = True -# force = True -# more_copyfiles = ['e_report.gms'] -# include_finished = False - -###### Procedure -#%% Shared parameters -reeds_path = os.path.dirname(os.path.abspath(__file__)) -#%% Get all runs -dictruns = get_run_status(reeds_path, batch_name) - -runs_unfinished = dictruns['running'] + dictruns['failed'] -runs_failed = dictruns['failed'] -runs_running = dictruns['running'] - -### Take a look -print('unfinished:', len(runs_unfinished)) -print('running:', len(runs_running)) -print('failed:', len(runs_failed)) - -#%% Double check -if not force: - for i in runs_failed: - print(os.path.basename(i)) - print(f'Restarting the {len(runs_failed)} runs listed above.') - confirm_local = str(input('Proceed? [y]/n: ') or 'y') - if confirm_local not in ['y','Y','yes','Yes','YES']: - quit() - - -#%% Get the cplex file to copy -if copy_cplex: - if copy_cplex == 1: - cplex_file = os.path.join(reeds_path,'cplex.opt') - else: - cplex_file = os.path.join(reeds_path,f'cplex.op{copy_cplex}') -else: - cplex_file = None - -#%% Copy the header from the srun_template.sh file if desired -if copy_srun_template: - srun_template = os.path.join(reeds_path,'srun_template.sh') - writelines_srun = list() - with open(srun_template, 'r') as f: - for line in f: - writelines_srun.append(line.strip()) -else: - writelines_srun = list() - -#%%### Loop through runs, figure out when they failed, and restart -for case in runs_failed: - casename = os.path.basename(case) - - #%% Copy the cplex file if desired - if copy_cplex: - shutil.copy(cplex_file, os.path.join(case,'')) - - #%% Copy additional files if desired - for f in more_copyfiles: - shutil.copy(os.path.join(reeds_path,f), os.path.join(case,f)) - - #%% Make a backup copy of the original bash and sbatch scripts - callfile = os.path.join(case,f'call_{casename}.sh') - shutil.copy(callfile, os.path.join(case,f'ORIGINAL_call_{casename}.sh')) - - sbatchfile = os.path.join(case,f'{casename}.sh') - shutil.copy(sbatchfile, os.path.join(case,f'ORIGINAL_{casename}.sh')) - - #%% Get last .lst file and restart from there - lstfiles = sorted(glob(os.path.join(case,'lstfiles','*.lst'))) - if any([os.path.basename(i).startswith('report') for i in lstfiles]): - restart_tag = '# Output processing' - elif len(lstfiles) < 2: - # If there is only 1 lst file, then it is an environment.csv so the run failed during inputs processing - restart_tag = '# Input processing' - elif len(lstfiles) == 2: - # If there are only 2 lst files, then one of them will be environment.csv and the other will be 1_inputs.lst so the run failed during the model compilation - restart_tag = '# Compile model' - else: - # Drop environment and inputs .lst files - lstfiles = [l for l in lstfiles if ("environment.csv" not in l) and ('1_Inputs.lst' not in l)] - lastfile = lstfiles[-1] - restart_year = int(os.path.splitext(lastfile)[0].split('_')[-1].split('i')[0]) - restart_tag = f'# Year: {restart_year}' - - #%% Comment out the unnecessary lines - writelines = [] - with open(callfile, 'r') as f: - comment = 0 - for line in f: - ## Start commenting at input processing - if '# Input processing' in line: - comment = 1 - ## Stop commenting at restart_tag - if line.startswith(restart_tag): - comment = 0 - ## Record it - writelines.append(('# ' if comment else '') + line.strip()) - - ### Write it - with open(callfile, 'w') as f: - for line in writelines: - f.writelines(line + '\n') - -# Check if we are going to run this in parallel or not -hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False -if hpc and len(runs_failed) > 1: - # On HPC with multiple cases - cases_per_node = int(input('Number of simultaneous runs per node [integer]: ')) -else: - cases_per_node = 1 - -if hpc and (cases_per_node > 1): - # Write the slurm scripts for parallel runs and - # submit them to the HPC - casenames = [os.path.basename(p).split(batch_name + "_", 1)[-1] for p in runs_failed] - submit_slurm_parallel_jobs( - reeds_path=reeds_path, - BatchName=batch_name, - casenames=casenames, - cases_per_node=cases_per_node, - ) - -else: - # Run each case individually - for case in runs_failed: - casename = os.path.basename(case) - callfile = os.path.join(case, f'call_{casename}.sh') - sbatchfile = os.path.join(case, f'{casename}.sh') - # It is a single case or we are not on HPC - if copy_srun_template: - writelines_srun_case = writelines_srun.copy() - writelines_srun_case.append(f"\n#SBATCH --job-name={casename}\n") - writelines_srun_case.append(f"sh {callfile}") - with open(sbatchfile, 'w') as f: - for line in writelines_srun_case: - f.writelines(line + '\n') - - #%% Run it - sbatch = f'sbatch {sbatchfile}' - sbatchout = subprocess.run(sbatch, capture_output=True, shell=True) - - if len(sbatchout.stderr): - print(sbatchout.stderr.decode()) - print(f"{casename}: {sbatchout.stdout.decode()}") diff --git a/run_pcm.py b/run_pcm.py deleted file mode 100644 index a7b58ff0..00000000 --- a/run_pcm.py +++ /dev/null @@ -1,393 +0,0 @@ -# %% Imports -import os -import sys -import subprocess -import argparse -import json -from glob import glob -import gdxpds -import pandas as pd - -## Local imports -import reeds -import e_report_dump -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'input_processing'))) -import hourly_repperiods -import hourly_writetimeseries - - -# %% Inferred inputs -reeds_path = os.path.dirname(__file__) - -# %% Default inputs -switch_mods_default = { - 'GSw_HourlyClusterAlgorithm': 'hierarchical', - 'GSw_HourlyNumClusters': 365, - 'GSw_HourlyType': 'day', - 'GSw_HourlyChunkLengthRep': 1, - 'GSw_HourlyChunkLengthStress': 1, - 'GSw_HourlyChunkAggMethod': 1, - 'GSw_PRM_CapCredit': 0, -} - - -# %% Functions -def check_slurm(forcelocal=False): - """Check whether to submit slurm jobs (if on HPC) or run locally""" - hpc = ( - False - if forcelocal - else ( - True - if int(os.environ.get('REEDS_USE_SLURM', 0)) and ('NREL_CLUSTER' in os.environ) - else False - ) - ) - return hpc - - -def solvestring_pcm( - batch_case, - sw, - t, - restartfile, - iteration=0, - hpc=0, - stress_year=0, - label='', -): - """ - Typical inputs: - * restartfile: batch_case if first solve year else {batch_case}_{prev_year} - * sw: loaded from {batch_case}/inputs_case/switches.csv - """ - savefile = f"pcm_{label}_{batch_case}_{t}i{iteration}" - _stress_year = f"{t}i{iteration}" if stress_year in ['keep', 'default'] else stress_year - out = ( - "gams d_solvepcm.gms" - + (" license=gamslice.txt" if hpc else '') - + f" o={os.path.join('lstfiles', f'{savefile}.lst')}" - + f" r={os.path.join('g00files', restartfile)}" - + " gdxcompress=1" - + f" xs={os.path.join('g00files', savefile)}" - + ' logOption=4 appendLog=1' - + f" logFile=gamslog_pcm_{label}_{t}.txt" - + f" --case={batch_case}" - + f" --cur_year={t}" - + f" --stress_year={stress_year}" - + f" --temporal_inputs=pcm_{label}" - + ''.join( - [ - f" --{s}={sw[s]}" - for s in [ - 'GSw_SkipAugurYear', - 'GSw_HourlyType', - 'GSw_HourlyWrapLevel', - 'GSw_ClimateWater', - 'GSw_Canada', - 'GSw_ClimateHydro', - 'GSw_HourlyChunkLengthRep', - 'GSw_HourlyChunkLengthStress', - 'GSw_StateCO2ImportLevel', - 'GSw_PVB_Dur', - 'GSw_ValStr', - 'GSw_gopt', - 'solver', - 'debug', - 'startyear', - ] - ] - ) - + '\n' - ) - - return out - - -def pcm_report_string(batch_case, sw, t, iteration=0, hpc=0, label=''): - savefile = f"pcm_{label}_{batch_case}_{t}i{iteration}" - out = ( - "gams e_report.gms" - + (' license=gamslice.txt' if hpc else '') - + f" o={os.path.join('lstfiles', f'report_pcm_{label}_{t}_{batch_case}.lst')}" - + f" r={os.path.join('g00files', savefile)}" - + ' gdxcompress=1' - + ' logOption=4 appendLog=1' - + f" logFile=gamslog_pcm_{label}_{t}.txt" - + f" --fname=pcm_{label}_{t}_{batch_case}" - + " --GSw_calc_powfrac=0 \n" - ) - return out - - -def submit_job(casepath, command_string, jobname='', joblabel='', bigmem=0): - """ - Create a slurm job submission script for `command_string` at `casepath`, - then submit it. - Uses the slurm settings from {reeds_path}/srun_template.sh. - """ - ### Get the SLURM boilerplate - commands_header, commands_sbatch, commands_other = [], [], [] - with open(os.path.join(reeds_path, 'srun_template.sh'), 'r') as f: - for line in f: - if bigmem and ('--mem=' in line): - line = '#SBATCH --mem=500000' - - if line.strip().startswith('#!'): - commands_header.append(line.strip()) - elif line.strip().startswith('#SBATCH'): - commands_sbatch.append(line.strip()) - else: - commands_other.append(line.strip()) - - ### Add the command for this run - slurm = ( - commands_header - + commands_sbatch - + ([f"#SBATCH --job-name={jobname}"] if len(jobname) else []) - + [f"#SBATCH --output={os.path.join(casepath, 'slurm-%j.out')}"] - + commands_other - + [''] - + [command_string] - ) - ### Write the SLURM command - callfile = os.path.join(casepath, f'submit_{joblabel}.sh') - with open(callfile, 'w+') as f: - for line in slurm: - f.writelines(line + '\n') - ### Submit the job - batchcom = f'sbatch {callfile}' - subprocess.Popen(batchcom.split()) - - -# %% -def main(casepath, t, switch_mods=switch_mods_default, label='', overwrite=False): - """ - Args: - kwargs: Passed to hourly_reppreiods.main() - """ - # %% Switch to run folder - os.chdir(casepath) - - # %% Get the run settings - sw = reeds.io.get_switches(casepath) - years = ( - pd.read_csv(os.path.join(casepath, 'inputs_case', 'modeledyears.csv')) - .columns.astype(int) - .values - ) - _t = t if t > 0 else max(years) - - # %% Set up logger - reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(casepath, f'gamslog_pcm_{label}_{_t}.txt'), - ) - - # %% Get and modify the switch settings - sw_pcm = sw.copy() - for key, val in switch_mods.items(): - sw_pcm[key] = val - - # %% Write the inputs for PCM - if (not os.path.isdir(os.path.join(casepath, 'inputs_case', f'pcm_{label}'))) or overwrite: - hourly_repperiods.main( - sw=sw_pcm, - reeds_path=reeds_path, - inputs_case=os.path.join(casepath, 'inputs_case'), - periodtype=f'pcm_{label}', - minimal=1, - ) - hourly_writetimeseries.main( - sw=sw_pcm, - reeds_path=reeds_path, - inputs_case=os.path.join(casepath, 'inputs_case'), - periodtype=f'pcm_{label}', - make_plots=0, - ) - ## Write a set of empty "stress0" inputs to turn off stress periods for PCM - stresspath = os.path.join(casepath, 'inputs_case', 'stress0') - if (not os.path.isdir(stresspath)) or overwrite: - os.makedirs(stresspath, exist_ok=True) - pd.DataFrame(columns=['rep_period', 'year', 'yperiod', 'actual_period']).to_csv( - os.path.join(stresspath, 'period_szn.csv'), - index=False, - ) - hourly_writetimeseries.main( - sw=sw_pcm, - reeds_path=reeds_path, - inputs_case=os.path.join(casepath, 'inputs_case'), - periodtype='stress0', - make_plots=0, - ) - - # %% Get ReEDS LP for specified year - batch_case = os.path.basename(casepath) - ### Get the restartfile and get the last year/iteration if t=0 and iteration='last' - if _t == min(years): - restartfile = batch_case - _iteration = 0 - elif iteration == 'last': - restartfile = sorted(glob(os.path.join(casepath, 'g00files', f"{batch_case}_{_t}i*")))[-1] - _iteration = int(restartfile[: -len('.g00')].split('i')[-1]) - else: - _iteration = iteration - restartfile = os.path.join(casepath, 'g00files', f"{batch_case}_{_t}i{_iteration}.g00") - - cmd_gams = solvestring_pcm( - batch_case=batch_case, - sw=sw_pcm, - t=_t, - restartfile=restartfile, - iteration=_iteration, - hpc=int(sw['hpc']), - label=label, - ) - print(cmd_gams) - - ### Run GAMS LP - result = subprocess.run(cmd_gams, shell=True) - if result.returncode: - raise Exception(f'd_solvepcm.gms failed with return code {result.returncode}') - - # %% Dump results to gdx - cmd_report = pcm_report_string( - batch_case=batch_case, - sw=sw_pcm, - t=_t, - iteration=_iteration, - hpc=int(sw['hpc']), - label=label, - ) - print(cmd_report) - - result = subprocess.run(cmd_report, shell=True) - if result.returncode: - raise Exception(f'e_report.gms failed with return code {result.returncode}') - - # %% Dump gdx to h5 - ## Get new file names if applicable - dfparams = pd.read_csv( - os.path.join(casepath, "e_report_params.csv"), - comment="#", - index_col="param", - ) - rename = dfparams.loc[~dfparams.output_rename.isnull(), "output_rename"].to_dict() - rename = {k.split("(")[0]: v for k, v in rename.items()} - print(f"renamed parameters: {rename}") - - print("Loading outputs gdx") - dict_out = gdxpds.to_dataframes( - os.path.join(casepath, 'outputs', f"rep_pcm_{label}_{_t}_{batch_case}.gdx") - ) - print("Finished loading outputs gdx") - - outputs_path = os.path.join(casepath, 'outputs', f'pcm_{label}_{_t}') - os.makedirs(outputs_path, exist_ok=True) - e_report_dump.write_dfdict( - dfdict=dict_out, - outputs_path=outputs_path, - rename=rename, - ) - - -# %% Procedure -if __name__ == '__main__': - # %% Argument inputs - parser = argparse.ArgumentParser( - description='Run ReEDS in PCM mode', - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument('casepath', type=str, help='ReEDS-2.0/runs/{case} directory') - parser.add_argument( - '--year', - '-t', - type=int, - default=0, - help='Year to run (must have the corresponding .g00 file), or 0 for last year', - ) - parser.add_argument( - '--iteration', - '-i', - type=str, - default='last', - help="Iteration to run, or 'last' for last iteration", - ) - parser.add_argument( - '--switch_mods', - '-s', - type=json.loads, - default=json.dumps(switch_mods_default), - help=( - 'Dictionary-formated string of switch arguments for PCM. ' - 'Use single quotes outside the dictionary and double quotes for keys, as in:\n' - '`-s \'{"GSw_HourlyChunkLengthRep":4}\'`' - ), - ) - parser.add_argument('--label', '-l', type=str, default='', help='Label for PCM outputs') - parser.add_argument( - '--overwrite', - '-o', - action='store_true', - help="Overwrite input files if they already exist (otherwise don't rewrite them)", - ) - parser.add_argument( - '--forcelocal', - '-f', - action='store_true', - help='Run locally (including on a compute node as part of a job)', - ) - parser.add_argument('--bigmem', '-b', action='store_true', help='Use bigmem node') - - args = parser.parse_args() - casepath = args.casepath - t = args.year - iteration = args.iteration - switch_mods = args.switch_mods - label = args.label - if not len(label): - label = f"{switch_mods['GSw_HourlyType'][0]}{switch_mods['GSw_HourlyChunkLengthRep']}h" - overwrite = args.overwrite - forcelocal = args.forcelocal - bigmem = args.bigmem - - # #%% Inputs for debugging - # casepath = os.path.join(reeds_path, 'runs', 'v20250206_pcmM0_Pacific') - # t = 0 - # iteration = 'last' - # switch_mods = switch_mods_default - # switch_mods = { - # 'GSw_HourlyClusterAlgorithm': 'hierarchical', - # 'GSw_HourlyType': 'day', 'GSw_HourlyNumClusters': 365, - # # 'GSw_HourlyType': 'wek', 'GSw_HourlyNumClusters': 73, - # 'GSw_HourlyChunkLengthRep': 4, - # } - # label = f"{switch_mods['GSw_HourlyType'][0]}{switch_mods['GSw_HourlyChunkLengthRep']}h" - # forcelocal = False - # overwrite = False - # bigmem = True - - # %% Determine whether to submit slurm job - hpc = check_slurm(forcelocal=forcelocal) - - ### Run it - if not hpc: - main(casepath=casepath, t=t, switch_mods=switch_mods, label=label, overwrite=overwrite) - else: - command_string = ( - f"python run_pcm.py {casepath} " - f"--year={t} " - f"--iteration={iteration} " - f"--switch_mods='{json.dumps(switch_mods)}' " - f"--label={label} " - "--forcelocal " - ) + ("--overwrite " if overwrite else "") - joblabel = f"pcm_{label}_{t}" - jobname = f"{os.path.basename(casepath)}-{joblabel}" - submit_job( - casepath=casepath, - command_string=command_string, - jobname=jobname, - joblabel=joblabel, - bigmem=bigmem, - ) diff --git a/runbatch.py b/runbatch.py deleted file mode 100644 index 33b32979..00000000 --- a/runbatch.py +++ /dev/null @@ -1,1864 +0,0 @@ -#%% =========================================================================== -### --- IMPORTS --- -### =========================================================================== - -import os -import git -import queue -import threading -import time -import shutil -import csv -import importlib -import numpy as np -import pandas as pd -import subprocess -import re -from datetime import datetime -import argparse -from pathlib import Path -import reeds - -# Assert core programs are accessible -CORE_PROGRAMS = ["gams"] -if not all(shutil.which(program) for program in CORE_PROGRAMS): - msg = ( - "Programs needed to run reeds not accessible on the environment. " - f"Check that all the {CORE_PROGRAMS=} are accessible on the PATH." - ) - raise ImportError(msg) - -#%% Constants -LINUXORMAC = True if os.name == 'posix' else False -ext = '.sh' if LINUXORMAC else '.bat' - -YAMPASERVERS = ['constellation01','cepheus','corvus','dorado','delphinus'] - -#%% =========================================================================== -### --- FUNCTIONS --- -### =========================================================================== - -def writeerrorcheck(checkfile, errorcode=17): - """ - Inputs - ------ - checkfile: Filename to check. If it does not exist, stop the run. - errorcode: Value to return if check fails. Should be >0. - """ - if LINUXORMAC: - return f'if [ ! -f {checkfile} ]; then echo "missing {checkfile}"; exit {errorcode}; fi\n' - else: - return f'\nif not exist {checkfile} (\n echo file {checkfile} missing \n goto:eof \n) \n \n' - -def writescripterrorcheck(script, errorcode=18): - """ - """ - if LINUXORMAC: - return f'if [ $? != 0 ]; then echo "{script} returned $?" >> gamslog.txt; exit {errorcode}; fi\n' - else: - return f'if not %errorlevel% == 0 (echo {script} returned %errorlevel%\ngoto:eof\n)\n' - - -def write_delete_file(checkfile, deletefile, PATH): - if LINUXORMAC: - PATH.writelines(f"if [ -f {checkfile} ]; then rm {deletefile}; fi\n") - else: - PATH.writelines("if exist " + checkfile + " (del " + deletefile + ')\n' ) - - -def comment(text, PATH): - commentchar = '#' if LINUXORMAC else '::' - PATH.writelines(f'{commentchar} {text}\n') - - -def big_comment(text, PATH): - commentchar = '#' if LINUXORMAC else '::' - PATH.writelines(f'\n{commentchar}\n') - comment(text, PATH) - PATH.writelines(f'{commentchar}\n') - - -def create_case_lists(df_cases:pd.DataFrame, BatchName:str, single:str=''): - """ - """ - # Initiate the empty lists which will be filled with info from cases - # Needs to be done after the MCS runs are processed, so that the case names are correct - caseList = [] - caseSwitches = [] #list of dicts, one dict for each case - # Redefine casenames to include all the Monte Carlo cases, which have been expanded in df_cases. - casenames = list(df_cases.columns) - - for case in casenames: - # If --single/-s was passed, only keep those cases (regardless of ignore) - # otherwise, drop any case marked ignore - if single: - if case not in single.split(','): - continue - else: - if int(df_cases.loc['ignore', case]) == 1: - continue - # Add switch settings to list of options passed to GAMS - shcom = f' --case={BatchName}_{case}' - for i,v in df_cases[case].items(): - #exclude certain switches that don't need to be passed to GAMS - if i not in ['file_replacements','keep_run_terminal']: - shcom += f' --{i}={v}' - caseList.append(shcom) - caseSwitches.append(df_cases[case].to_dict()) - - return caseSwitches, casenames, caseList - - -def get_ivt_numclass(reeds_path, casedir, caseSwitches): - """ - Extend ivt if necessary and calculate numclass - """ - ivt = pd.read_csv( - os.path.join( - reeds_path, 'inputs', 'userinput', 'ivt_{}.csv'.format(caseSwitches['ivt_suffix'])), - index_col=0) - ivt_step = pd.read_csv(os.path.join(reeds_path, 'inputs', 'userinput', 'ivt_step.csv'), - index_col=0).squeeze(1) - lastdatayear = max([int(c) for c in ivt.columns]) - addyears = list(range(lastdatayear + 1, int(caseSwitches['endyear']) + 1)) - num_added_years = len(addyears) - ### Add v for the extra years - ivt_add = {} - for i in ivt.index: - vlast = ivt.loc[i,str(lastdatayear)] - if ivt_step[i] == 0: - ### Use the same v forever - ivt_add[i] = [vlast] * num_added_years - else: - ### Use the same spacing forever - forever = [[vlast + 1 + x] * ivt_step[i] for x in range(1000)] - forever = [item for sublist in forever for item in sublist] - ivt_add[i] = forever[:num_added_years] - ivt_add = pd.DataFrame(ivt_add, index=addyears).T - ### Concat and resave - ivtout = pd.concat([ivt, ivt_add], axis=1) - ivtout.to_csv(os.path.join(casedir, 'inputs_case', 'ivt.csv')) - ### Get numclass, which is used in b_inputs.gms - numclass = ivtout.max().max() - - return numclass - - -def get_rev_paths(revswitches, caseSwitches): - # Expand on reV path based on where this run is happening - # when running on the HPC this links to the shared-projects folder - hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False - if os.environ.get('NREL_CLUSTER') == 'kestrel': - hpc_path = '/kfs2/shared-projects/reeds/Supply_Curve_Data' - else: - hpc_path = '/shared-projects/reeds/Supply_Curve_Data' - - if hpc: - rev_prefix = hpc_path - else: - hostname = os.environ.get('HOSTNAME') - if (hostname) and (hostname.split('.')[0] in YAMPASERVERS): - drive = '/data/shared/shared_data' - elif LINUXORMAC: - drive = '/Volumes' - else: - drive = '//nrelnas01' - rev_prefix = os.path.join(drive,'ReEDS','Supply_Curve_Data') - revswitches['hpc_sc_path'] = revswitches['sc_path'].apply(lambda row: os.path.join(hpc_path,row)) - revswitches['sc_path'] = revswitches['sc_path'].apply(lambda row: os.path.join(rev_prefix,row)) - revswitches['rev_path'] = revswitches.apply(lambda row: os.path.join(row.sc_path, "reV", row.rev_case), axis=1) - - # link to the pre-processed reV supply curves from hourlize - def get_rev_sc_file_name(caseSwitches, rev_row, use_hpc=False): - if pd.isnull(rev_row.original_sc_file): - return "" - else: - if caseSwitches['GSw_RegionResolution'] == "county": - sc_folder_suffix = "_county" - else: - sc_folder_suffix = "_ba" - - # link to HPC or other sc_path - if use_hpc: - sc_path = rev_row.hpc_sc_path - else: - sc_path = rev_row.sc_path - - # supply curve name should be in format of {tech}_rev_supply_curves_raw.csv - # in the hourlize results folder (must match format in 'save_sc_outputs' function of hourlize/resource.py) - sc_file = os.path.join(sc_path, - rev_row.tech + "_" + rev_row.access_case + sc_folder_suffix, - "results", - rev_row.tech + "_supply_curve_raw.csv" - ) - return sc_file - revswitches['sc_file'] = revswitches.apply(lambda row: get_rev_sc_file_name(caseSwitches, row), axis=1) - revswitches['hpc_sc_file'] = revswitches.apply(lambda row: get_rev_sc_file_name(caseSwitches, row, use_hpc=True), axis=1) - - return revswitches - -def check_compatibility(sw): - if int(sw['startyear']) < 2010: - raise ValueError(f"startyear = {sw['startyear']} but must be ≥ 2010") - - if (sw['GSw_HourlyType'] in ['year']) and int(sw['GSw_InterDayLinkage']): - raise ValueError( - "GSw_HourlyType cannot be 'year' when GSw_InterDayLinkage is enabled. " - f"Current values: GSw_HourlyType={sw['GSw_HourlyType']}, GSw_InterDayLinkage={sw['GSw_InterDayLinkage']}" - ) - - if 24 % (int(sw['GSw_HourlyWindowOverlap']) * int(sw['GSw_HourlyChunkLengthRep'])): - raise ValueError( - ('24 must be divisible by GSw_HourlyWindowOverlap * GSw_HourlyChunkLengthRep:' - '\nGSw_HourlyWindowOverlap = {}\nGSw_HourlyChunkLengthRep = {}'.format( - sw['GSw_HourlyWindowOverlap'], sw['GSw_HourlyChunkLengthRep']))) - - if int(sw['GSw_HourlyWindow']) <= int(sw['GSw_HourlyWindowOverlap']): - raise ValueError( - ('GSw_HourlyWindow must be greater than GSw_HourlyWindowOverlap:' - '\nGSw_HourlyWindow = {}\nGSw_HourlyWindowOverlap = {}'.format( - sw['GSw_HourlyWindow'], sw['GSw_HourlyWindowOverlap']))) - - if ((sw['GSw_HourlyClusterAlgorithm'] not in ['hierarchical','optimized','kmeans','kmedoids']) - and ('user' not in sw['GSw_HourlyClusterAlgorithm']) - ): - if sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical'): - args = sw['GSw_HourlyClusterAlgorithm'].split('_') - assert len(args) == 3 - ## https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html - assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] - assert args[2] in ['ward', 'complete', 'average', 'single'] - if args[2] == 'ward': - assert args[1] == 'euclidean' - elif sw['GSw_HourlyClusterAlgorithm'].startswith('kmedoids'): - args = sw['GSw_HourlyClusterAlgorithm'].split('_') - assert len(args) == 3 - assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] - assert args[2] in ['heuristic','k-medoids++','random','build'] - else: - raise ValueError( - "GSw_HourlyClusterAlgorithm must be set to 'hierarchical', 'optimized', " - "'kmeans', or 'kmedoids', or must " - "contain the substring 'user' and match a scenario in " - "inputs/temporal/period_szn_user.csv" - ) - - if ((sw['GSw_PRM_StressModel'].lower() not in ['pras']) - and ('user' not in sw['GSw_PRM_StressModel'])): - raise ValueError( - "GSw_PRM_StressModel must be set to 'pras' or must " - "contain the substring 'user' and match a scenario at " - "inputs/temporal/stressperiods_{GSw_PRM_StressModel}.csv" - ) - - if (int(sw['GSw_H2_PTC']) == 1) and (int(sw['GSw_H2']) != 2): - raise ValueError( - 'When running with the H2 PTC enabled, GSw_H2 should be set to 2.\n' - f"GSw_H2_PTC={sw['GSw_H2_PTC']}, GSw_H2={sw['GSw_H2']}" - ) - - if int(sw['GSw_H2_SMR']) == 0 and sw['GSw_H2_Demand_Case'] in ['BAU', 'Aggressive', 'Decarb_with_BAU']: - raise ValueError( - f"GSw_H2_SMR is set to 0, but GSw_H2_Demand_Case is set to '{sw['GSw_H2_Demand_Case']}', which requires SMR set to 1.\n" - "When GSw_H2_SMR is 0, GSw_H2_Demand_Case must be one of: 'none', 'Decarb', or 'LTS'." - ) - - if ('usa' not in sw['GSw_Region'].lower()) and (int(sw['GSw_GasCurve']) != 2): - raise ValueError( - 'Should use GSw_GasCurve=2 (fixed prices) when running sub-nationally\n' - f"GSw_Region={sw['GSw_Region']}, GSw_GasCurve={sw['GSw_GasCurve']}" - ) - - if sw['GSw_RegionResolution'] in ['county','mixed']: - err_switch_configs = [] - if int(sw['GSw_OffshoreZones']): - err_switch_configs.append('GSw_OffshoreZones=1') - if sw['GSw_LoadAllocationMethod'] == 'state_lpf': - err_switch_configs.append('GSw_LoadAllocationMethod=state_lpf') - - if len(err_switch_configs) > 0: - raise NotImplementedError( - 'The following switch configurations are not implemented for ' - 'county/mixed resolution:\n{}\n' - .format('\n'.join(err_switch_configs)) - ) - - reeds.inputs.validate_zoneset(sw['GSw_ZoneSet']) - - ### Aggregation - if (sw['GSw_RegionResolution'] != 'aggreg') and (int(sw['GSw_NumCSPclasses']) != 12): - raise NotImplementedError( - 'Aggregated CSP classes only work with aggregated regions. ' - 'GSw_NumCSPclasses is incompatible with ' - 'GSw_RegionResolution != aggreg') - - ### Parsed string switches - ## Automatic inputs - reeds_path = os.path.dirname(__file__) - hierarchy = reeds.io.get_hierarchy(GSw_ZoneSet=sw['GSw_ZoneSet']).reset_index() - - for threshold in sw['GSw_PRM_StressThreshold'].split('/'): - ## Example: threshold = 'transgrp_10_EUE_sum' - allowed_levels = ['country','interconnect','nercr','transreg','transgrp','st','r'] - (hierarchy_level, ppm, stress_metric, period_agg_method) = threshold.split('_') - if hierarchy_level not in allowed_levels: - raise ValueError( - f"GSw_PRM_StressThreshold: level={hierarchy_level} but must be in:\n" - + '\n'.join(allowed_levels) - ) - if period_agg_method.lower() not in ['sum','max']: - raise ValueError("Fix period agg method in GSw_PRM_StressThreshold") - if not (float(ppm) >= 0): - raise ValueError( - "ppm in GSw_PRM_StressThreshold must be a positive number " - f"but '{ppm}' was provided" - ) - if stress_metric.upper() not in ['EUE','NEUE']: - raise ValueError( - "stress metric in GSw_PRM_StressThreshold must be 'EUE' or 'NEUE' " - f"but '{stress_metric}' was provided" - ) - if (sw['GSw_PRM_StressModel'].lower() != 'pras') and (stress_metric.upper() != 'EUE'): - err = ( - f"The combination of GSw_PRM_StressModel={sw['GSw_PRM_StressModel']} and " - f"stress_metric={stress_metric} is not supported." - ) - raise NotImplementedError(err) - - if sw['GSw_PRM_StressStorageCutoff'].lower() not in ['off','0','false']: - metric, value = sw['GSw_PRM_StressStorageCutoff'].split('_') - if metric.lower()[:3] not in ['eue', 'cap', 'abs']: - raise ValueError( - "The first argument of GSw_PRM_StressStorageCutoff must be in " - f"['eue', 'cap', 'abs'] but {metric} was provided" - ) - try: - float(value) - except ValueError: - raise ValueError( - "The second argument of GSw_PRM_StressStorageCutoff must be a number " - f"but {value} was provided" - ) - if (metric.lower()[:3] == 'abs') and (int(value) != 1): - raise NotImplementedError( - "GSw_PRM_StressStorageCutoff: only abs_1 is implemented for abs but " - f"{metric}_{value} was provided" - ) - - for keyval in sw['GSw_PRM_NetImportLimitScen'].split('/'): - err = ( - "GSw_PRM_NetImportLimitScen accepts inputs in the format " - "{year1}_{'hist' or float}/{year2}_{float}/{year3}_{float} " - "or a single value given as {year1}_{'hist' or float}. Examples are " - "2024_hist/2035_40, 2025_20/2032_40, 2024_hist, 2025_20/2032_40/2050_60. " - f"You entered {sw['GSw_PRM_NetImportLimitScen']}." - ) - year, limit = keyval.split('_') - try: - int(year) - except ValueError: - raise ValueError(err) - if limit not in ['hist', 'histmax']: - try: - float(limit) - except ValueError: - raise ValueError(err) - - for bir in sw['GSw_PVB_BIR'].split('_'): - if not (float(bir) >= 0): - raise ValueError("Fix GSw_PVB_BIR") - - for ilr in sw['GSw_PVB_ILR'].split('_'): - if not (float(ilr) >= 0): - raise ValueError("Fix GSw_PVB_ILR") - - for pvbtype in sw['GSw_PVB_Types'].split('_'): - if not (1 <= int(pvbtype) <= 3): - raise ValueError("Fix GSw_PVB_Types") - - try: - prm = float(sw['GSw_PRM_scenario']) - if prm >= 1: - raise Exception( - f"GSw_PRM_scenario={sw['GSw_PRM_scenario']} but should be formatted as a " - "fraction, not a percent" - ) - except ValueError: - pass - - scalars = reeds.io.get_scalars() - ilr_upv = scalars['ilr_utility'] * 100 - - if ( - int(sw['GSw_PVB']) - and not all([np.isclose(float(ilr), ilr_upv) for ilr in sw['GSw_PVB_ILR'].split('_')]) - ): - raise ValueError( - f"GSw_PVB_ILR = {sw['GSw_PVB_ILR']} but all entries must be {int(ilr_upv)}" - ) - - allowed_years = list(range(2007,2014)) + list(range(2016,2024)) - allowed_years_string = ','.join([str(year) for year in allowed_years]) - - resource_adequacy_years = [int(y) for y in sw['resource_adequacy_years'].split('_')] - for year in resource_adequacy_years: - if year not in allowed_years: - raise ValueError( - f"resource_adequacy_years must be in {allowed_years_string} but is " - f"{sw['resource_adequacy_years']}" - ) - - for year in sw['GSw_HourlyWeatherYears'].split('_'): - if int(year) not in allowed_years: - raise ValueError( - f"GSw_HourlyWeatherYears must be in {allowed_years_string} but is " - f"{sw['GSw_HourlyWeatherYears']}" - ) - - if int(year) not in resource_adequacy_years: - raise ValueError( - "GSw_HourlyWeatherYears must be a subset of resource_adequacy_years but " - f"GSw_HourlyWeatherYears={sw['GSw_HourlyWeatherYears']} and " - f"resource_adequacy_years={sw['resource_adequacy_years']}" - ) - - solveyears = reeds.inputs.parse_yearset(sw['yearset']) - if int(sw['endyear']) not in solveyears: - err = f"`endyear` = {sw['endyear']} but must be in `yearset`: {sw['yearset']}" - raise ValueError(err) - - # Add a row for each county - ## TEMPORARY 20260402 until the aggregation procedure is updated - county2zone = reeds.io.get_county2zone(GSw_ZoneSet='z134', as_map=False) - county2zone['county'] = 'p' + county2zone.FIPS - # Add county info to hierarchy - hierarchy = hierarchy.merge(county2zone.drop(columns=['FIPS','state']), on='r') - - # Make sure specified regions are allowed for the specified hierarchy level - region_groups = sw['GSw_Region'].split('//') if '//' in sw['GSw_Region'] else [sw['GSw_Region']] - for group in region_groups: - level, regions = group.split('/') - if level not in hierarchy: - err = ( - f"The specified hierarchy level '{level}' does not exist in the hierarchy file." - f"\nUpdate GSw_Region={sw['GSw_Region']} to specify a valid level." - ) - raise ValueError(err) - invalid_regions = [ - region for region in regions.split('.') - if region.lower() not in hierarchy[level].str.lower().values - ] - if invalid_regions: - err = f"GSw_Region: {', '.join(invalid_regions)} need to be in {hierarchy[level].unique()}" - raise Exception(err) - - ### Compatible switch combinations - if sw['GSw_LoadProfiles'] == 'historic': - if ('demand_' + sw['demandscen'] +'.csv') not in os.listdir(os.path.join(reeds_path, 'inputs','load')) : - raise ValueError("The demand file specified by the demandscen switch is not in the inputs/load folder") - - if ( - re.match(r'(\/|[a-zA-Z]:[\\\/]).+$', sw['GSw_LoadProfiles']) - and not Path(sw['GSw_LoadProfiles']).is_file() - ): - err = f"GSw_LoadProfiles={sw['GSw_LoadProfiles']} but the specified file does not exist" - raise FileNotFoundError(err) - - ### Dependent model availability - if ( - ((int(sw['pras']) == 2) or int(sw['GSw_PRM_StressIterateMax'])) - and (not os.path.isfile(os.path.join(reeds_path, 'Manifest.toml'))) - ): - err = ( - "Manifest.toml does not exist. " - "Please set up julia by following the instructions at " - "https://natlabrockies.github.io/ReEDS-2.0/setup.html#reeds2pras-julia-and-stress-periods-setup" - ) - raise Exception(err) - - ### Land use and reeds_to_rev - if (int(sw['land_use_analysis'])) and (not int(sw['reeds_to_rev'])): - raise ValueError( - "'reeds_to_rev' must be enable for land_use analysis to run." - ) - - disallowed_characters = ['~', '|', '*'] - invalid_switches = [ - key for key, val in sw.items() - if any([char in val for char in disallowed_characters]) - ] - if len(invalid_switches) > 0: - raise ValueError( - "The following switches have values with disallowed characters " - f"({', '.join(disallowed_characters)}): {', '.join(invalid_switches)}" - ) - - ### Contents of user-specified files - reeds.checks.check_switches(sw) - - ### Uncommonly used packages - if sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): - if importlib.util.find_spec("sklearn_extra") is None: - err = ( - "The scikit-learn-extra package is required for GSw_HourlyClusterAlgorithm=" - f"{sw['GSw_HourlyClusterAlgorithm']} but is not available in your conda " - "environment. Please install it by running:\n" - " pip install 'scikit-learn-extra>=0.2.0,<0.3.0'" - "\nor:\n" - " conda install -c conda-forge scikit-learn-extra=0.2" - ) - raise ModuleNotFoundError(err) - - -def solvestring_sequential( - batch_case, caseSwitches, - cur_year, next_year, prev_year, restartfile, - toLogGamsString=' logOption=4 logFile=gamslog.txt appendLog=1 ', - hpc=0, iteration=0, stress_year=None, - temporal_inputs='rep', - ): - """ - Typical inputs: - * restartfile: batch_case if first solve year else {batch_case}_{prev_year} - * caseSwitches: loaded from {batch_case}/inputs_case/switches.csv - """ - savefile = f"{batch_case}_{cur_year}i{iteration}" - _stress_year = f"{cur_year}i0" if stress_year is None else stress_year - out = ( - "gams d_solveoneyear.gms" - + (" license=gamslice.txt" if hpc else '') - + " o=" + os.path.join("lstfiles", f"{savefile}.lst") - + " r=" + os.path.join("g00files", restartfile) - + " gdxcompress=1" - + " xs=" + os.path.join("g00files", savefile) - + toLogGamsString - + f" --case={batch_case}" - + f" --cur_year={cur_year}" - + f" --next_year={next_year}" - + f" --prev_year={prev_year}" - + f" --stress_year={_stress_year}" - + f" --temporal_inputs={temporal_inputs}" - + ''.join([f" --{s}={caseSwitches[s]}" for s in [ - 'GSw_Canada', - 'GSw_ClimateHydro', - 'GSw_ClimateWater', - 'GSw_gopt', - 'GSw_HourlyChunkLengthRep', - 'GSw_HourlyChunkLengthStress', - 'GSw_HourlyType', - 'GSw_HourlyWrapLevel', - 'GSw_MGA_CostDelta', - 'GSw_MGA_Direction', - 'GSw_PVB_Dur', - 'GSw_SkipAugurYear', - 'GSw_StateCO2ImportLevel', - 'GSw_StartMarkets', - 'GSw_ValStr', - 'solver', - 'debug', - 'startyear', - 'diagnose', - 'diagnose_year' - ]]) - + '\n' - ) - - return out - - -def setup_sequential_year( - cur_year, prev_year, next_year, - caseSwitches, hpc, - solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, - ): - ## Get save file (for this year) and restart file (from previous year) - savefile = f"{batch_case}_{cur_year}i0" - restartfile = batch_case if cur_year == min(solveyears) else f"{batch_case}_{prev_year}i0" - - ## Run the ReEDS LP - if (cur_year >= min(solveyears)): - ## solve one year - OPATH.writelines( - solvestring_sequential( - batch_case, caseSwitches, - cur_year, next_year, prev_year, restartfile, - toLogGamsString, hpc, - )) - OPATH.writelines(writescripterrorcheck(f"d_solveoneyear.gms_{cur_year}")) - OPATH.writelines(f'python {logger} --year={cur_year}\n') - - if int(caseSwitches['GSw_ValStr']): - OPATH.writelines("python valuestreams.py" + '\n') - - ## check to see if the restart file exists - OPATH.writelines(writeerrorcheck(os.path.join("g00files", savefile + ".g*"))) - - ## Run Augur if it not the final solve year and if not skipping Augur - if (( - (cur_year < max(solveyears)) - and (next_year > int(caseSwitches['GSw_SkipAugurYear'])) - ) or (cur_year == max(solveyears))): - OPATH.writelines( - f"\npython Augur.py {next_year} {cur_year} {casedir}\n") - ## Check to make sure Augur ran successfully; quit otherwise - OPATH.writelines( - writeerrorcheck(os.path.join( - "ReEDS_Augur", "augur_data", f"ReEDS_Augur_{cur_year}.gdx"))) - - ## delete the previous restart file unless we're keeping them - if (cur_year > min(solveyears)) and (not int(caseSwitches['keep_g00_files'])): - write_delete_file( - checkfile=os.path.join("g00files", savefile + ".g00"), - deletefile=os.path.join("g00files", restartfile + '.g00'), - PATH=OPATH, - ) - - -def setup_sequential( - caseSwitches, reeds_path, hpc, - solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, - ): - ### loop over solve years - for i in range(len(solveyears)): - ## current year is the value in solveyears - cur_year = solveyears[i] - - if cur_year < max(solveyears): - ## next year becomes the next item in the solveyears vector - next_year = solveyears[i+1] - ## Get previous year if after first year - if i: - prev_year = solveyears[i-1] - else: - prev_year = solveyears[i] - - ### make an indicator in the batch file for what year is being solved - big_comment(f'Year: {cur_year}', OPATH) - - ### Write the tax credit phaseout call - OPATH.writelines(f"python tc_phaseout.py {cur_year} {casedir}\n\n") - - ### Write the GAMS LP and Augur calls - if int(caseSwitches['GSw_PRM_StressIterateMax']): - OPATH.writelines( - f"python d_solve_iterate.py {casedir} {cur_year}\n" - ) - OPATH.writelines(writescripterrorcheck(f"d_solve_iterate.py_{cur_year}")) - else: - setup_sequential_year( - cur_year, prev_year, next_year, - caseSwitches, hpc, - solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, - ) - - if int(caseSwitches['GSw_CheckInputs']): - ### Run input parameter error checks after the first solve year (since financial - ### multipliers aren't created until the first solve year is run) - if cur_year == min(solveyears): - OPATH.writelines( - f"\npython {os.path.join(casedir, 'input_processing', 'check_inputs.py')} " - f"{casedir}\n" - ) - OPATH.writelines(writescripterrorcheck('check_inputs.py')+'\n') - - ### Run Augur plots in background - OPATH.writelines( - f"python {os.path.join('ReEDS_Augur','diagnostic_plots.py')} " - f"--reeds_path={reeds_path} --casedir={casedir} --t={cur_year} &\n") - - -def setup_intertemporal( - caseSwitches, startiter, niter, ccworkers, - solveyears, endyear, batch_case, toLogGamsString, yearset_augur, OPATH, - ): - ### beginning year is passed to augurbatch - begyear = min(solveyears) - ### first save file from d_solveprep is just the case name - savefile = batch_case - ### if this is the first iteration - if startiter == 0: - ## restart file becomes the previous calls save file - restartfile = savefile - ## if this is not the first iteration... - if startiter > 0: - ## restart file is now the case name plus the iteration number - restartfile = batch_case + "_" + startiter - - ### per the instructions, iterations are - ### the number of iterations after the first solve - niter = niter+1 - - ### for the number of iterations we have... - for i in range(startiter,niter): - ## make an indicator in the batch file for what iteration is being solved - big_comment(f'Iteration: {i}', OPATH) - ## call the intertemporal solve - savefile = batch_case + "_" + str(i) - - if i==0: - ## check to see if the restart file exists - ## only need to do this with the zeroth iteration - ## as the other checks will all be after the solves - OPATH.writelines(writeerrorcheck(os.path.join("g00files", restartfile + ".g*"))) - - OPATH.writelines( - "gams d_solveallyears.gms o="+os.path.join("lstfiles",batch_case + "_" + str(i) + ".lst") - +" r="+os.path.join("g00files", restartfile) - + " gdxcompress=1 xs="+os.path.join("g00files", savefile) + toLogGamsString - + " --niter=" + str(i) + " --case=" + batch_case + ' \n') - - ## check to see if the save file exists - OPATH.writelines(writeerrorcheck(os.path.join("g00files",savefile + ".g*"))) - - ## start threads for cc/curt - ## no need to run cc curt scripts for final iteration - if i < niter-1: - ## batch out calls to augurbatch - OPATH.writelines( - "python augurbatch.py " + batch_case + " " + str(ccworkers) + " " - + yearset_augur + " " + savefile + " " + str(begyear) + " " - + str(endyear) + " " + caseSwitches['distpvscen'] + " " - + str(caseSwitches['calc_csp_cc']) + " " - + str(caseSwitches['timetype']) + " " - + str(caseSwitches['GSw_WaterMain']) + " " + str(i) + " " - + str(caseSwitches['marg_vre_mw']) + " " - + str(caseSwitches['marg_stor_mw']) + " " - + str(caseSwitches['marg_evmc_mw']) + " " - + '\n') - ## merge all the resulting gdx files - ## the output file will be for the next iteration - nextiter = i+1 - gdxmergedfile = os.path.join( - "ReEDS_Augur","augur_data","ReEDS_Augur_merged_" + str(nextiter)) - OPATH.writelines( - "gdxmerge "+os.path.join("ReEDS_Augur","augur_data","ReEDS_Augur*") - + " output=" + gdxmergedfile + ' \n') - ## check to make sure previous calls were successful - OPATH.writelines(writeerrorcheck(gdxmergedfile+".gdx")) - - ## restart file becomes the previous save file - restartfile = savefile - - if caseSwitches['GSw_ValStr'] != '0': - OPATH.writelines( "python valuestreams.py" + '\n') - - -def setup_window( - caseSwitches, startiter, niter, ccworkers, reeds_path, - batch_case, toLogGamsString, yearset_augur, OPATH, - ): - ### load the windows - win_in = list(csv.reader(open( - os.path.join( - reeds_path,"inputs","userinput", - "windows_{}.csv".format(caseSwitches['windows_suffix'])), - 'r'), delimiter=",")) - - restartfile = batch_case - - ### for windows indicated in the csv file - for win in win_in[1:]: - - ## beginning year is the first column (start) - begyear = win[1] - ## end year is the second column (end) - endyear = win[2] - ## for the number of iterations we have... - for i in range(startiter,niter): - big_comment(f'Window: {win}', OPATH) - comment(f'Iteration: {i}', OPATH) - - ## call the window solve - savefile = batch_case+"_"+str(i) - ## check to see if the save file exists - OPATH.writelines(writeerrorcheck(os.path.join("g00files", restartfile + ".g*"))) - ## solve via the window solve file - OPATH.writelines( - "gams d_solvewindow.gms o=" + os.path.join("lstfiles", batch_case + "_" + str(i) + ".lst") - +" r=" + os.path.join("g00files", restartfile) - + " gdxcompress=1 xs=g00files\\"+savefile + toLogGamsString + " --niter=" + str(i) - + " --maxiter=" + str(niter-1) + " --case=" + batch_case + " --window=" + win[0] + ' \n') - ## start threads for cc/curt - OPATH.writelines(writeerrorcheck(os.path.join("g00files",savefile + ".g*"))) - OPATH.writelines( - "python augurbatch.py " + batch_case + " " + str(ccworkers) + " " - + yearset_augur + " " + savefile + " " + str(begyear) + " " - + str(endyear) + " " + caseSwitches['distpvscen'] + " " - + str(caseSwitches['calc_csp_cc']) + " " - + str(caseSwitches['timetype']) + " " - + str(caseSwitches['GSw_WaterMain']) + " " + str(i) + " " - + str(caseSwitches['marg_vre_mw']) + " " - + str(caseSwitches['marg_stor_mw']) + " " - + str(caseSwitches['marg_evmc_mw']) + " " - + '\n') - ## merge all the resulting r2_in gdx files - ## the output file will be for the next iteration - nextiter = i+1 - ## create names for then merge the curt and cc gdx files - gdxmergedfile = os.path.join( - "ReEDS_Augur","augur_data","ReEDS_Augur_merged_" + str(nextiter)) - OPATH.writelines( - "gdxmerge " + os.path.join("ReEDS_Augur","augur_data","ReEDS_Augur*") - + " output=" + gdxmergedfile + ' \n') - ## check to make sure previous calls were successful - OPATH.writelines(writeerrorcheck(gdxmergedfile+".gdx")) - restartfile = savefile - if caseSwitches['GSw_ValStr'] != '0': - OPATH.writelines( "python valuestreams.py" + '\n') - - -#%% =========================================================================== -### --- PROCEDURE --- -### =========================================================================== - -def setupEnvironment( - BatchName=False, cases_suffix=False, single='', simult_runs=0, - forcelocal=0, skip_checks=False, - debug=False, debugnode=False, cases_per_node=1, - dryrun=False, - ): - #%% Settings for testing - # BatchName = 'v20260307_downloadM0' - # cases_suffix = 'test' - # WORKERS = 1 - # forcelocal = 0 - # single = '' - # skip_checks = False - # debug = False - # dryrun = True - - #%% Automatic inputs - reeds_path = os.path.dirname(__file__) - - #%% User inputs - print(" ") - print("------------- ") - print(" ") - print("WINDOWS USERS - This script will open multiple command prompts, the number of which") - print("is based on the number of simultaneous runs you've chosen") - print(" ") - print("MAC/LINUX USERS - Your cases will run in the background. All console output") - print("is written to the cases' appropriate gamslog.txt file in the cases' runs folders") - print(" ") - print("------------- ") - print(" ") - print(" ") - - if not BatchName: - print("-- Specify the batch prefix --") - print(" ") - print("The batch prefix is attached to the beginning of all cases' outputs files") - print("Note - it must start with a letter and not a number or symbol") - print(" ") - print("A value of 0 will assign the date and time as the batch name (e.g. v20190520_072310)") - print(" ") - - BatchName = str(input('Batch Prefix: ')) - - if BatchName == '0': - BatchName = 'v' + time.strftime("%Y%m%d_%H%M%S") - - #check for period in batchname and replace with underscore - BatchName = BatchName.replace('.', '_') - - if not cases_suffix: - print("\n\nSpecify the suffix for the cases_suffix.csv file") - print("A blank input will default to the cases.csv file\n") - - cases_suffix = str(input('Case Suffix: ')) - - #%% Check whether to submit slurm jobs (if on HPC) or run locally - hpc = True if (int(os.environ.get('REEDS_USE_SLURM',0))) else False - hpc = False if forcelocal else hpc - - ### If on NLR HPC but NOT submitting slurm job, ask for confirmation - if ('NREL_CLUSTER' in os.environ) and (not hpc): - print( - "It looks like you're running on the NLR HPC but the REEDS_USE_SLURM environment " - "variable is not set to 1, meaning the model will run locally rather than being " - "submitted as a slurm job. Are you sure you want to run locally?" - ) - confirm_local = str(input('Run job locally? y/[n]: ') or 'n') - if confirm_local not in ['y','Y','yes','Yes','YES']: - quit() - - #%% Check whether the ReEDS conda environment is activated - if (not skip_checks) and ( - ('reeds2' not in os.environ['CONDA_DEFAULT_ENV'].lower()) - or (not pd.__version__.startswith('2')) - ): - print( - f"Your environment is {os.environ['CONDA_DEFAULT_ENV']} and your pandas " - f"version is {pd.__version__}.\nThe default environment is 'reeds2', with\n" - "pandas version 2.x, so the python parts of ReEDS are unlikely to work.\n" - "To build the environment for the first time, run:\n" - " `conda env create -f environment.yml`\n" - "To activate the created environment, run:\n" - " `conda activate reeds2` (or `activate reeds2` on Windows)\n" - "Do you want to continue without activating the environment?" - ) - confirm_env = str(input("Continue? y/[n]: ") or 'n') - if confirm_env not in ['y','Y','yes','Yes','YES']: - quit() - - #%% Load specified case file, infer other settings from cases.csv - if cases_suffix in ['', 'default']: - cases_filename = 'cases.csv' - else: - cases_filename = f'cases_{cases_suffix}.csv' - - df_cases = reeds.inputs.parse_cases( - cases_filename=cases_filename, - single=single, - skip_checks=skip_checks, - ) - ## Propagate debug setting - if debug: - df_cases.loc['debug'] = str(debug) - - caseSwitches, casenames, caseList = create_case_lists( - df_cases=df_cases, - BatchName=BatchName, - single=single, - ) - - #%% Stop now if any switches are incompatible - for sw in caseSwitches: - check_compatibility(sw) - if dryrun: - quit() - - # If no --single/-s, drop the ignored cases, otherwise leave them - if not single: - casenames = [case for case in casenames - if int(df_cases.loc['ignore',case]) != 1] - df_cases.drop( - df_cases.loc['ignore'].loc[df_cases.loc['ignore']=='1'].index, - axis=1, - inplace=True - ) - # If the "single" argument is provided, only run that case - if single: - for s in single.split(','): - if s not in df_cases: - err = ( - f'Specified single={single} but available cases are:\n' - + '\n> '.join([c for c in df_cases.columns]) - ) - raise KeyError(err) - df_cases = df_cases[single.split(',')].copy() - casenames = single.split(',') - - # Make sure the run folders don't already exist - outpaths = [os.path.join(reeds_path,'runs',f'{BatchName}_{case}') for case in casenames] - existing_outpaths = [i for i in outpaths if os.path.isdir(i)] - if len(existing_outpaths): - print( - f'The following {len(existing_outpaths)} output directories already exist:\n' - + 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n' - + '\n'.join([os.path.basename(i) for i in existing_outpaths]) - + '\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' - ) - overwrite = str(input('Do you want to overwrite them? y/[n]: ') or 'n') - if overwrite.lower() in ['y', 'yes']: - for outpath in existing_outpaths: - shutil.rmtree(outpath) - else: - keep = [i for (i,c) in enumerate(outpaths) if c not in existing_outpaths] - caseList = [caseList[i] for i in keep] - casenames = [casenames[i] for i in keep] - caseSwitches = [caseSwitches[i] for i in keep] - print( - f"\nThe following {(len(keep))} output directories don't exist:\n" - + 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n' - + '\n'.join([f'{BatchName}_{c}' for c in casenames]) - + '\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' - ) - skip = str(input('Do you want to run them and skip the rest? [y]/n: ') or 'y') - if skip.lower() not in ['y','yes']: - raise IsADirectoryError('\n'+'\n'.join(existing_outpaths)) - - #%% User warnings - if (df_cases.loc['cleanup_level'].astype(int) > 0).any() and not skip_checks: - print( - '\nWARNING: At least one case uses cleanup_level ≥ 1, which removes files ' - 'used by R2X.\nIf you plan to run R2X, do not proceed; set cleanup_level ' - 'to 0 in your cases file and restart the run.' - ) - confirm = str(input('\nProceed? y/[n]: ') or 'n') - if confirm.lower() not in ['y', 'yes']: - quit() - - print("{} cases being run:".format(len(caseList))) - for case in casenames: - print(case) - print(" ") - - reschoice = 0 - startiter = 0 - ccworkers = 5 - niter = 5 - #%% Set number of workers, with user input if necessary - if len(caseList)==1: - print("Only one case is to be run, therefore only one thread is needed") - WORKERS = 1 - elif simult_runs < 0 or hpc: - WORKERS = min(10, len(caseList)) - elif simult_runs > 0: - WORKERS = simult_runs - else: - WORKERS = int(input('Number of simultaneous runs [integer]: ')) - - if 'int' in df_cases.loc['timetype'].tolist() or 'win' in df_cases.loc['timetype'].tolist(): - ccworkers = int(input('Number of simultaneous CC/Curt runs [integer]: ')) - print("") - print("The number of iterations defines the number of combinations of") - print(" solving the model and computing capacity credit and curtailment") - print(" Note this does not include the initial solve") - print("") - niter = int(input('How many iterations between the model and CC/Curt scripts: ')) - - if reschoice==1: - startiter = int(input('Iteration to start from (recall it starts at zero): ')) - - if hpc and cases_per_node is None: - if df_cases.shape[1] > 1: - # On HPC with multiple cases and no cases_per_node provided - cases_per_node = int(input('Number of simultaneous runs per node [integer]: ')) - else: - # On HPC with one case only - cases_per_node = 1 - elif not hpc: - # Not on HPC - cases_per_node = None - - #%% Sync remote files - print('Syncing remote files') - # If using Monte Carlo sampling, download everything (since combinations of switches - ## not listed in cases{}.csv may be used) - if df_cases.loc['MCS_runs'].astype(int).sum(): - reeds.remote.download_remote_files() - ## Otherwise, only download the files needed for the present set of runs - else: - required_files = [ - reeds.remote.identify_required_remote_files(df_cases[case]) for case in df_cases - ] - required_files = sorted(set([i for sublist in required_files for i in sublist])) - reeds.remote.download_remote_files(required_files) - - envVar = { - 'WORKERS': WORKERS, - 'ccworkers': ccworkers, - 'casenames': casenames, - 'BatchName': BatchName, - 'caseList': caseList, - 'caseSwitches': caseSwitches, - 'reeds_path' : reeds_path, - 'niter' : niter, - 'startiter' : startiter, - 'cases_filename': cases_filename, - 'hpc': hpc, - 'debugnode': debugnode, - 'cases_per_hpc_node': cases_per_node, - } - - return envVar - - -def createmodelthreads(envVar): - - q = queue.Queue() - num_worker_threads = envVar['WORKERS'] - - def worker(): - while True: - ThreadInit = q.get() - if ThreadInit is None: - break - launch_single_case_run( - options=ThreadInit['scen'], - caseSwitches=ThreadInit['caseSwitches'], - niter=ThreadInit['niter'], - reeds_path=ThreadInit['reeds_path'], - ccworkers=ThreadInit['ccworkers'], - startiter=ThreadInit['startiter'], - BatchName=ThreadInit['BatchName'], - case=ThreadInit['casename'], - cases_filename=ThreadInit['cases_filename'], - hpc=envVar['hpc'], - debugnode=envVar['debugnode'], - ) - print(ThreadInit['batch_case'] + " has finished \n") - q.task_done() - - - threads = [] - - for i in range(num_worker_threads): - t = threading.Thread(target=worker) - t.start() - threads.append(t) - - for i in range(len(envVar['caseList'])): - q.put({ - 'scen': envVar['caseList'][i], - 'caseSwitches': envVar['caseSwitches'][i], - 'batch_case':envVar['BatchName']+'_'+envVar['casenames'][i], - 'niter':envVar['niter'], - 'reeds_path':envVar['reeds_path'], - 'ccworkers':envVar['ccworkers'], - 'startiter':envVar['startiter'], - 'BatchName':envVar['BatchName'], - 'casename':envVar['casenames'][i], - 'cases_filename':envVar['cases_filename'], - }) - - # block until all tasks are done - q.join() - - # stop workers - for i in range(num_worker_threads): - q.put(None) - - for t in threads: - t.join() - - -def write_batch_script( - options, - batch_case, - reeds_path, - casedir, - caseSwitches, - cases_filename, - hpc, - startiter, - niter, - ccworkers, - BatchName, - case, -): - inputs_case = os.path.join(casedir,"inputs_case") - - if os.path.exists(os.path.join(reeds_path, 'runs', batch_case)): - print('Caution, case ' + batch_case + ' already exists in runs \n') - - #%% Set up case-specific directory structure - os.makedirs(inputs_case, exist_ok=True) - os.makedirs(os.path.join(casedir, 'g00files'), exist_ok=True) - os.makedirs(os.path.join(casedir, 'lstfiles'), exist_ok=True) - os.makedirs(os.path.join(casedir, 'outputs', 'figures'), exist_ok=True) - os.makedirs(os.path.join(casedir, 'outputs', 'tc_phaseout_data'), exist_ok=True) - - if int(caseSwitches['diagnose']): - os.makedirs(os.path.join(casedir, 'outputs', 'model_diagnose'), exist_ok=True) - - #%% Information on reV supply curves associated with this run - shutil.copytree(os.path.join(reeds_path,'inputs','supply_curve','metadata'), - os.path.join(inputs_case,'supplycurve_metadata'), dirs_exist_ok=True) - rev_paths = pd.read_csv( - os.path.join(reeds_path,'inputs','supply_curve','rev_paths.csv') - ) - - # Separate techs with no associated switch - rev_paths_none = rev_paths.loc[rev_paths.access_switch == "none",:].copy() - rev_paths = rev_paths.loc[rev_paths.access_switch != "none",:] - - # Match possible supply curves with switches from this run - revswitches = pd.DataFrame.from_dict({s:caseSwitches[s] for s in rev_paths.access_switch.unique()}, - orient='index').reset_index().rename(columns={'index':'access_switch', 0:'access_case'}) - revswitches = revswitches.merge(rev_paths, on=['access_switch', 'access_case']) - revswitches = pd.concat([revswitches[rev_paths_none.columns.tolist()], rev_paths_none]) - - # Get bin information - bins = {"wind-ons":"numbins_windons", "wind-ofs": "numbins_windofs", "upv":"numbins_upv"} - binSwitches = pd.DataFrame.from_dict({b:caseSwitches[bins[b]] for b in bins}, - orient='index').reset_index().rename(columns={'index':'tech', 0:'bins'}) - - revswitches = revswitches.merge(binSwitches, on=['tech'], how='left') - - # format rev paths - revswitches = get_rev_paths(revswitches, caseSwitches) - - # save rev paths file for run - revswitches[['tech','access_switch','access_case','rev_case','bins','sc_path', - 'sc_file','hpc_sc_file','cf_path','original_rev_folder'] - ].to_csv(os.path.join(inputs_case,'rev_paths.csv'), index=False) - - #%% Set up the meta.csv file to track repo information and runtime - logger = os.path.join(reeds_path, 'reeds', 'log.py') - loglines = ['repo,branch,commit,tag,description\n'] - ### Get some git metadata - try: - repo = git.Repo() - try: - branch = repo.active_branch.name - tag = sorted(repo.tags, key=lambda t: t.commit.committed_datetime)[-1].name - description = repo.git.describe() - except TypeError: - branch = 'DETACHED_HEAD' - tag = '' - description = '' - text = f'{repo.git_dir},{branch},{repo.head.object.hexsha},{tag},{description}\n' - loglines.append(text) - except Exception: - ## In case the user isn't in a git repo or anything else goes wrong - loglines.append('None,None,None,None,None\n') - - with open(os.path.join(casedir,'meta.csv'),'a') as METAFILE: - ### Header for timing metadata - for line in loglines: - METAFILE.writelines(line) - METAFILE.writelines('#,#,#,#,#\n') - METAFILE.writelines('year,process,starttime,stoptime,processtime\n') - ### Also write the git metadata to gamslog.txt for debugging - with open(os.path.join(casedir,'gamslog.txt'),'a') as LOGFILE: - for line in loglines: - LOGFILE.writelines(line) - - ### Write the environment info for debugging - try: - environment = subprocess.run('conda list', capture_output=True, shell=True) - pd.Series(environment.stdout.decode().split('\n')).to_csv( - os.path.join(casedir,'lstfiles','environment.csv'), - header=False, index=False, - ) - except Exception as err: - print(err) - - ### Copy over the cases file - shutil.copy2(os.path.join(reeds_path, cases_filename), casedir) - - ### Switches with values derived from other switches - ## Get hpc setting (used in Augur) - caseSwitches['hpc'] = int(hpc) - ## Get numclass from the max value in ivt - caseSwitches['numclass'] = get_ivt_numclass( - reeds_path=reeds_path, casedir=casedir, caseSwitches=caseSwitches) - - for switchname in ['hpc', 'numclass']: - options += f" --{switchname}={caseSwitches[switchname]}" - options += f' --reeds_path={reeds_path}{os.sep} --casedir={casedir}' - - #%% Record the switches for this run - reeds.io.write_gswitches(pd.Series(caseSwitches), inputs_case) - - pd.Series(caseSwitches).to_csv( - os.path.join(inputs_case,'switches.csv'), header=False) - - solveyears = reeds.inputs.parse_yearset(caseSwitches['yearset']) - - # If start year is not in solveyears, start year is added into solveyears set - startyear = int(caseSwitches['startyear']) - endyear = int(caseSwitches['endyear']) - if startyear not in solveyears: - solveyears.append(startyear) - solveyears = sorted(solveyears) - - solveyears = [y for y in solveyears if (y <= endyear and y >= startyear)] - - yearset_augur = os.path.join('inputs_case','modeledyears.csv') - toLogGamsString = ' logOption=4 logFile=gamslog.txt appendLog=1 ' - - ## Copy code folders - for dirname in ['reeds', 'ReEDS_Augur', 'input_processing', 'reeds2pras']: - shutil.copytree( - os.path.join(reeds_path, dirname), - os.path.join(casedir, dirname), - ignore=shutil.ignore_patterns('test'), - ) - - #make the augur_data folder - os.makedirs(os.path.join(casedir,'ReEDS_Augur','augur_data'), exist_ok=True) - os.makedirs(os.path.join(casedir,'ReEDS_Augur','PRAS'), exist_ok=True) - - ###### Replace files according to 'file_replacements' in cases. Ignore quotes in input text. - # << is used to separate the file that is to be replaced from the file that is used - # || is used to separate multiple replacements. - if caseSwitches['file_replacements'] != 'none': - file_replacements = caseSwitches['file_replacements'].replace('"','').replace("'","").split('||') - for file_replacement in file_replacements: - replace_arr = file_replacement.split('<<') - replaced_file = replace_arr[0].strip() - replaced_file = os.path.join(casedir, replaced_file) - if not os.path.isfile(replaced_file): - raise FileNotFoundError('FILE REPLACEMENT ERROR: "' + replaced_file + '" was not found') - used_file = replace_arr[1].strip() - if not os.path.isfile(used_file): - raise FileNotFoundError('FILE REPLACEMENT ERROR: "' + used_file + '" was not found') - if os.path.isfile(replaced_file) and os.path.isfile(used_file): - shutil.copy(used_file, replaced_file) - print('FILE REPLACEMENT SUCCESS: Replaced "' + replaced_file + '" with "' + used_file + '"') - - #%% Write the call script - with open(os.path.join(casedir, 'call_' + batch_case + ext), 'w+') as OPATH: - OPATH.writelines(f"echo 'Running {batch_case}'\n") - OPATH.writelines("cd " + casedir + '\n' + '\n' + '\n') - - if hpc: - comment('Set up nodal environment for run', OPATH) - OPATH.writelines(". $HOME/.bashrc \n") - OPATH.writelines("module purge \n") - - if os.environ.get('NREL_CLUSTER') == 'kestrel': - OPATH.writelines("source /nopt/nrel/apps/env.sh \n") - OPATH.writelines("module load anaconda3 \n") - OPATH.writelines("module use /nopt/nrel/apps/software/gams/modulefiles \n") - OPATH.writelines("module load gams \n") - else: - OPATH.writelines("module load conda \n") - OPATH.writelines("module load gams \n") - - OPATH.writelines("conda activate reeds2 \n") - OPATH.writelines('export R_LIBS_USER="$HOME/rlib" \n\n\n') - - #%% Write the input_processing script calls - big_comment('Input processing', OPATH) - for s in [ - 'copy_files', - 'mcs_sampler', - 'aggregate_regions', - 'hydcf', - 'h2_storage', - 'calc_financial_inputs', - 'fuelcostprep', - 'writecapdat', - 'writesupplycurves', - 'writedrshift', - 'plantcostprep', - 'climateprep', - 'hourly_load', - 'recf', - 'forecast', - 'WriteHintage', - 'transmission', - 'outage_rates', - 'hourly_repperiods', - ]: - OPATH.writelines(f"echo {'-'*12+'-'*len(s)}\n") - OPATH.writelines(f"echo 'starting {s}.py'\n") - OPATH.writelines(f"echo {'-'*12+'-'*len(s)}\n") - OPATH.writelines( - f"python {os.path.join(casedir,'input_processing',s)}.py {reeds_path} {inputs_case}\n") - OPATH.writelines(writescripterrorcheck(s)+'\n') - - OPATH.writelines( - f"python {os.path.join(reeds_path, 'postprocessing', 'cleanup_files.py')} " - f"{casedir} --force --quiet\n" - ) - - if int(caseSwitches['input_processing_only']): - OPATH.writelines( - f"python {os.path.join(reeds_path, 'postprocessing', 'cleanup_files.py')} " - f"{casedir} --force --quiet --level {caseSwitches['cleanup_level']}\n" - ) - OPATH.writelines('\n' + ('exit' if LINUXORMAC else 'goto:eof') + '\n\n') - - big_comment('Compile model', OPATH) - - OPATH.writelines( - "\ngams createmodel.gms gdxcompress=1 xs="+os.path.join("g00files",batch_case) - + (' license=gamslice.txt' if hpc else '') - + " o="+os.path.join("lstfiles","1_Inputs.lst") + options + " " + toLogGamsString + '\n') - OPATH.writelines(f'python {logger}\n') - restartfile = batch_case - OPATH.writelines(writeerrorcheck(os.path.join('g00files', restartfile + '.g*'))) - - ################################ - # -- CORE MODEL SETUP -- # - ################################ - if caseSwitches['timetype'] == 'seq': - setup_sequential( - caseSwitches, reeds_path, hpc, - solveyears, casedir, batch_case, toLogGamsString, OPATH, logger, - ) - elif caseSwitches['timetype'] == 'int': - setup_intertemporal( - caseSwitches, startiter, niter, ccworkers, - solveyears, endyear, batch_case, toLogGamsString, yearset_augur, OPATH, - ) - elif caseSwitches['timetype'] == 'win': - setup_window( - caseSwitches, startiter, niter, ccworkers, reeds_path, - batch_case, toLogGamsString, yearset_augur, OPATH, - ) - - ################################# - # -- OUTPUT PROCESSING -- # - ################################# - #create reporting files - big_comment('Output processing', OPATH) - if not LINUXORMAC: - OPATH.writelines("setlocal enabledelayedexpansion\n") - ### If not iterating, run for iteration 0 - if not int(caseSwitches['GSw_PRM_StressIterateMax']): - OPATH.writelines( - f"r={os.path.join('g00files', f'{batch_case}_{max(solveyears)}i0')}\n" - if LINUXORMAC else - f'set "r={os.path.join("g00files", f"{batch_case}_{max(solveyears)}i0")}"\n' - ) - ### Otherwise, run for the last iteration (lexicographically sorted) - else: - OPATH.writelines( - f"for r in g00files/{batch_case}_*.g00; do true; done\n" - if LINUXORMAC else - f'for %%i in (g00files\{batch_case}_*.g00) do (set "r=%%i")\n' - ) - OPATH.writelines( - "gams e_report.gms" - + f" o={os.path.join('lstfiles',f'report_{batch_case}.lst')}" - + (' license=gamslice.txt' if hpc else '') - + (' r=$r' if LINUXORMAC else ' r=!r!') - + ' gdxcompress=1' - + toLogGamsString - + f"--fname={batch_case}" - + f" --GSw_calc_powfrac={caseSwitches['GSw_calc_powfrac']} \n" - ) - OPATH.writelines(writescripterrorcheck("e_report.gms")) - if not LINUXORMAC: - OPATH.writelines("endlocal\n") - OPATH.writelines(f'python {logger}\n') - OPATH.writelines(f'python e_report_dump.py {casedir} -c\n\n') - if int(caseSwitches['diagnose']): - OPATH.writelines( - "python" - + f" {os.path.join(reeds_path,'postprocessing','diagnose','diagnose_process.py')}" - + f" --casepath {casedir} \n\n" - ) - - ### Run the retail rate module - OPATH.writelines( - "python" - + f" {os.path.join(reeds_path,'postprocessing','retail_rate_module','retail_rate_calculations.py')}" - + f" {batch_case} -p\n\n" - ) - - ## Run air-quality and health damages calculation script - OPATH.writelines( - "python " - f"{os.path.join(reeds_path,'postprocessing','air_quality','health_damage_calculations.py')} " - f"{casedir}\n\n" - ) - - ### Make script to unload all data to .gdx file - command = ( - 'gams dump_alldata.gms' - + ' o='+os.path.join('lstfiles','dump_alldata_{}_{}.lst'.format(BatchName,case)) - ) - command_write = ( - command - + ' r='+os.path.join('g00files','{}_{}_{}i0'.format(BatchName,case,solveyears[-1])) - ) - with open(os.path.join(casedir,'dump_alldata' + ext), 'w+') as datadumper: - datadumper.writelines('cd ' + os.path.join(reeds_path,'runs','{}_{}'.format(BatchName,case)) + '\n') - for line in [ - f"By default, this script dumps data for the first iteration of {solveyears[-1]}.", - "If more iterations were needed, increase the number at the end of the", - f"next line after 'i' (e.g. {solveyears[-1]}i0 -> {solveyears[-1]}i1)", - ]: - comment(line, datadumper) - datadumper.writelines(command_write) - if int(caseSwitches['dump_alldata']) or int(caseSwitches['debug']): - OPATH.writelines(command + (' r=$r' if LINUXORMAC else ' r=!r!') + '\n') - - ## ReEDS_to_rev processing - if caseSwitches['reeds_to_rev'] == '1': - OPATH.writelines('cd {} \n\n'.format(reeds_path)) - OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "priority" ' - '-t "wind-ons" -l "gamslog.txt" -r\n') - OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "priority" ' - '-t "wind-ofs" -l "gamslog.txt" -r\n') - OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "simultaneous" ' - '-t "upv" -l "gamslog.txt" -r\n\n') - OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "simultaneous" ' - '-t "geohydro_allkm" -l "gamslog.txt" -r\n\n') - OPATH.writelines(f'python hourlize/reeds_to_rev.py {reeds_path} {casedir} "simultaneous" ' - '-t "egs_allkm" -l "gamslog.txt" -r\n\n') - - if caseSwitches['land_use_analysis'] == '1': - # Run the land-used characterization module - OPATH.writelines( - f"python {os.path.join(reeds_path,'postprocessing','land_use','land_use_analysis.py')} {casedir}\n\n" - ) - - ## Run Bokeh - bokehdir = os.path.join(reeds_path,"postprocessing","bokehpivot","reports") - OPATH.writelines( - 'python ' + os.path.join(bokehdir,"interface_report_model.py") + ' "ReEDS 2.0" ' - + os.path.join(reeds_path,"runs",batch_case) + " all No none " - + os.path.join(bokehdir,"templates","reeds2","standard_report_reduced.py") + ' "html,excel,csv" one ' - + os.path.join(reeds_path,"runs",batch_case,"outputs","reeds-report-reduced") + ' No\n') - OPATH.writelines( - 'python ' + os.path.join(bokehdir,"interface_report_model.py") + ' "ReEDS 2.0" ' - + os.path.join(reeds_path,"runs",batch_case) + " all No none " - + os.path.join(bokehdir,"templates","reeds2","standard_report_expanded.py") + ' "html,excel" one ' - + os.path.join(reeds_path,"runs",batch_case,"outputs","reeds-report") + ' No\n') - OPATH.writelines( - 'python ' + os.path.join(bokehdir,"interface_report_model.py") + ' "ReEDS 2.0" ' - + os.path.join(reeds_path,"runs",batch_case) + " all No none " - + os.path.join(bokehdir,"templates","reeds2","state_report.py") + ' "csv" one ' - + os.path.join(reeds_path,"runs",batch_case,"outputs","reeds-report-state") + ' No\n\n') - OPATH.writelines('python postprocessing/vizit/vizit_prep.py ' + '"{}"'.format(os.path.join(casedir,'outputs')) + '\n\n') - - OPATH.writelines( - f'python postprocessing/single_case_plots.py {casedir} --year {solveyears[-1]}\n\n' - ) - - ### Remove unnecessary files from case folder - OPATH.writelines( - f"python {os.path.join(reeds_path, 'postprocessing', 'cleanup_files.py')} " - f"{casedir} --force --quiet --level {caseSwitches['cleanup_level']}\n" - ) - - ### Run R2X if using debug mode - ### First install uvx: https://docs.astral.sh/uv/getting-started/installation/ - if int(caseSwitches['debug']): - r2xpath = os.path.join(casedir, 'outputs', 'r2x') - os.makedirs(r2xpath, exist_ok=True) - OPATH.writelines( - "uvx --python 3.11 --from git+https://github.com/nrel/r2x@main r2x -vv run " - f"-i {casedir} " - f"-o {r2xpath} " - "--input-model=reeds-US " - "--output-model=plexos " - f"--year={endyear} " - f"--weather-year={caseSwitches['GSw_HourlyWeatherYears'].split('_')[0]} " - "\n" - ) - - ### Check the error level - pipe = '2>&1 | tee -a' if LINUXORMAC else '>>' - tolog = f"{pipe} {os.path.join(casedir,'gamslog.txt')}" - OPATH.writelines(f"\npython postprocessing/check_error.py {casedir} {tolog}\n") - - ### Run dispatch mode if desired - if int(caseSwitches['pcm']): - OPATH.writelines(f'\npython run_pcm.py {casedir} -b\n\n') - - -def submit_slurm_parallel_jobs( - reeds_path, BatchName, casenames, cases_per_node, debugnode = False, -): - """ - Write and submit Slurm parallel run scripts for each group of cases in the batch. - """ - num_cases = len(casenames) - num_nodes = int(np.ceil(num_cases / cases_per_node)) - - batch_folder = os.path.join( - reeds_path, 'slurm_parallel_runs', - f"{BatchName}-{datetime.now().strftime('%Y%m%dT%H%M%S')}" - ) - if os.path.isdir(batch_folder): - shutil.rmtree(batch_folder) - os.makedirs(batch_folder) - - for node_index in range(num_nodes): - start_case_index = cases_per_node * node_index - stop_case_index = min(start_case_index + cases_per_node, num_cases) - casenames_print = casenames[start_case_index:stop_case_index] - run_script_fpath = os.path.join(batch_folder, f"run_{'-'.join(casenames_print)}.sh") - shutil.copy("srun_template.sh", run_script_fpath) - job_name = f"{BatchName}_({','.join(casenames_print)})" - - writelines = [] - with open(run_script_fpath, 'r') as SPATH: - for line in SPATH: - stripped = line.strip() - if debugnode and ('--time' in stripped or '--partition' in stripped): - continue - elif '--ntasks-per-node' in stripped: - writelines.append('# ' + stripped) - else: - writelines.append(stripped) - - with open(run_script_fpath, 'w') as SPATH: - for line in writelines: - SPATH.write(line + '\n') - - if debugnode: - SPATH.write("#SBATCH --time=04:00:00\n") - SPATH.write("#SBATCH --partition=debug\n") - - SPATH.write(f"#SBATCH --ntasks-per-node={cases_per_node}\n") - SPATH.write(f"#SBATCH --job-name={job_name}\n") - SPATH.write(f"#SBATCH --output={os.path.join(batch_folder, 'slurm-%j.out')}\n\n") - SPATH.write(". $HOME/.bashrc\n\n") - - for idx, case_index in enumerate(range(start_case_index, stop_case_index)): - casename = casenames[case_index] - batch_case = f"{BatchName}_{casename}" - casedir = os.path.join(reeds_path, 'runs', batch_case) - - bash_path = os.path.join(casedir, 'call_' + batch_case + ext) - task_out = os.path.join(casedir, f'slurm-${{SLURM_JOB_ID}}_{idx}.out') - resource_stats = os.path.join(casedir, 'resource_stats.log') - srun_line = ( - f"srun --ntasks=1 --overlap " - f"/usr/bin/time -a -o {resource_stats} " - f"-f 'memory_KB=%M, runtime=%E' " - f"bash {bash_path} 2>&1 | tee {task_out} &" - ) - SPATH.write(srun_line + '\n') - - # Also write the Slurm script for a single run - # just in case you need to restart a single case in the future - write_case_submission_script( - casedir, batch_case, debugnode=debugnode, - ) - - SPATH.write("wait\n") - - # Submit the job script via Slurm - try: - subprocess.Popen(["sbatch", run_script_fpath]) - except Exception as e: - raise RuntimeError(f"Failed to submit job script: {run_script_fpath}\nError: {e}") - - -def write_case_submission_script( - casedir, batch_case, debugnode = False, -): - """ - Writes a SLURM submission script in the specified case directory. - - This script (named based on the batch_case) includes SLURM resource - allocation directives and is responsible for launching the actual - ReEDS execution script (e.g., run logic). - """ - # Create a copy of the SLURM template - slurm_script_path = os.path.join(casedir, batch_case + ".sh") - shutil.copy("srun_template.sh", slurm_script_path) - - # If using debug node, comment out time and replace with short time - if debugnode: - writelines = [] - with open(slurm_script_path, 'r') as SPATH: - for line in SPATH: - writelines.append(('# ' if '--time' in line else '') + line.strip()) - with open(slurm_script_path, 'w') as SPATH: - for line in writelines: - SPATH.writelines(line + '\n') - SPATH.writelines("#SBATCH --time=01:00:00\n") - SPATH.writelines("#SBATCH --partition=debug\n") - - # Append additional settings to launch the actual run script - with open(slurm_script_path, 'a') as SPATH: - SPATH.writelines(f"#SBATCH --job-name={batch_case}\n") - SPATH.writelines(f"#SBATCH --output={os.path.join(casedir, 'slurm-%j.out')}\n\n") - SPATH.writelines("#load your default settings\n") - SPATH.writelines(". $HOME/.bashrc\n\n") - SPATH.writelines(f"sh {os.path.join(casedir, 'call_' + batch_case + ext)}\n") - - SPATH.close() - - -def generate_parallel_cases_batch_scripts(envVar): - """ - Generate batch case with ReEDS specific instructions for each case - """ - for i, case in enumerate(envVar['caseList']): - casename = envVar['casenames'][i] - batch_case = f"{envVar['BatchName']}_{casename}" - casedir = os.path.join(envVar['reeds_path'], 'runs', batch_case) - - write_batch_script( - case, - batch_case, - envVar['reeds_path'], - casedir, - envVar['caseSwitches'][i], - envVar['cases_filename'], - envVar['hpc'], - envVar['startiter'], - envVar['niter'], - envVar['ccworkers'], - envVar['BatchName'], - casename, - ) - - now = datetime.isoformat(datetime.now()) - try: - with open(os.path.join(casedir, 'meta.csv'), 'a') as METAFILE: - METAFILE.write(f'0,end,,{now},\n') - except Exception as e: - print(f"[Warning] meta.csv not found or not writeable for {casename}: {e}") - - -def launch_single_case_run( - options, caseSwitches, niter, reeds_path, ccworkers, startiter, - BatchName, case, cases_filename, hpc=False, debugnode=False -): - - ### For testing/debugging - # caseSwitches = caseSwitches[0] - # options = caseList[0] - ### Inferred inputs - batch_case = f'{BatchName}_{case}' - casedir = os.path.join(reeds_path,'runs',batch_case) - - write_batch_script( - options, - batch_case, - reeds_path, - casedir, - caseSwitches, - cases_filename, - hpc, - startiter, - niter, - ccworkers, - BatchName, - case, - ) - - ### ===================================================================================== - ### --- CALL THE CREATED BATCH FILE --- - ### ===================================================================================== - # If you're not running on eagle or AWS... - if (not hpc) & (not int(caseSwitches['AWS'])): - # Start the command prompt similar to the sequential solve - # - waiting for it to finish before starting a new thread - if LINUXORMAC: - print("Starting the run for case " + batch_case) - # Give execution rights to the shell script - os.chmod(os.path.join(casedir, 'call_' + batch_case + ext), 0o777) - # Open it up - note the in/out/err will be written to the shellscript parameter - shellscript = subprocess.Popen( - [os.path.join(casedir, 'call_' + batch_case + ext)], shell=True) - # Wait for it to finish before killing the thread - shellscript.wait() - else: - if int(caseSwitches['keep_run_terminal']) == 1: - terminal_keep_flag = ' /k ' - else: - terminal_keep_flag = ' /c ' - os.system('start /wait cmd' + terminal_keep_flag + os.path.join(casedir, 'call_' + batch_case + ext)) - - elif hpc: - write_case_submission_script( - casedir, batch_case, debugnode=debugnode, - ) - - batchcom = "sbatch " + os.path.join(casedir, batch_case + ".sh") - subprocess.Popen(batchcom.split()) - - elif int(caseSwitches['AWS']): - print("Starting the run for case " + batch_case) - # Give execution rights to the shell script - os.chmod(os.path.join(casedir, 'call_' + batch_case + ext), 0o777) - # Issue a nohup (no hangup) command and direct output to - # case-specific txt files in the root of the repository - shellscript = subprocess.Popen( - ['nohup ' + os.path.join(casedir, 'call_' + batch_case + ext) + " > " +os.path.join(casedir,batch_case+ ".txt") ], - stdin=open(os.path.join(casedir,batch_case+"_in.txt"),'w'), - stdout=open(os.path.join(casedir,batch_case+"_out.txt"),'w'), - stderr=open(os.path.join(casedir,batch_case+"_err.log"),'w'), - shell=True,preexec_fn=os.setpgrp) - # Wait for it to finish before killing the thread - shellscript.wait() - - ### Record the ending time - now = datetime.isoformat(datetime.now()) - try: - with open(os.path.join(casedir,'meta.csv'), 'a') as METAFILE: - METAFILE.writelines('0,end,,{},\n'.format(now)) - except Exception as e: - print(f"[Warning] meta.csv not found or not writeable for {batch_case}: {e}") - - -def main( - BatchName='', cases_suffix='', single='', simult_runs=0, - forcelocal=False, skip_checks=False, - debug=False, debugnode=False, cases_per_node=1, - dryrun=False, - ): - """ - Executes parallel solves based on cases in 'cases.csv' - """ - print(" ") - print(" ") - print("---------------------------------------------------------------------------------------------------------------------") - print(" ") - print(" +++++++++++++++ ") - print(" +++++++++++++++++++++++ ") - print("=+++++++++++++++++++++++++ ") - print("+++++++++++++++++++++++++++ ############ ########### ############# ######### ") - print("++++++++++++++ +++++++++++++ ############## ## ### #### ### ") - print("++++++++++++ ++++++++++++++ #### #### ## ### ### ## ") - print("+++++++++++ ++++++++++++++ #### ##### ######### ## ### ### ## ") - print("+++++++++ ++++++++ #### ##### ############# ## ### ## #### ") - print("++++++++ ++++++++++ ############# #### ##### ########### ### ## ###### ") - print("+++++++++++++ +++++++++++ ############ ############### ## ### ## #### ") - print("++++++++++++ +++++++++++ #### ##### ############### ## ### ### ### ") - print("+++++++++++ +++++++++++ #### ##### #### ## ### ### ### ") - print("++++++++++ +++++++++++++ #### #### #### ## ### ### ### ") - print("+++++++++ +++++++++++++ #### ##### ############# ## ### ##### ### ### ") - print("++++++++ +++++++++++++ #### ##### ########### ############ ########### ######### ") - print("++++++++ +++++++++++ ") - print(" +++++ ++++++++++ ") - print(" ++ ++++ ") - print(" ") - print("---------------------------------------------------------------------------------------------------------------------") - print(" ") - print(" ") - - # Gather user inputs before calling GAMS programs - envVar = setupEnvironment( - BatchName=BatchName, cases_suffix=cases_suffix, - single=single, simult_runs=simult_runs, - forcelocal=forcelocal, skip_checks=skip_checks, - debug=debug, debugnode=debugnode, cases_per_node=cases_per_node, - dryrun=dryrun, - ) - - if (envVar['hpc']) and (envVar['cases_per_hpc_node'] > 1): - # Write each .sh script for each case individually - generate_parallel_cases_batch_scripts(envVar) - - # Write the slurm scripts for parallel runs and - # submit them to the HPC - submit_slurm_parallel_jobs( - reeds_path=envVar['reeds_path'], - BatchName=envVar['BatchName'], - casenames=envVar['casenames'], - cases_per_node=envVar['cases_per_hpc_node'], - debugnode=envVar['debugnode'], - ) - - else: - # Threads are created which will handle each case individually - createmodelthreads(envVar) - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--BatchName', '-b', type=str, default='', - help='Name for batch of runs') - parser.add_argument('--cases_suffix', '-c', type=str, default='', - help='Suffix for cases CSV file') - parser.add_argument('--single', '-s', type=str, default='', - help='Name of a single case to run (or comma-delimited list)') - parser.add_argument('--simult_runs', '-r', type=int, default=0, - help='Number of simultaneous runs. If negative, run all simultaneously.') - parser.add_argument('--forcelocal', '-l', action='store_true', - help='Force model to run locally instead of submitting a slurm job') - parser.add_argument('--skip_checks', '-f', action="store_true", - help="Force run, skipping checks on conda environment and switches") - parser.add_argument('--debug', '-d', action='count', default=0, - help="Run in debug mode (same behavior as debug switch in cases.csv)") - parser.add_argument('--debugnode', '-n', action="store_true", - help="Run using debug specifications for slurm on an hpc system") - parser.add_argument('--cases_per_node', '-p', type=int, default=None, - help="Number of ReEDS cases to run concurrently on a single HPC node. " - "If not provided, the user will be prompted to specify it.") - parser.add_argument('--dryrun', '-t', action='store_true', - help="Check inputs but don't start runs") - - args = parser.parse_args() - - main( - BatchName=args.BatchName, cases_suffix=args.cases_suffix, single=args.single, - simult_runs=args.simult_runs, forcelocal=args.forcelocal, skip_checks=args.skip_checks, - debug=args.debug, debugnode=args.debugnode, cases_per_node=args.cases_per_node, - dryrun=args.dryrun, - ) diff --git a/runfiles.csv b/runfiles.csv deleted file mode 100644 index 1a60f823..00000000 --- a/runfiles.csv +++ /dev/null @@ -1,525 +0,0 @@ -filename,filepath,required_if,aggfunc,disaggfunc,region_col,fix_cols,i_col,wide,header,key,post_copy,GAMStype,GAMSname,comment,notes -#,for input files only,"conditions determinining whether or not the file is required (1 if always required, 0 if always optional)",,,,,,1 if any parameters are in wide format,0 if file has column labels,,files are created after copy_files,for auto-imported files,,, -i.csv,inputs/sets/i.csv,1,ignore,ignore,,,,,,,1,set,i,generation technologies, -ctt.csv,inputs/sets/ctt.csv,1,ignore,ignore,,,,,,,1,set,ctt,cooling technology type, -wst.csv,inputs/sets/wst.csv,1,ignore,ignore,,,,,,,1,set,wst,water source type, -w.csv,inputs/sets/w.csv,1,ignore,ignore,,,,,,,1,set,w,form of water use (withdrawal or consumption), -geotech.csv,inputs/sets/geotech.csv,1,ignore,ignore,,,,,,,1,set,geotech,broader geothermal categories, -i_subtech.csv,inputs/sets/i_subtech.csv,1,ignore,ignore,,,,,,,1,set,i_subtech,technology subset categories, -i_h2_ptc_gen.csv,inputs/sets/i_h2_ptc_gen.csv,1,ignore,ignore,,,,,,,1,set,i_h2_ptc_gen,technology subset category for clean generators which qualify for the hydrogen production tax credit, -sdbin.csv,inputs/sets/sdbin.csv,1,ignore,ignore,,,,,,,1,set,sdbin,storage duration bins, -tg.csv,inputs/sets/tg.csv,1,ignore,ignore,,,,,,,1,set,tg,tech groups for growth constraints, -pcat.csv,inputs/sets/pcat.csv,1,ignore,ignore,,,,,,,1,set,pcat,prescribed capacity categories, -ccseason.csv,,1,ignore,ignore,,,,,,,1,set,ccseason,seasons used for capacity credit; cold is Oct 15-April 14 and hot is April 15-Oct 14, -quarter.csv,inputs/sets/quarter.csv,1,ignore,ignore,,,,,,,1,set,quarter,original h17 seasons (four per year), -month.csv,inputs/sets/month.csv,1,ignore,ignore,,,,,,,1,set,month,calendar months in a year, -RPSCat.csv,inputs/sets/RPSCat.csv,1,ignore,ignore,,,,,,,,,RPSCat,, -aclike.csv,inputs/sets/aclike.csv,1,ignore,ignore,,,,,0,,,,aclike,, -acp_disallowed.csv,inputs/state_policies/acp_disallowed.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,*st,"RPSCat,val",,,0,,,,,, -acp_prices.csv,inputs/state_policies/acp_prices.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,st,,1,0,,,,,, -tscbin.csv,,1,ignore,ignore,,,,,,,1,set,tscbin,transmission upgrade supply curve bins, -numpartitions.csv,,1,ignore,ignore,,,,,0,,,,,, -agglevels.csv,,1,ignore,ignore,ignore,,,,0,,,,,, -aggreg.csv,,1,ignore,ignore,ignore,,,0,0,,,,,, -aggreg2anchorreg.csv,,1,ignore,ignore,ignore,,,0,0,,,,,, -anchorreg2aggreg.csv,,1,ignore,ignore,ignore,,,0,0,,,,,, -allt.csv,inputs/sets/allt.csv,1,ignore,ignore,,,,,,,,,allt,, -alpha.csv,inputs/fuelprices/alpha_{ngscen}.csv,1,ignore,ignore,wide_cendiv,t,,1,0,,,,,, -bio_supplycurve.csv,inputs/supply_curve/bio_supplycurve.csv,1,ignore,ignore,usda_region,,,,0,,,,,, -bioclass.csv,inputs/sets/bioclass.csv,1,ignore,ignore,,,,,,,,,bioclass,, -can_exports.csv,inputs/canada_imports/can_exports.csv,int(sw.GSw_Canada) != 0,sum,ignore,r,wide,,1,0,,,,,, -can_exports_h_frac.csv,,1,ignore,ignore,,,,,0,,,,,, -can_exports_szn_frac.csv,inputs/canada_imports/can_exports_szn_frac.csv,int(sw.GSw_Canada) != 0,ignore,ignore,,,,,0,,,,,, -can_imports.csv,inputs/canada_imports/can_imports.csv,int(sw.GSw_Canada) != 0,sum,ignore,r,wide,,1,0,,,,,, -can_imports_capacity.csv,,1,sum,ignore,*r,t,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -can_imports_quarter_frac.csv,inputs/canada_imports/can_imports_quarter_frac.csv,int(sw.GSw_Canada) != 0,ignore,ignore,,,,,0,,,,,, -can_imports_szn_frac.csv,,1,ignore,ignore,,,,,0,,1,,,, -cangrowth.csv,inputs/load/cangrowth.csv,int(sw.GSw_Canada) != 0,ignore,ignore,st,,,1,0,,,,,, -canmexload.csv,,1,sum,ignore,*r,h,,0,0,,1,,,, -cap_cspns.csv,,1,sum,ignore,*r,t,,0,0,,1,,,,disaggfunc set to ignore because data is pulled from the county-indexed generator database -cap_existing_hydro.csv,inputs/hydro/cap_existing_hydro.csv,1,ignore,ignore,,t,,0,0,,,,,, -cap_existing_psh.csv,inputs/storage/cap_existing_psh.csv,(int(sw.GSw_Storage) != 0) and ((int(sw.GSw_HydroPSHDurData) == 1) or (sw.GSw_HydroStorInMaxFrac == 'data')),sum,ignore,r,"*i,v",,0,0,,,,,, -cap_hyd_ccseason_adj.csv,,1,mean,uniform,r,"*i,ccseason",,0,0,,1,,,, -cap_hyd_szn_adj.csv,,1,mean,uniform,r,"*i,szn",,0,0,,1,,,, -cap_limit.csv,,1,ignore,ignore,,,,1,0,,,,,, -cap_penalty.csv,inputs/financials/cap_penalty.csv,1,ignore,ignore,,tg,,,0,,,,,, -capnonrsc.csv,,1,sum,ignore,r,i,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -capnonrsc_energy.csv,,1,sum,ignore,r,i,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -cappayments_ba.csv,inputs/capacity_exogenous/cappayments_ba.csv,1,ignore,ignore,,*r,,0,0,,,,,,not done but not used -caprsc.csv,,1,sum,ignore,r,i,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -ccreg.csv,,1,ignore,ignore,,,,,0,,,,,, -ccs_link.csv,inputs/emission_constraints/ccs_link.csv,1,ignore,ignore,,,,,0,,,,,, -ccs_link_water.csv,inputs/emission_constraints/ccs_link_water.csv,1,ignore,ignore,,,,,0,,,,,, -ccseason_dates.csv,,1,ignore,ignore,,,,,0,,,,,, -ccsflex_cat.csv,inputs/sets/ccsflex_cat.csv,1,ignore,ignore,,,,,,,,,ccsflex_cat,, -ccsflex_perf.csv,,1,ignore,ignore,,,,,0,,,,,, -cd_beta0.csv,inputs/fuelprices/cd_beta0.csv,1,ignore,ignore,*cendiv,,,,0,,,,,, -cd_beta0_allsector.csv,inputs/fuelprices/cd_beta0_allsector.csv,1,ignore,ignore,*cendiv,,,,0,,,,,, -cendivweights.csv,inputs/fuelprices/cendivweights.csv,1,mean,ignore,r_cendiv,wide,,1,0,,,,,,Includes two region definitions; disaggfunc set to ignore because data is already at county resolution in inputs folder -ces_fraction.csv,inputs/state_policies/ces_fraction.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,st,,1,0,,,,,, -cf_hyd.csv,,1,mean,uniform,r,"*i,szn,t",,0,0,,1,,,, -cf_vre.csv,,1,mean_cap,ignore,r,"*i,h",*i,0,0,,1,,,,disaggfunc set to ignore because data will be written in correct spatial resolution by hourly_writetimeseries in hourly_repperiods.py -climate_UnappWaterMult.csv,,1,ignore,ignore,,,,,0,,1,,,,created by hourly_writetimeseries.py from temp_UnappWaterMult.csv data -climate_UnappWaterMultAnn.csv,,1,ignore,ignore,,,,,0,,1,,,,created by climateprep.py -climate_UnappWaterSeaAnnDistr.csv,,1,ignore,ignore,,,,,0,,1,,,,created by hourly_writetimeseries.py from temp_UnappWaterSeaAnnDistr.csv data -climate_heuristics_finalyear.csv,,1,ignore,ignore,,,,,0,,,,,, -climate_heuristics_yearfrac.csv,,1,ignore,ignore,,,,,0,,,,,, -climate_hydadjann.csv,,1,ignore,ignore,,,,,0,,1,,,,created by climateprep.py -climate_hydadjsea.csv,,1,ignore,ignore,,,,,0,,1,,,,created by hourly_writetimeseries.py from temp_hydadjsea.csv data -climate_loaddelta_timeslice.csv,,1,ignore,ignore,,"r,h",,1,0,,,,,,not done but rarely used; ignore for now -climate_param.csv,inputs/sets/climate_param.csv,1,ignore,ignore,,,,,,,,,climate_param,, -co2_cap.csv,,int(sw.GSw_AnnualCap) != 0,ignore,ignore,,,,0,0,,,,,, -co2_capture_incentive.csv,,1,ignore,ignore,,,,,0,,,,,, -co2_site_char.csv,inputs/ctus/co2_site_char.csv,1,ignore,ignore,,,,0,,,,,,, -co2_tax.csv,,int(sw.GSw_CarbTax) != 0,ignore,ignore,,,,0,,,,,,, -coal_price.csv,inputs/fuelprices/coal_{coalscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, -construction_schedules.csv,inputs/financials/construction_schedules_{construction_schedules_suffix}.csv,1,ignore,ignore,,,,1,0,,,,,, -construction_times.csv,inputs/financials/construction_times_{construction_times_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -consume_char.csv,inputs/consume/consume_char_{GSw_H2_Inputs}.csv,int(sw.GSw_H2) != 0,ignore,ignore,,"*i,t,parameter",,0,0,,,,,, -consumecat.csv,inputs/sets/consumecat.csv,1,ignore,ignore,,,,,,,,,consumecat,, -consumechardac.csv,,1,ignore,ignore,,"*i,t,variable",,0,0,,,,,, -cost_cap_mult.csv,inputs/waterclimate/cost_cap_mult.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, -cost_hurdle_country.csv,inputs/transmission/cost_hurdle_country.csv,1,ignore,ignore,*country,,,,0,,,,,, -cost_hurdle_intra.csv,inputs/transmission/cost_hurdle_intra.csv,1,ignore,ignore,,t,,,0,,,,,, -cost_hurdle_rate1.csv,,1,ignore,ignore,,t,,0,0,,1,,,, -cost_hurdle_rate2.csv,,1,ignore,ignore,,t,,0,0,,1,,,, -cost_opres.csv,,1,ignore,ignore,,,,,0,,,,,, -cost_opres_default.csv,inputs/plant_characteristics/cost_opres_default.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, -cost_opres_market.csv,inputs/plant_characteristics/cost_opres_market.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, -cost_vom.csv,,1,mean,ignore,r,"i,v,t",,0,0,,1,,,,ReEDS-to-PLEXOS output -cost_vom_mult.csv,inputs/waterclimate/cost_vom_mult.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, -county2zone.csv,,1,ignore,ignore,,,,,,,1,,,, -county2zone_original.csv,,1,ignore,ignore,,,,,,,1,,,, -crf.csv,,1,ignore,ignore,,,,0,,,,,,, -crf_co2_incentive.csv,,1,ignore,ignore,,,,,0,,,,,, -crf_h2_incentive.csv,,1,ignore,ignore,,,,,0,,,,,, -csapr_cat.csv,inputs/sets/csapr_cat.csv,1,ignore,ignore,,,,,,,,,csapr_cat,, -csapr_group.csv,inputs/sets/csapr_group.csv,1,ignore,ignore,,,,,,,,,csapr_group,, -csapr_group1_ex.csv,inputs/emission_constraints/csapr_group1_ex.csv,int(sw.GSw_CSAPR) != 0,ignore,ignore,*st,,,,0,,,,,, -csapr_group2_ex.csv,inputs/emission_constraints/csapr_group2_ex.csv,int(sw.GSw_CSAPR) != 0,ignore,ignore,*st,,,,0,,,,,, -csapr_ozone_season.csv,inputs/emission_constraints/csapr_ozone_season.csv,int(sw.GSw_CSAPR) != 0,ignore,ignore,st,,,,0,,,,,, -ctus_r_cs_spurlines_200mi.csv,,1,ignore,ignore,,,,,,,1,,,, -currency_incentives.csv,inputs/financials/currency_incentives.csv,1,ignore,ignore,,,,,0,,,,,, -dac_elec.csv,inputs/consume/dac_elec_{dacscen}.csv,int(sw.GSw_DAC) != 0,ignore,ignore,,,,1,0,,,,,, -dac_gas.csv,inputs/consume/dac_gas_{GSw_DAC_Gas_Case}.csv,int(sw.GSw_DAC) != 0,ignore,ignore,,,,1,0,,1,,,, -deflator.csv,inputs/financials/deflator.csv,1,ignore,ignore,,,,,0,,,,,, -degradation_annual.csv,inputs/degradation/degradation_annual_{degrade_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -demonstration_plants.csv,inputs/capacity_exogenous/demonstration_plants.csv,int(sw.GSw_NuclearDemo) != 0,sum,ignore,r,"t,i,coolingwatertech,ctt,wst,notes",i,0,0,,,,,, -depreciation_schedules.csv,inputs/financials/depreciation_schedules_{depreciation_schedules_suffix}.csv,1,ignore,ignore,,,,1,0,,,,,, -diagnose.gms,postprocessing/diagnose/diagnose.gms,1,ignore,ignore,,,,,,,,,,, -disagg_geosize.csv,,1,ignore,ignore,,,,,0,,,,,, -disagg_hydroexist.csv,inputs/disaggregation/disagg_hydroexist.csv,1,ignore,ignore,,,,,0,,,,,, -disagg_population.csv,inputs/disaggregation/county_population.csv,1,ignore,ignore,FIPS,,,,0,,1,,,, -disagg_state_lpf.csv,inputs/disaggregation/county_state_lpf.csv,1,ignore,ignore,FIPS,,,,0,,1,,,, -distance_reinforcement.csv,,1,ignore,ignore,r,"*i,rscbin",*i,0,0,,1,,,, -distance_spur.csv,,1,ignore,ignore,r,"*i,rscbin",*i,0,0,,1,,,, -distpvcap.csv,inputs/dgen_model_inputs/{distpvscen}/distpvcap_{distpvscen}.csv,1,sum,ignore,r,wide,,1,0,,,,,, -dollaryear_consume.csv,inputs/consume/dollaryear.csv,int(sw.GSw_DAC) != 0,ignore,ignore,,,,,0,,,,,,Do we really need 3 separate instances of dollaryear? -dollaryear_plant.csv,inputs/plant_characteristics/dollaryear.csv,1,ignore,ignore,,,,,0,,,,,,Do we really need 3 separate instances of dollaryear? -dollaryear_fuel.csv,inputs/fuelprices/dollaryear.csv,1,ignore,ignore,,,,,0,,,,,, -dollaryear_sc.csv,inputs/supply_curve/dollaryear.csv,1,ignore,ignore,,,,,0,,,,,, -dr_shed_avail_scalar.csv,inputs/demand_response/dr_shed_avail_scalar.csv,1,ignore,ignore,,,,,0,,,,,, -dr_shed_cap.csv,inputs/supply_curve/dr_shed_cap_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,wide,tech,,1,0,,,,,,done -dr_shed_cost.csv,inputs/supply_curve/dr_shed_cost_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,wide,tech,,1,0,,,,,,done -dr_shed_capacity_scalar.csv,inputs/demand_response/dr_shed_capacity_scalar_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,done -dr_shed_hourly.h5,inputs/profiles_dr/dr_shed_hourly_{dr_shedscen}.h5,int(sw.GSw_DRShed) != 0,ignore,ignore,wide,"datetime,year",,1,keepindex,,,,,,Agg/Disagg handled in hourly_load -e.csv,inputs/sets/e.csv,1,ignore,ignore,,,,,0,,,,e,, -eall.csv,inputs/sets/eall.csv,1,ignore,ignore,,,,,,,,,eall,, -emit_rate.csv,,1,ignore,ignore,,"etype,e,i,v,r",,0,0,,,,,,ReEDS-to-PLEXOS output -emitrate.csv,inputs/emission_constraints/emitrate.csv,1,ignore,ignore,,,,,0,,,,,, -energy_communities.csv,inputs/financials/energy_communities.csv,1,ignore,ignore,,,,,0,,,,,,region aggregation and filtering is handled in copy_files -etype.csv,inputs/sets/etype.csv,1,ignore,ignore,,,,,,,,,etype,, -eval_period_adj_mult.csv,,1,ignore,ignore,,,,,0,,,,,, -exog_cap_geohydro.csv,inputs/capacity_exogenous/exog_cap_geohydro_{GSw_SitingGeo}.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,region,"*tech,sc_point_gid,year",,0,0,,,,,, -exog_cap_upv.csv,inputs/capacity_exogenous/exog_cap_upv_{GSw_SitingUPV}.csv,1,ignore,ignore,region,"*tech,sc_point_gid,year",,0,0,,,,,, -exog_cap_wind-ons.csv,inputs/capacity_exogenous/exog_cap_wind-ons_{GSw_SitingWindOns}.csv,1,ignore,ignore,region,"*tech,sc_point_gid,year",,0,0,,,,,, -f.csv,inputs/sets/f.csv,1,ignore,ignore,,,,,,,,,f,, -financials_hydrogen.csv,inputs/financials/financials_hydrogen.csv,int(sw.GSw_H2) != 0,ignore,ignore,,,,,0,,,,,, -financials_sys.csv,inputs/financials/financials_sys_{financials_sys_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -financials_tech.csv,inputs/financials/financials_tech_{financials_tech_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -financials_transmission.csv,inputs/financials/financials_transmission_{financials_trans_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -financing_risk_mult.csv,,1,ignore,ignore,,,,,0,,,,,, -firm_import_limit.csv,,1,ignore,ignore,,,,,0,,1,,,, -firstyear.csv,,1,ignore,ignore,,,,,0,,1,,,, -years_until_endogenous.csv,inputs/plant_characteristics/years_until_endogenous.csv,1,ignore,ignore,,,,,0,,,,,, -flex_frac_all.csv,,1,mean,population,r,"*flextype,h,wide",,1,0,,1,,,, -flex_type.csv,inputs/sets/flex_type.csv,1,ignore,ignore,,,,,,,,,flex_type,, -forced_retirements.csv,inputs/state_policies/forced_retirements.csv,1,ignore,ignore,st,"*i,t",,0,0,,,,,, -forceperiods.csv,,1,ignore,ignore,,,,,,,,,,, -frac_h_ccseason_weights.csv,,1,ignore,ignore,,,,,,,,,,, -frac_h_month_weights.csv,,1,ignore,ignore,,,,,,,,,,, -frac_h_quarter_weights.csv,,1,ignore,ignore,,,,,,,,,,, -fuel2tech.csv,inputs/sets/fuel2tech.csv,1,ignore,ignore,,,,,0,,,,fuel2tech,, -fuel_price.csv,,1,ignore,ignore,,"i,r",,0,0,,,,,,ReEDS-to-PLEXOS output -fuelbin.csv,inputs/sets/fuelbin.csv,1,ignore,ignore,,,,,,,,,fuelbin,, -futurefiles.csv,inputs/userinput/futurefiles.csv,1,ignore,ignore,,,,,0,,,,,, -gb.csv,inputs/sets/gb.csv,1,ignore,ignore,,,,,,,,,gb,, -gbin.csv,inputs/sets/gbin.csv,1,ignore,ignore,,,,,,,,,gbin,, -gbin_min.csv,inputs/growth_constraints/gbin_min.csv,1,ignore,ignore,,,,,0,,,,,, -gen_mandate_tech_list.csv,,1,ignore,ignore,,,,,0,,,,,, -gen_mandate_trajectory.csv,,1,ignore,ignore,,,,,0,,,,,, -geo_discovery_factor.csv,inputs/geothermal/geo_discovery_factor_{geohydrosupplycurve}.csv,int(sw.GSw_Geothermal) != 0,mean,uniform,r,*i,*i,0,0,,,,,, -geo_discovery_rate.csv,inputs/geothermal/geo_discovery_{geodiscov}.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,,,,0,,,,,,, -geo_fom.csv,,1,mean,uniform,r,*i,,0,0,,1,,,, -geo_fom_mult.csv,,1,ignore,ignore,,0,,0,,,,,,, -geo_retirements.csv,,1,sum,ignore,r,"i,v,wide",,1,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -geo_rsc.csv,inputs/geothermal/geo_rsc_ATB_2023.csv,int(sw.GSw_Geothermal) != 0,sc_cat,geosize,r,*i,,0,0,value,,,,, -geocapcostmult.csv,,1,ignore,ignore,,,,1,0,,,,,, -geoexist.csv,,1,ignore,ignore,r,*i,,0,0,,1,,,,written to proper spatial aggregation in writecapdat.py -growth_bin_size_mult.csv,inputs/growth_constraints/growth_bin_size_mult.csv,1,ignore,ignore,,,,,0,,,,,, -growth_limit_absolute.csv,inputs/growth_constraints/growth_limit_absolute.csv,1,ignore,ignore,,,,,0,,,,,, -growth_penalty.csv,inputs/growth_constraints/growth_penalty.csv,1,ignore,ignore,,,,,0,,,,,, -gswitches.csv,,1,ignore,ignore,,,,,0,,,,,, -h2_ba_share.csv,inputs/consume/h2_demand_county_share.csv,int(sw.GSw_H2) != 0,sum,ignore,*r,t,,0,0,,,,,, -gwp.csv,inputs/emission_constraints/gwp.csv,1,ignore,ignore,,,,,0,,,,gwp,, -h2_existing_smr_cap.csv,,1,sum,population,*r,t,,0,0,,1,,,, -h_preh.csv,,1,ignore,ignore,,,,,0,,,,,, -h2_leakage_rate.csv,inputs/emission_constraints/h2_leakage_rate.csv,1,ignore,ignore,,,,,0,,,,,, -h2_exogenous_demand.csv,inputs/consume/h2_exogenous_demand.csv,int(sw.GSw_H2) != 0,ignore,ignore,,p,,1,0,,,,,, -h2_pipeline_cap_cost_mult.csv,,1,ignore,ignore,,,,,,,1,,,, -h2_ptc.csv,,1,ignore,ignore,,,*i,,0,,,,,, -h2_st.csv,inputs/sets/h2_st.csv,1,ignore,ignore,,,,,,,,,h2_st,, -h2_stor.csv,inputs/sets/h2_stor.csv,1,ignore,ignore,,,,,0,,,,h2_stor,, -h2_storage_rb.csv,,int(sw.GSw_H2) != 0,ignore,ignore,rb,,,0,0,,1,,,, -h2_transport_and_storage_costs.csv,inputs/consume/h2_transport_and_storage_costs.csv,int(sw.GSw_H2) != 0,ignore,ignore,,,,,,,,,,, -h_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, -h_ccseason_prm.csv,,1,ignore,ignore,,,,,0,,,,,, -h_dt_szn.csv,,1,ignore,ignore,,,,,0,,1,,,, -h_szn.csv,,1,ignore,ignore,,,,0,0,,,,,, -h_szn_end.csv,,1,ignore,ignore,,,,,0,,,,,, -h_szn_start.csv,,1,ignore,ignore,,,,,0,,,,,, -heat_rate.csv,,1,ignore,ignore,,"i,v,r",,0,0,,,,,,ReEDS-to-PLEXOS output -heat_rate_adj.csv,inputs/plant_characteristics/heat_rate_adj.csv,1,ignore,ignore,,,,,0,,,,,, -heat_rate_mult.csv,inputs/waterclimate/heat_rate_mult.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, -heat_rate_penalty_spin.csv,inputs/plant_characteristics/heat_rate_penalty_spin.csv,1,ignore,ignore,,,,,0,,,,,, -hierarchy.csv,,1,first,ignore,*r,"nercr,transreg,transgrp,cendiv,st,interconnect,country,usda_region,h2ptcreg",,0,0,,1,,,,post_copy column set to 1 since copy_files filters this file separately -hierarchy_itlgrp.csv,,1,ignore,ignore,,,,,0,,1,,,,post_copy column set to 1 since copy_files copies this file separately -hierarchy_original.csv,,1,ignore,ignore,,,,,0,,1,,,,post_copy column set to 1 since copy_files copies this file separately -hierarchy_with_res.csv,,1,ignore,ignore,,,,,0,,1,,,,post_copy column set to 1 since copy_files copies this file separately -hintage_char.csv,inputs/sets/hintage_char.csv,1,ignore,ignore,,,,,,,,,hintage_char,, -hintage_data.csv,,1,ignore,ignore,,,,,0,,,,,,handled separately in WriteHintage.py -hmap_allyrs.csv,,1,ignore,ignore,,,,,0,,,,,, -hmap_myr.csv,,1,ignore,ignore,,,,,0,,,,,, -hour_szn_group.csv,,1,ignore,ignore,,,,,,,,,,, -hourly_szn_end.csv,,1,ignore,ignore,,,,,,,,,,, -hourly_szn_start.csv,,1,ignore,ignore,,,,,,,,,,, -hours_hourly.csv,,1,ignore,ignore,,,,,,,,,,, -hset_hourly.csv,,1,ignore,ignore,,,,,,,,,,, -hyd_add_upg_cap.csv,inputs/supply_curve/hyd_add_upg_cap.csv,int(sw.GSw_HydroCapEnerUpgradeType) == 2,sum,hydroexist,r,"i,rscbin,wide",,1,0,,,,,, -hyd_fom.csv,inputs/hydro/hyd_fom.csv,1,mean,uniform,wide,i,,1,0,,,,,, -hydadjann.csv,inputs/climate/{climatescen}/hydadjann.csv,int(sw.GSw_ClimateHydro) != 0,mean,uniform,r,t,,0,0,,,,,, -hydadjsea.csv,inputs/climate/{climatescen}/hydadjsea.csv,int(sw.GSw_ClimateHydro) != 0,mean,uniform,r,"month,t",,0,0,,,,,, -hydcap.csv,inputs/supply_curve/hydcap.csv,1,sum,geosize,wide,"tech,class",,1,0,,,,,, -hydcapadj.csv,inputs/hydro/SeaCapAdj_hy.csv,1,mean,uniform,r,"*i,month",,0,0,,,,,, -hydcf.csv,,1,ignore,ignore,r,"t,*i,month",,0,0,,1,,,, -hydcf_fixed.csv,inputs/hydro/hydcf_fixed.csv,1,mean,uniform,r,"*i,month",,0,0,,,,,, -hydcost.csv,inputs/supply_curve/hydcost.csv,1,mean,uniform,wide,"tech,class",,1,0,,,,,, -hydro_mingen.csv,inputs/hydro/hydro_mingen.csv,1,mean,uniform,r,"*i,quarter",,0,0,,,,,,might be better to do something capacity-weighted -hydrocapcostmult.csv,,1,ignore,ignore,,,,1,0,,,,,, -hydrofrac_policy.csv,inputs/state_policies/hydrofrac_policy.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,"RPS_All,CES",,,0,,,,,, -hydrogen_price.csv,inputs/fuelprices/h2-combustion_{h2combustionfuelscen}.csv,int(sw.GSw_H2Combustion) != 0,ignore,ignore,,,,0,0,,,,,, -i_coolingtech_watersource.csv,inputs/waterclimate/i_coolingtech_watersource.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, -i_coolingtech_watersource_link.csv,inputs/waterclimate/i_coolingtech_watersource_link.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, -i_coolingtech_watersource_upgrades.csv,inputs/upgrades/i_coolingtech_watersource_upgrades.csv,1,ignore,ignore,,,,,0,,,,,, -i_coolingtech_watersource_upgrades_link.csv,inputs/upgrades/i_coolingtech_watersource_upgrades_link.csv,1,ignore,ignore,,,,,0,,,,,, -i_geotech.csv,inputs/sets/i_geotech.csv,1,ignore,ignore,,,,,0,,,,i_geotech,, -i_p.csv,inputs/sets/i_p.csv,1,ignore,ignore,,,,,0,,,,i_p,, -i_water_nocooling.csv,inputs/sets/i_water_nocooling.csv,1,ignore,ignore,,,,,0,,,,i_water_nocooling,, -incentives.csv,inputs/financials/incentives_{incentives_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -inflation.csv,inputs/financials/inflation_{inflation_suffix}.csv,1,ignore,ignore,,,,0,0,,,,,, -interconnection_queues.csv,inputs/capacity_exogenous/interconnection_queues.csv,1,ignore,ignore,r,"tg,r",,1,0,,,,,, -itc_energy_comm_bonus.csv,,1,mean,ignore,r,*i,,0,0,,1,,,, -itc_frac_monetized.csv,,1,ignore,ignore,,,,,0,,,,,, -itc_fractions.csv,,1,ignore,ignore,,"i,country,t",,0,0,,,,,, -ivt.csv,,1,ignore,ignore,,,,,0,,,,,,created in runbatch.py -ivt_step.csv,,1,ignore,ignore,,,,,0,,,,,, -lcclike.csv,inputs/sets/lcclike.csv,1,ignore,ignore,,,,,0,,,,lcclike,, -load_2010.csv,,1,sum,ignore,r,wide,,1,0,,1,,,,disaggfunc set to ignore because load will already be in correct spatial resolution -load_allyear.csv,,1,sum,ignore,*r,"h,t",,0,0,,1,,,,disaggfunc set to ignore because load will already be in correct spatial resolution -load_multiplier.csv,inputs/load/demand_{demandscen}.csv,1,ignore,ignore,,,,,0,,,,,, -load_multiplier_r.csv,,1,ignore,ignore,,,,1,0,,,,,, -loadsite_annual.csv,inputs/load/loadsite_{GSw_LoadSiteTrajectory}.csv,float(sw.GSw_LoadSiteCF) > 0,ignore,ignore,*loadsitereg,t,,,0,,,,,, -maps.gpkg,,1,ignore,ignore,,,,,,,1,,,, -maxage.csv,inputs/plant_characteristics/maxage.csv,1,ignore,ignore,,,,,0,,,,,, -maxdailycf.csv,inputs/plant_characteristics/maxdailycf.csv,int(sw.GSw_MaxDailyCF) != 0,ignore,ignore,,,,,0,,,,,, -mcs_distributions.yaml,inputs/userinput/mcs_distributions_{MCS_dist}.yaml,int(sw.MCS_runs) != 0,ignore,ignore,,,,,,,,,,, -mcs_group_weights.csv,,1,ignore,ignore,,,,,,,,,,, -methane_leakage_rate.csv,,1,ignore,ignore,,,,0,0,,,,,, -mex_growth_rate.csv,inputs/load/mex_growth_rate.csv,1,ignore,ignore,,,,0,,,,,,, -minCF.csv,inputs/plant_characteristics/minCF.csv,int(sw.GSw_MinCF) != 0,ignore,ignore,,,,,0,,,,,, -min_retire_age.csv,inputs/plant_characteristics/min_retire_age.csv,1,ignore,ignore,,,,,0,,,,,, -mingen_fixed.csv,inputs/plant_characteristics/mingen_fixed.csv,int(sw.GSw_MingenFixed) != 0,ignore,ignore,,,*i,,0,,,,,, -minloadfrac0.csv,inputs/plant_characteristics/minloadfrac0.csv,(int(sw.GSw_Mingen) != 0) or (int(sw.GSw_MinLoading) != 0),ignore,ignore,,,,,0,,,,,, -modeled_regions.csv,inputs/userinput/modeled_regions.csv,1,ignore,ignore,,,,,,,,,,, -modeledyears.csv,,1,ignore,ignore,,,,,0,,,,,, -month2quarter.csv,inputs/temporal/month2quarter.csv,1,ignore,ignore,,,,,0,,,,,, -mttr.csv,inputs/plant_characteristics/mttr.csv,1,ignore,ignore,,,tech,,0,,,,,, -natgas_price_cendiv.csv,inputs/fuelprices/ng_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, -national_rps_frac_allScen.csv,inputs/national_generation/national_rps_frac_allScen.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,,,,1,0,,,,,, -net_gen_existing_hydro.csv,inputs/hydro/net_gen_existing_hydro.csv,1,ignore,ignore,,"t,month",,0,0,,,,,, -peak_net_imports.csv,inputs/reserves/peak_net_imports.csv,1,ignore,ignore,nercr,t,,0,0,,,,,, -nexth.csv,,1,ignore,ignore,,,,,0,,,,,, -nexth_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, -nextpartition.csv,,1,ignore,ignore,,,,,0,,,,,, -ng_crf_penalty.csv,,1,ignore,ignore,,,,0,0,,,,,, -ng_crf_penalty_st.csv,inputs/state_policies/ng_crf_penalty_st.csv,1,ignore,ignore,st,*t,,0,0,,,,,, -ng_demand_elec.csv,inputs/fuelprices/ng_demand_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, -ng_demand_tot.csv,inputs/fuelprices/ng_tot_demand_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, -noretire.csv,inputs/sets/noretire.csv,1,ignore,ignore,,,,,0,,,,noretire,, -notvsc.csv,inputs/sets/notvsc.csv,1,ignore,ignore,,,,,0,,,,notvsc,, -nuclear_ba_ban_list.csv,,int(sw.GSw_NukeStateBan) != 0,ignore,ignore,,,,,,,,,,, -nuclear_energy_communities.csv,inputs/financials/nuclear_energy_communities.csv,1,ignore,ignore,,,,,0,,,,,,region aggregation and filtering is handled in copy_files -nuclear_subsidies.csv,inputs/state_policies/nuclear_subsidies.csv,1,ignore,ignore,*st,year,,0,0,,,,,, -numhours.csv,,1,ignore,ignore,,,,,0,,,,,, -numhours_nexth.csv,,1,ignore,ignore,,,,,0,,1,,,, -objective_function_params.yaml,tests/objective_function_params.yaml,1,ignore,ignore,,,,,,,,,,, -offshore_req.csv,inputs/state_policies/offshore_req_{GSw_OfsWindForceScen}.csv,(int(sw.GSw_StateRPS) != 0) and (int(sw.GSw_OfsWind) != 0),ignore,ignore,st,,,1,0,,,,,, -offshore.csv,,1,ignore,ignore,,,,0,none,,,,,, -ofstype.csv,inputs/sets/ofstype.csv,1,ignore,ignore,,,,,,,,,ofstype,, -ofstype_i.csv,inputs/sets/ofstype_i.csv,1,ignore,ignore,,,,,0,,,,ofstype_i,, -ofswind_rsc_mult.csv,,1,ignore,ignore,,,,1,0,,,,,, -oosfrac.csv,inputs/state_policies/oosfrac.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,*st,,,,0,,,,,, -opres_periods.csv,inputs/reserves/opres_periods.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, -orcat.csv,inputs/sets/orcat.csv,1,ignore,ignore,,,,,,,,,orcat,, -orperc.csv,inputs/reserves/orperc.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, -ortype.csv,inputs/sets/ortype.csv,1,ignore,ignore,,,,,,,,,ortype,, -outage_forced_h.csv,,1,ignore,ignore,r,"*i,h",,0,0,,1,,,,handled in outage_rates.py -outage_forced_hourly.h5,,1,ignore,ignore,wide,index,index,1,keepindex,,1,,,,handled in outage_rates.py -outage_forced_static.csv,inputs/plant_characteristics/outage_forced_static.csv,1,ignore,ignore,,,,,none,,,,,, -outage_forced_temperature.csv,inputs/plant_characteristics/outage_forced_temperature_{GSw_OutageScen}.csv,sw.GSw_OutageScen != 'static',ignore,ignore,,,,,0,,,,,, -outage_scheduled_static.csv,inputs/plant_characteristics/outage_scheduled_static.csv,1,ignore,ignore,,,,,0,,,,,, -outage_scheduled_monthly.csv,inputs/plant_characteristics/outage_scheduled_monthly.csv,1,ignore,ignore,,,,,0,,,,,, -p.csv,inputs/sets/p.csv,1,ignore,ignore,,,,,,,,,p,, -peak_ccseason.csv,,1,sum,ignore,*r,"ccseason,t",,0,0,,1,,,,ok because it's load during peak NERC hour -peak_h.csv,,1,sum,ignore,r,"h,wide",,1,0,,1,,,, -peakload.csv,,1,ignore,ignore,,,,,0,,1,,,, -peakload_nercr.csv,,1,ignore,ignore,,,,,0,,1,,,, -period_szn.csv,,1,ignore,ignore,,,,,0,,,,,, -period_szn_user.csv,inputs/temporal/period_szn_{GSw_HourlyClusterAlgorithm}.csv,sw.GSw_HourlyClusterAlgorithm == 'user*',ignore,ignore,,,,,,,,,,, -period_weights.csv,,1,ignore,ignore,,,,,0,,,,,, -periodmap_1yr.csv,,1,ignore,ignore,,,,0,0,,,,,, -pipeline_cost_mult.csv,,int(sw.GSw_H2) != 0,trans_lookup,uniform,"*r,rr",,,0,0,drop_dup_r,1,,,, -plantcat.csv,inputs/sets/plantcat.csv,1,ignore,ignore,,,,,,,,,plantcat,, -plantcharout.csv,,1,ignore,ignore,,"0,2",,0,,,,,,, -plantchar_beccs.csv,inputs/plant_characteristics/{plantchar_beccs}.csv,int(sw.GSw_BECCS) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_biopower.csv,inputs/plant_characteristics/{plantchar_biopower}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_ccsflex_cost.csv,inputs/plant_characteristics/{ccsflexscen}_cost.csv,(int(sw.GSw_CCSFLEX_BYP) != 0) or (int(sw.GSw_CCSFLEX_DAC) != 0) or (int(sw.GSw_CCSFLEX_STO) != 0),ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_ccsflex_perf.csv,inputs/plant_characteristics/{ccsflexscen}_perf.csv,(int(sw.GSw_CCSFLEX_BYP) != 0) or (int(sw.GSw_CCSFLEX_DAC) != 0) or (int(sw.GSw_CCSFLEX_STO) != 0),ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_coal_ccs.csv,inputs/plant_characteristics/{plantchar_coal_ccs}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_coal.csv,inputs/plant_characteristics/{plantchar_coal}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_battery.csv,inputs/plant_characteristics/{plantchar_battery}.csv,int(sw.GSw_Storage) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_csp.csv,inputs/plant_characteristics/{plantchar_csp}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_dr_shed.csv,inputs/plant_characteristics/dr_shed_capcost_scalars_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,used to define plantcharout.csv -plantchar_dr_shed_vom.csv,inputs/plant_characteristics/dr_shed_vom_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,used to define plantcharout.csv -plantchar_dr_shed_fom.csv,inputs/plant_characteristics/dr_shed_fom_{dr_shedscen}.csv,int(sw.GSw_DRShed) != 0,ignore,ignore,r,"tech,wide",,1,0,,,,,,used to define plantcharout.csv -plantchar_evmc_shape.csv,inputs/plant_characteristics/evmc_shape_{evmcscen}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_evmc_storage.csv,inputs/plant_characteristics/evmc_storage_{evmcscen}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_fuelcell.csv,inputs/plant_characteristics/{plantchar_fuelcell}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_gas_ccs.csv,inputs/plant_characteristics/{plantchar_gas_ccs}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_gas.csv,inputs/plant_characteristics/{plantchar_gas}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_geo.csv,inputs/plant_characteristics/{plantchar_geo}.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_h2combustion.csv,inputs/plant_characteristics/{plantchar_h2combustion}.csv,int(sw.GSw_H2Combustion) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_hydro.csv,inputs/plant_characteristics/{plantchar_hydro}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_nuclear_smr.csv,inputs/plant_characteristics/{plantchar_nuclear_smr}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_nuclear.csv,inputs/plant_characteristics/{plantchar_nuclear}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_ofswind.csv,inputs/plant_characteristics/{plantchar_ofswind}.csv,int(sw.GSw_OfsWind) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_onswind.csv,inputs/plant_characteristics/{plantchar_onswind}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_other.csv,inputs/plant_characteristics/{plantchar_other}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_pvb.csv,inputs/plant_characteristics/pvb_{pvbscen}.csv,int(sw.GSw_PVB) != 0,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_upgrades.csv,inputs/upgrades/{upgradescen}.csv,sw.upgradescen != 'default',ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -plantchar_upv.csv,inputs/plant_characteristics/{plantchar_upv}.csv,1,ignore,ignore,,,,,0,,,,,,used to define plantcharout.csv -poi_cap_init.csv,,1,sum,ignore,*r,,,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -prepost.csv,inputs/sets/prepost.csv,1,ignore,ignore,,,,,,,,,prepost,, -prescribed_builds_wind-ofs.csv,inputs/capacity_exogenous/prescribed_builds_wind-ofs_{GSw_OffshoreFiles}_{GSw_SitingWindOfs}.csv,int(sw.GSw_OfsWind) != 0,sum,ignore,region,year,,0,0,,,,,,disaggfunc set to ignore because data will be read in at the correct spatial resolution -prescribed_builds_wind-ons.csv,inputs/capacity_exogenous/prescribed_builds_wind-ons_{GSw_SitingWindOns}.csv,1,sum,ignore,region,year,,0,0,,,,,,disaggfunc set to ignore because data will be read in at the correct spatial resolution -prescribed_nonRSC.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -prescribed_rsc.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -prescriptivelink0.csv,inputs/sets/prescriptivelink0.csv,1,ignore,ignore,,,,,0,,,,prescriptivelink0,, -prm_initial.csv,,1,ignore,ignore,*r,,,,0,,1,,,, -prm.csv,,1,ignore,ignore,*r,,,,0,,1,,,, -psh_sc_duration.csv,,1,ignore,ignore,,,,,0,,1,,,,Delete once aggregate_regions.py is moved up -psh_supply_curves_duration.csv,inputs/storage/PSH_supply_curves_durations.csv,int(sw.GSw_Storage) != 0,ignore,ignore,,,,,0,,1,,,, -psh_supply_curves_capacity.csv,inputs/supply_curve/PSH_supply_curves_capacity_{pshsupplycurve}.csv,int(sw.GSw_Storage) != 0,sum,geosize,r,wide,,1,0,,,,,, -psh_supply_curves_cost.csv,inputs/supply_curve/PSH_supply_curves_cost_{pshsupplycurve}.csv,int(sw.GSw_Storage) != 0,mean,uniform,r,wide,,1,0,,,,,, -pv_cf_improve.csv,,1,ignore,ignore,,,,0,,,,,,, -pvb_agg.csv,inputs/sets/pvb_agg.csv,1,ignore,ignore,,,,,0,,,,pvb_agg,, -pvb_bir.csv,,1,ignore,ignore,,,,,0,,,,,, -pvb_config.csv,inputs/sets/pvb_config.csv,1,ignore,ignore,,,,,,,,,pvb_config,, -pvb_ilr.csv,,1,ignore,ignore,,,,,0,,,,,, -pvbcapcostmult.csv,,1,ignore,ignore,,,,0,0,,,,,, -r.csv,,1,first,ignore,0,,,0,,,1,,,,disaggfunc set to ignore because this file is dynamic to the user-defined spatial aggregation level -r_ba.csv,,1,ignore,ignore,,,,,,,,,,, -r_cendiv.csv,,1,ignore,ignore,,,,,,,,,,, -r_county.csv,,1,ignore,ignore,,,,,,,,,,, -r_cs.csv,,1,first,ignore,*r,cs,,0,0,,1,,,,disaggfunc set to ignore because the spatial aggregation level of this file is controlled by the agglevel switch -r_cs_distance_mi.csv,,1,mean,ignore,*r,cs,,0,0,,1,,,,disaggfunc set to ignore because the spatial aggregation level of this file is controlled by the agglevel switch -routes_adjacent.csv,,1,trans_lookup,ignore,"*r,rr",,,0,0,,1,,,,disaggfunc set to ignore because the spatial aggregation level of this file is controlled by the agglevel switch -ramprate.csv,inputs/plant_characteristics/ramprate.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, -ramptime.csv,inputs/reserves/ramptime.csv,int(sw.GSw_OpRes) != 0,ignore,ignore,,,,,0,,,,,, -rb.csv,,1,first,ignore,0,,,0,,,1,,,,disaggfunc set to ignore because this file is specifically a collection of all valid BA regions -rb_aggreg.csv,,1,ignore,ignore,,,,,,,,,,, -recstyle.csv,inputs/state_policies/recstyle.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,*st,"RPSCat,style",,,0,,,,,, -rectable.csv,inputs/state_policies/rectable.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st_st,st,,,0,,,,,, -regional_cap_cost_diff.csv,inputs/financials/reg_cap_cost_diff_{reg_cap_cost_diff_suffix}.csv,1,mean,ignore,r,wide,,1,0,,,,,,precursor data to reg_cap_cost_diff.csv -regions.csv,,1,ignore,ignore,,,,,0,,,,,,not done but only for retail -resourceclass.csv,inputs/sets/resourceclass.csv,1,ignore,ignore,,,,,,,,,resourceclass,, -resources.csv,,1,resources,ignore,r,"i,ccreg",i,0,0,,1,,,,disaggfunc set to ignore because this file contains all spatial resolutions valid to the model run -retire_penalty.csv,inputs/financials/retire_penalty.csv,1,ignore,ignore,,,,,0,,,,,, -retirements.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -retirements_energy.csv,,1,sum,ignore,r,"t,i",,0,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -rev_paths.csv,inputs/supply_curve/rev_paths.csv,1,ignore,ignore,,,,,0,,,,,, -rev_transmission_basecost.csv,inputs/transmission/rev_transmission_basecost.csv,1,ignore,ignore,,,,,0,,,,,, -rggi_states.csv,inputs/emission_constraints/rggi_states.csv,int(sw.GSw_RGGI) != 0,ignore,ignore,*st,,,,0,,,,,, -rggicon.csv,inputs/emission_constraints/rggicon.csv,int(sw.GSw_RGGI) != 0,ignore,ignore,,,,0,,,,,,, -rps_fraction.csv,inputs/state_policies/rps_fraction.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,st,,1,0,,,,,, -rsc_combined.csv,,1,sc_cat,ignore,r,"*i,rscbin",*i,0,0,value,1,,,,done for upv/csp/wind; disaggfunc set to ignore because supply curve data is already at county level -rsc_wsc.csv,,1,ignore,ignore,,,,,0,,,,,, -runfiles.csv,runfiles.csv,1,ignore,ignore,ignore,,,0,0,,,,,,so meta -sc_cat.csv,inputs/sets/sc_cat.csv,1,ignore,ignore,,,,,0,,,,sc_cat,, -scalars.csv,inputs/scalars.csv,1,ignore,ignore,,,,,0,,1,,,, -set_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, -set_allh.csv,,1,ignore,ignore,,,,,0,,,,,, -set_allszn.csv,,1,ignore,ignore,,,,,0,,,,,, -set_h.csv,,1,ignore,ignore,,,,,,,,,,, -set_szn.csv,,1,ignore,ignore,,,,,,,,,,, -site_bin_map.csv,,1,ignore,ignore,,,,,0,,,,,, -spur_parameters.csv,,1,ignore,ignore,,,,,0,,,,,,"TODO (only used for plotting so ignoring for now, but should be fixed)" -spurline_cost.csv,,1,ignore,ignore,,,,,0,,1,,,,Delete once aggregate_regions.py is moved up -spurline_sitemap.csv,,1,ignore,ignore,,,,,0,,,,,,handled in writesupplycurves.py -startcost.csv,inputs/plant_characteristics/startcost.csv,int(sw.GSw_StartCost) != 0,ignore,ignore,,,*i,,0,,,,,, -state_cap.csv,inputs/emission_constraints/state_cap.csv,int(sw.GSw_StateCap) != 0,ignore,ignore,*st,t,,0,0,,,,,, -storage_duration.csv,inputs/storage/storage_duration.csv,int(sw.GSw_Storage) != 0,ignore,ignore,,,,,0,,,,,, -storage_duration_pshdata.csv,,1,ignore,ignore,r,"*i,v",,0,0,,1,,,, -storage_mandates.csv,inputs/state_policies/storage_mandates.csv,int(sw.GSw_BatteryMandate) != 0,ignore,ignore,*st,t,,0,0,,,,,, -storinmaxfrac.csv,,1,ignore,ignore,r,"*i,v",,0,0,,1,,,, -stressperiods_user.csv,inputs/temporal/stressperiods_{GSw_PRM_StressModel}.csv,sw.GSw_PRM_StressModel == 'user*',ignore,ignore,,,,,,,,,,, -stressperiods_seed.csv,,1,ignore,ignore,,,,,0,,,,,, -supply_chain_adjust.csv,inputs/financials/supply_chain_adjust.csv,1,ignore,ignore,,,,,0,,,,,, -supplycurve_egs.csv,inputs/supply_curve/supplycurve_egs-reference.csv,int(sw.GSw_Geothermal) != 0,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file -supplycurve_upv.csv,inputs/supply_curve/supplycurve_upv-{GSw_SitingUPV}.csv,1,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file -supplycurve_wind-ofs.csv,inputs/supply_curve/supplycurve_wind-ofs-{GSw_SitingWindOfs}.csv,int(sw.GSw_OfsWind) != 0,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file -supplycurve_wind-ons.csv,inputs/supply_curve/supplycurve_wind-ons-{GSw_SitingWindOns}.csv,1,ignore,ignore,region,,,,0,,,,,,No fix_col (only region_col) because this is an intermediate input file -sw.csv,inputs/sets/sw.csv,1,ignore,ignore,,,,,0,,,,sw,, -switches.csv,,1,ignore,ignore,,,,,0,,,,,, -szn_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, -tc_phaseout_schedule.csv,inputs/financials/tc_phaseout_schedule_{GSw_TCPhaseout_schedule}.csv,int(sw.GSw_TCPhaseout) != 0,ignore,ignore,,,,,0,,,,,, -tech-subset-table.csv,inputs/tech-subset-table.csv,1,ignore,ignore,,,,,0,,,,,, -tech_resourceclass.csv,inputs/techs/tech_resourceclass.csv,1,ignore,ignore,,,,,0,,,,,, -techs.csv,inputs/techs/techs_{techs_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -techs_banned.csv,inputs/state_policies/techs_banned.yaml,1,ignore,ignore,wide,i,,,0,,,,,, -techs_banned_ces.csv,inputs/state_policies/techs_banned_ces.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,wide_st,i,,,0,,,,,, -techs_banned_imports_rps.csv,inputs/state_policies/techs_banned_imports_rps.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,wide_st,i,,,0,,,,,, -techs_banned_rps.csv,inputs/state_policies/techs_banned_rps.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,wide_st,i,,,0,,,,,, -temp_UnappWaterMult.csv,,1,ignore,ignore,,,,,0,,1,,,,intermediate file created in climateprep.py to create climate_UnappWaterMult.csv -temp_UnappWaterSeaAnnDistr.csv,,1,ignore,ignore,,,,,0,,1,,,,intermediate file created in climateprep.py to create climate_UnappWaterSeaAnnDistr.csv -temp_hydadjsea.csv,,1,ignore,ignore,,,,,0,,1,,,,intermediate file created in climateprep.py to create climate_hydadjsea.csv -tg_rsc_cspagg.csv,inputs/sets/tg_rsc_cspagg.csv,1,ignore,ignore,,,,,0,,,,tg_rsc_cspagg,, -tg_rsc_cspagg_tmp.csv,inputs/waterclimate/tg_rsc_cspagg_tmp.csv,(int(sw.GSw_CSP) != 0) and (int(sw.GSw_WaterMain) != 0),ignore,ignore,,,,,0,,,,,, -tg_rsc_upvagg.csv,inputs/sets/tg_rsc_upvagg.csv,1,ignore,ignore,,,,,0,,,,tg_rsc_upvagg,, -timestamps.csv,,1,ignore,ignore,,,,,0,,,,,, -trancap_fut.csv,,1,sum,ignore,"*r,rr","status,trtype,t",,0,0,drop_dup_r,1,,,,disaggfunc set to ignore because all transmisison data will be read into model at the correct spatial resolution -trancap_fut_cat.csv,inputs/sets/trancap_fut_cat.csv,1,ignore,ignore,,,,,,,,,trancap_fut_cat,, -trancap_init_energy.csv,,1,ignore,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,, -trancap_init_prm.csv,,1,ignore,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,, -trancap_init_transgroup.csv,,1,ignore,ignore,,,,0,0,,,,,, -trancap_init_itlgrp.csv,,1,ignore,ignore,,,,0,0,,,,,, -tranloss.csv,,1,trans_lookup,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,,disaggfunc set to ignore because all transmisison data will be read into model at the correct spatial resolution -trans_itc_fractions.csv,,1,ignore,ignore,,,,,0,,,,,, -transmission_capacity_future.csv,inputs/transmission/transmission_capacity_future_{lvl}_{GSw_TransScen}.csv,1,sum,ignore,"r,rr","status,trtype,t",,0,0,drop_dup_r,,,,,'ignore’ in disaggfunc because all transmisison data will be read into model at appropriate spatial resolution -transmission_capacity_future_baseline.csv,inputs/transmission/transmission_capacity_future_{lvl}_baseline.csv,1,sum,ignore,"r,rr","status,trtype,t",,0,0,drop_dup_r,,,,,'ignore’ in disaggfunc because all transmisison data will be read into model at appropriate spatial resolution -transmission_cost_ac.csv,inputs/transmission/transmission_cost_ac_{GSw_TransUpgradeMethod}_{lvl}.h5,1,trans_lookup,ignore,"r,rr",tscbin,,0,0,drop_dup_r,,,,, -transmission_cost_dc.csv,inputs/transmission/transmission_cost_dc_{lvl}.csv,1,trans_lookup,ignore,"r,rr",,,0,0,drop_dup_r,,,,, -transmission_distance.csv,inputs/transmission/transmission_distance_{lvl}.h5,1,trans_lookup,ignore,"r,rr",,,0,0,drop_dup_r,,,,,Stored in wide-format h5 to reduce county filesize but converted to long in copy_files.py -transmission_line_fom.csv,,1,trans_lookup,ignore,"*r,rr",trtype,,0,0,drop_dup_r,1,,,,disaggfunc set to ignore because all transmisison data will be read into model at the correct spatial resolution -trtype.csv,inputs/sets/trtype.csv,1,ignore,ignore,,,,,,,,,trtype,, -unapp_water_sea_distr.csv,inputs/waterclimate/unapp_water_sea_distr.csv,int(sw.GSw_WaterMain) != 0,mean,geosize,r,"wst,wide",,1,0,,,,,, -UnappWaterMult.csv,inputs/climate/{climatescen}/UnappWaterMult.csv,int(sw.GSw_ClimateWater) != 0,mean,uniform,r,"wst,month,t",,0,0,,,,,, -UnappWaterMultAnn.csv,inputs/climate/{climatescen}/UnappWaterMultAnn.csv,int(sw.GSw_ClimateWater) != 0,mean,uniform,r,"wst,t",,0,0,,,,,, -UnappWaterSeaAnnDistr.csv,inputs/climate/{climatescen}/UnappWaterSeaAnnDistr.csv,int(sw.GSw_ClimateWater) != 0,mean,uniform,r,"wst,month,t",,0,0,,,,,, -unbundled_limit_ces.csv,inputs/state_policies/unbundled_limit_ces.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,,,,0,,,,,, -unbundled_limit_rps.csv,inputs/state_policies/unbundled_limit_rps.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,st,,,,0,,,,,, -unitdata.csv,inputs/capacity_exogenous/ReEDS_generator_database_final_{unitdata}.csv,1,ignore,ignore,FIPS,,,,0,,,,,, -unitsize.csv,,1,ignore,ignore,,,,,0,,1,,,, -unitspec_upgrades.csv,inputs/sets/unitspec_upgrades.csv,1,ignore,ignore,,,,,0,,,,unitspec_upgrades,, -upgrade_costs_ccs_coal.csv,,1,ignore,ignore,,,,,0,,,,,, -upgrade_costs_ccs_gas.csv,,1,ignore,ignore,,,,,0,,,,,, -upgrade_hintage_char.csv,inputs/sets/upgrade_hintage_char.csv,1,ignore,ignore,,,,,0,,,,upgrade_hintage_char,, -upgrade_link.csv,inputs/upgrades/upgrade_link.csv,1,ignore,ignore,,,,,0,,,,,, -upgrade_mult_advanced.csv,inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv,int(sw.GSw_UpgradeCost_Mult) != 0,ignore,ignore,,,,,0,,,,,, -upgrade_mult_conservative.csv,inputs/upgrades/upgrade_mult_atb23_ccs_con.csv,int(sw.GSw_UpgradeCost_Mult) == 2,ignore,ignore,,,,,0,,,,,, -upgrade_mult_final.csv,,1,ignore,ignore,,,,,0,,,,,, -upgrade_mult_mid.csv,inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv,"int(sw.GSw_UpgradeCost_Mult) in [0, 4]",ignore,ignore,,,,,0,,,,,, -upgradelink_water.csv,inputs/upgrades/upgradelink_water.csv,1,ignore,ignore,,,,,0,,,,,, -uranium_price.csv,inputs/fuelprices/uranium_{uraniumscen}.csv,1,ignore,ignore,,,,0,0,,,,,, -va_ng_crf_penalty.csv,,1,ignore,ignore,,,,0,0,,,,,, -val_aggreg.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_ba.csv,,1,ignore,ignore,,,,,,,1,,,, -val_itlgrp.csv,,1,ignore,ignore,,,,,,,1,,,, -val_cendiv.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_country.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_county.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_cs.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_hurdlereg.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_interconnect.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_nercr.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_h2ptcreg.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_r.csv,,1,ignore,ignore,0,,,0,none,,1,,,, -val_r_all.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_st.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_transgrp.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_transreg.csv,,1,ignore,ignore,,,,0,none,,,,,, -val_usda_region.csv,,1,ignore,ignore,,,,0,none,,,,,, -var_map.csv,inputs/valuestreams/var_map.csv,1,ignore,ignore,,,,,0,,,,,, -wat_access_cap_cost.csv,inputs/waterclimate/wat_access_cap_cost.csv,int(sw.GSw_WaterMain) != 0,sc_cat,geosize,r,*wst,,0,0,value,,,,, -water_req_psh_10h_1_51.csv,inputs/waterclimate/water_req_psh_10h_1_51.csv,(int(sw.GSw_PSHwatercon) != 0) and (int(sw.GSw_WaterMain) != 0),mean,geosize,r,wide,,1,0,,,,,,not yet ready for county-level disaggregation -water_with_cons_rate.csv,inputs/waterclimate/water_with_cons_rate.csv,int(sw.GSw_WaterMain) != 0,ignore,ignore,,,,,0,,,,,, -wind_retirements.csv,,1,sum,ignore,r,"i,v,wide",,1,0,,1,,,,disaggfunc set to ignore because data will be written at county resolution by writecapdat.py -windcfmult.csv,,1,ignore,ignore,,,,1,0,,,,,, -windcfout.csv,,1,ignore,ignore,,,,1,0,,,,,, -windows.csv,inputs/userinput/windows_{windows_suffix}.csv,1,ignore,ignore,,,,,0,,,,,, -wst_climate.csv,inputs/sets/wst_climate.csv,1,ignore,ignore,,,,,0,,,,wst_climate,, -x.csv,,1,ignore,ignore,,,,,0,,,,,, -x_r.csv,,1,first,ignore,"*x,r",,,0,0,,1,,,,handled in writesupplycurves.py -yearafter.csv,inputs/sets/yearafter.csv,1,ignore,ignore,,,,,,,,,yearafter,, -inputs.gdx,,1,ignore,ignore,,,,,0,,,,,, -plexos_inputs.gdx,,1,ignore,ignore,,,,,0,,,,,, -load.h5,,1,sum,ignore,wide,"year,hour",,1,keepindex,,1,,,,Disaggregation handled in hourly_load.py -recf.h5,,1,recf,ignore,wide,datetime,,1,keepindex,,1,,,, -csp.h5,,1,csp,ignore,wide,datetime,,1,keepindex,,1,,,, -gswitches.txt,,1,ignore,ignore,,,,,0,,,,,, -scalars.txt,,1,ignore,ignore,,,,,0,,,,,, -Augur.py,Augur.py,1,ignore,ignore,,,,,,,,,,, -Project.toml,Project.toml,1,ignore,ignore,,,,,,,,,,, -b_inputs.gms,b_inputs.gms,1,ignore,ignore,,,,,,,,,,, -c_mga.gms,c_mga.gms,1,ignore,ignore,,,,,,,,,,, -c_supplymodel.gms,c_supplymodel.gms,1,ignore,ignore,,,,,,,,,,, -c_supplyobjective.gms,c_supplyobjective.gms,1,ignore,ignore,,,,,,,,,,, -cbc.opt,cbc.opt,1,ignore,ignore,,,,,,,,,,, -cplex.op2,cplex.op2,1,ignore,ignore,,,,,,,,,,, -cplex.opt,cplex.opt,1,ignore,ignore,,,,,,,,,,, -createmodel.gms,createmodel.gms,1,ignore,ignore,,,,,,,,,,, -d_solveallyears.gms,d_solveallyears.gms,1,ignore,ignore,,,,,,,,,,, -d_solveoneyear.gms,d_solveoneyear.gms,1,ignore,ignore,,,,,,,,,,, -d_solve_iterate.py,d_solve_iterate.py,1,ignore,ignore,,,,,,,,,,, -d_solvepcm.gms,d_solvepcm.gms,1,ignore,ignore,,,,,,,,,,, -d_solveprep.gms,d_solveprep.gms,1,ignore,ignore,,,,,,,,,,, -d_solvewindow.gms,d_solvewindow.gms,1,ignore,ignore,,,,,,,,,,, -d1_temporal_params.gms,d1_temporal_params.gms,1,ignore,ignore,,,,,,,,,,, -d1_financials.gms,d1_financials.gms,1,ignore,ignore,,,,,,,,,,, -d2_post_solve_adjustments.gms,d2_post_solve_adjustments.gms,1,ignore,ignore,,,,,,,,,,, -d2_unfix_op.gms,d2_unfix_op.gms,1,ignore,ignore,,,,,,,,,,, -d2_varfix.gms,d2_varfix.gms,1,ignore,ignore,,,,,,,,,,, -d3_data_dump.gms,d3_data_dump.gms,1,ignore,ignore,,,,,,,,,,, -dump_alldata.gms,dump_alldata.gms,1,ignore,ignore,,,,,,,,,,, -e_powfrac_calc.gms,e_powfrac_calc.gms,1,ignore,ignore,,,,,,,,,,, -e_report.gms,e_report.gms,1,ignore,ignore,,,,,,,,,,, -e_report_dump.py,e_report_dump.py,1,ignore,ignore,,,,,,,,,,, -e_report_params.csv,e_report_params.csv,1,ignore,ignore,,,,,,,,,,, -gamslice.txt,gamslice.txt,0,ignore,ignore,,,,,,,,,,, -gurobi.opt,gurobi.opt,1,ignore,ignore,,,,,,,,,,, -interim_report.py,interim_report.py,1,ignore,ignore,,,,,,,,,,, -max_hintage_number.txt,,1,ignore,ignore,,,,,0,,,,,, -raw_value_streams.py,raw_value_streams.py,1,ignore,ignore,,,,,,,,,,, -runbatch.py,runbatch.py,1,ignore,ignore,,,,,,,,,,, -tc_phaseout.py,tc_phaseout.py,1,ignore,ignore,,,,,,,,,,, -valuestreams.py,valuestreams.py,1,ignore,ignore,,,,,,,,,,, diff --git a/runstatus.py b/runstatus.py deleted file mode 100644 index 8e44d134..00000000 --- a/runstatus.py +++ /dev/null @@ -1,187 +0,0 @@ -#%% Imports -import os -import re -import datetime -import subprocess -import argparse -from glob import glob - -# #%% Inputs for debugging -# batch_name = 'v20250812_mcK0' -# include_finished = False -# verbose = 0 - -#%%### Functions -def parse_multiple_runs_per_node(runs_running): - expanded_runs = [] - for i in runs_running: - ## Matches the form used for multiple runs per node: foo_(bar,baz[,etc]) - if re.match('^\w+_\(\w+,\w+(,\w+)*\)$', i): - batch_ = i.split('(')[0] - constituents = i.split('(')[1].strip(')').split(',') - expanded_runs.extend([batch_+c for c in constituents]) - ## Otherwise it's a normal run - else: - expanded_runs.append(i) - return sorted(expanded_runs) - - -def print_log_if_verbose(fullcase, verbose=0): - if verbose: - gamslog = os.path.join(fullcase, 'gamslog.txt') - print('vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv') - subprocess.run(f'tail {gamslog} -n {verbose}', shell=True) - print('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n') - -def get_run_status(reeds_path, batch_name): - #%% Get active runs - sq = f'squeue -u {os.environ["USER"]} -o "%.200j"' - sqout = subprocess.run(sq, capture_output=True, shell=True) - runs_running_all = [os.path.splitext(i.decode())[0] for i in sqout.stdout.split()] - - #%% If no batch_name is provided, use the pre-underscore text from the last run - if not len(batch_name): - batch_name = sorted(runs_running_all)[-1].split('_')[0] - print(f'Runs with batch_name = {batch_name}:') - - #%% Get all runs - runs_all = sorted(glob(os.path.join(reeds_path,'runs',batch_name+'*'))) - ### Identify finished runs - runs_finished = [ - i for i in runs_all - if os.path.exists(os.path.join(i, 'outputs', 'reeds-report', 'report.xlsx')) - ] - ### Keep unfinished runs - runs_unfinished = [i for i in runs_all if i not in runs_finished] - - ### Get failed runs by identifying and excluding active runs - runs_running_unparsed = [i for i in runs_running_all if i.startswith(batch_name)] - runs_running = parse_multiple_runs_per_node(runs_running_unparsed) - - # If a run is finished but on a shared node with another run still going, - # drop it from the running list - runs_running = [i for i in runs_running if os.path.join(reeds_path,'runs',i) not in runs_finished] - runs_failed = [i for i in runs_unfinished if os.path.basename(i) not in runs_running] - - ### Store the runs - dictruns = { - 'finished': [os.path.join(reeds_path,'runs',os.path.basename(i)) for i in runs_finished], - 'running': [os.path.join(reeds_path,'runs',os.path.basename(i)) for i in runs_running], - 'failed': [os.path.join(reeds_path,'runs',os.path.basename(i)) for i in runs_failed], - } - ## Only keep running runs in the current repo - dictruns['running'] = [i for i in dictruns['running'] if os.path.isdir(i)] - - return dictruns - -#%%### Procedure -if __name__ == '__main__': - - #%% Argument inputs - parser = argparse.ArgumentParser(description='Print status of runs on the HPC') - parser.add_argument('batch_name', type=str, nargs='?', default='', - help='batch name (case prefix) to search for') - parser.add_argument('--include_finished', '-f', action='store_true', - help='Include finished runs in response') - parser.add_argument('--verbose', '-v', action='count', default=0, - help='How many tail lines to print from gamslog.txt') - - args = parser.parse_args() - batch_name = args.batch_name - include_finished = args.include_finished - verbose = args.verbose - - #%% Shared parameters - reeds_path = os.path.dirname(os.path.abspath(__file__)) - dictruns = get_run_status(reeds_path, batch_name) - - #%%### Loop through categories and runs and report their status - for key, runs in dictruns.items(): - text = f'{key}: {len(runs)}' - print(f"\n{text}\n{'-'*len(text)}") - ### Loop through runs - try: - longest = max([len(os.path.basename(i)) for i in runs]) - except ValueError: - longest = 0 - for fullcase in runs: - case = os.path.basename(fullcase) - if (key == 'finished'): - if include_finished: - import pandas as pd - duration = pd.read_csv( - os.path.join(fullcase,'meta.csv'), skiprows=3).processtime.sum() - print(f"{case:<{longest}}: {datetime.timedelta(seconds=int(duration))}") - else: - ### Get last .lst file - lstfiles = sorted(glob(os.path.join(fullcase,'lstfiles','*'))) - if any([os.path.basename(i).startswith('report') for i in lstfiles]): - last_lst = 'e_report.gms' - penultimatefile = None - else: - if len(lstfiles) > 1: - # Drop environment file - lstfiles = [ - line for line in lstfiles if ( - ('environment.csv' not in line) - and ('mcs_group_weights.csv' not in line) - ) - ] - try: - lastfile = lstfiles[-1] - except IndexError: - print(f"{case:<{longest}}: failed in input_processing") - print_log_if_verbose(fullcase, verbose) - continue - try: - # Get time since previous lst file was modified - penultimatefile = lstfiles[-2] - penultimateyear = os.path.splitext(penultimatefile)[0].split('_')[-1] - lasttime = os.path.getmtime(penultimatefile) - nowtime = datetime.datetime.now().timestamp() - duration = datetime.timedelta(seconds=int((nowtime - lasttime))) - except IndexError: - penultimatefile = None - last_lst = os.path.splitext(lastfile)[0].split('_')[-1] - - if (key == 'running'): - - # check if PRAS is stalled - logfile = os.path.join(fullcase,'gamslog.txt') - with open(logfile, "r") as file: - gamslog = file.readlines() - # only look at last 5 lines in case there was a restart - gamslog = ''.join(gamslog[-5:]) - if "signal (6): Aborted" in gamslog: - errortext = "(WARNING: PRAS may be stalled, check gamslog)" - else: - errortext = "" - if penultimatefile: - print( - f"{case:<{longest}}: running {last_lst} " - f"({duration} since {penultimateyear} finished) " - f"{errortext}" - ) - else: - print(f"{case:<{longest}}: running {last_lst} {errortext}") - elif (key == 'failed'): - # add some details on the runs that failed by reading the slurm file - slurmfile = sorted(glob(os.path.join(fullcase,'slurm*.out')))[-1] - with open(slurmfile, "r") as file: - slurm = file.read() - # check if timed out - if "CANCELLED AT" in slurm and "DUE TO TIME LIMIT" in slurm: - errortext = "(timed out)" - # check if dual objective limit was reached - elif "dual objective limit exceeded" in slurm: - errortext = "(hit dual obj. limit)" - # check if infeasible - elif "d_solveoneyear.gms failed with return code 3" in slurm: - errortext = "(infeasible)" - else: - errortext = "" - print(f"{case:<{longest}}: failed in {last_lst} {errortext}") - else: - print(f"Unrecognized key for {case}: {key}") - - print_log_if_verbose(fullcase, verbose) diff --git a/sources.csv b/sources.csv deleted file mode 100644 index 6ecdbcef..00000000 --- a/sources.csv +++ /dev/null @@ -1,805 +0,0 @@ -RelativeFilePath,RelativeFolderPath,FileName_new,FileExtension,Description_new,Indices,DollarYear,Citation,Filetype,Units -/cases.csv,/,cases,.csv,Contains the configuration settings for the ReEDS run(s).,,2004,https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv,Switches file, -/cases_examples.csv,/,cases_examples,.csv,,,,,, -/cases_small.csv,/,cases_small,.csv,Contains settings to run ReEDS at a smaller scale to test operability of the ReEDS model. Turns off several technologies and reduces the model size to significantly improve solve times.,,,,, -/cases_standardscenarios.csv,/,cases_standardscenarios,.csv,Contains the configuration settings for the Standard Scenarios ReEDS runs.,,,,StdScen Cases file, -/cases_test.csv,/,cases_test,.csv,Contains the configuration settings for doing test runs including the default Pacific census division test case.,,,,, -/e_report_params.csv,/,e_report_params,.csv,Contains a parameter list used in the model along with descriptions of what they are and units used.,,,,, -/hourlize/inputs/load/dummy_agg_op_datacenters.csv,/hourlize/inputs/load,dummy_agg_op_datacenters,.csv,,,,,, -/hourlize/inputs/load/legacy_ba_state_map.csv,/hourlize/inputs/load,legacy_ba_state_map,.csv,,,,,, -/hourlize/inputs/load/legacy_ba_timezone.csv,/hourlize/inputs/load,legacy_ba_timezone,.csv,,,,,, -/hourlize/inputs/resource/egs_resource_classes.csv,/hourlize/inputs/resource,egs_resource_classes,.csv,,,,,, -/hourlize/inputs/resource/geohydro_resource_classes.csv,/hourlize/inputs/resource,geohydro_resource_classes,.csv,,,,,, -/hourlize/inputs/resource/offshore_zone_names.csv,/hourlize/inputs/resource,offshore_zone_names,.csv,,,,,, -/hourlize/inputs/resource/rev_sc_columns.csv,/hourlize/inputs/resource,rev_sc_columns,.csv,,,,,, -/hourlize/inputs/resource/state_abbrev.csv,/hourlize/inputs/resource,state_abbrev,.csv,Contains state names and codesfor the US.,,,,, -/hourlize/inputs/resource/upv_resource_classes.csv,/hourlize/inputs/resource,upv_resource_classes,.csv,Contains information related to UPV class segregation based on mean irradiance levels.,,,,, -/hourlize/inputs/resource/wind-ofs_resource_classes.csv,/hourlize/inputs/resource,wind-ofs_resource_classes,.csv,Contains information related to Offshore wind class segregation and turbine type (fixed vs floating) based on water depth and site lcoe,n/a,,,supply curve input, -/hourlize/inputs/resource/wind-ons_resource_classes.csv,/hourlize/inputs/resource,wind-ons_resource_classes,.csv,Contains information related to Onshore wind class segregation based on mean wind speeds.,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/expected_results,df_sc_out_upv_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,hierarchy_original,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,maxage,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,rev_paths,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,site_bin_map,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case,switches,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap_exog,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap_new_bin_out,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs,systemcost,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves/upv_supply_curve_raw_unpacked.csv,/hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves,upv_supply_curve_raw_unpacked,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/expected_results,df_sc_out_upv_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,hierarchy_original,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,maxage,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,rev_paths,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,site_bin_map,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case,switches,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap_exog,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap_new_bin_out,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs,systemcost,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves/upv_supply_curve_raw_unpacked.csv,/hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves,upv_supply_curve_raw_unpacked,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/expected_results,df_sc_out_upv_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,hierarchy_original,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,maxage,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,rev_paths,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,site_bin_map,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case,switches,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap_exog,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap_new_bin_out,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs,systemcost,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves/upv_supply_curve_raw_unpacked.csv,/hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves,upv_supply_curve_raw_unpacked,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results,df_sc_out_wind-ons_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,hierarchy_original,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,maxage,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,rev_paths,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,site_bin_map,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata/rev_supply_curves.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata,rev_supply_curves,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case,switches,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap_exog,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap_new_bin_out,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs,systemcost,.csv,,,,,, -/hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves/wind-ons_supply_curve_raw.csv,/hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves,wind-ons_supply_curve_raw,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs,df_sc_out_upv_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs,df_sc_out_wind-ofs_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs,df_sc_out_wind-ons_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint,df_sc_out_upv_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint,df_sc_out_wind-ofs_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint,df_sc_out_wind-ons_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs,df_sc_out_upv_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs,df_sc_out_wind-ofs_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs,df_sc_out_wind-ons_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_upv,.csv,,,,,, -/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_upv_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced_simul_fill.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_upv_reduced_simul_fill,.csv,,,,,, -/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ofs,.csv,,,,,, -/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs_reduced.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ofs_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ons,.csv,,,,,, -/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons_reduced.csv,/hourlize/tests/data/r2r_integration/expected_results,df_sc_out_wind-ons_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,hierarchy_original,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,maxage,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,rev_paths,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,site_bin_map,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_integration/reeds/inputs_case,switches,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap_exog,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap_new_bin_out,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, -/hourlize/tests/data/r2r_integration/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_integration/reeds/outputs,systemcost,.csv,,,,,, -/hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results/upv_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results,upv_supply_curve_raw,.csv,,,,,, -/hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results/wind-ofs_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results,wind-ofs_supply_curve_raw,.csv,,,,,, -/hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results/wind-ons_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results,wind-ons_supply_curve_raw,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_egs_allkm,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm_reduced.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_egs_allkm_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_geohydro_allkm,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm_reduced.csv,/hourlize/tests/data/r2r_integration_geothermal/expected_results,df_sc_out_geohydro_allkm_reduced,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/hierarchy_original.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,hierarchy_original,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/maxage.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,maxage,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/rev_paths.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,rev_paths,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/site_bin_map.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,site_bin_map,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/switches.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case,switches,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_exog.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap_exog,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_bin_out.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap_new_bin_out,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_ivrt_refurb.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,cap_new_ivrt_refurb,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/systemcost.csv,/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs,systemcost,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/supply_curves/egs_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration_geothermal/supply_curves,egs_supply_curve_raw,.csv,,,,,, -/hourlize/tests/data/r2r_integration_geothermal/supply_curves/geohydro_supply_curve_raw.csv,/hourlize/tests/data/r2r_integration_geothermal/supply_curves,geohydro_supply_curve_raw,.csv,,,,,, -/inputs/canada_imports/can_exports.csv,/inputs/canada_imports,can_exports,.csv,Annual exports to Canada by BA,"r,t",,,Input,MWh -/inputs/canada_imports/can_exports_szn_frac.csv,/inputs/canada_imports,can_exports_szn_frac,.csv,Fraction of annual exports to Canada by season,N/A,,,Input,rate (unitless) -/inputs/canada_imports/can_imports.csv,/inputs/canada_imports,can_imports,.csv,Annual imports from Canada by BA,"r,t",,,Input,MWh -/inputs/canada_imports/can_imports_quarter_frac.csv,/inputs/canada_imports,can_imports_quarter_frac,.csv,Fraction of annual imports from Canada by season,N/A,,,Input,rate (unitless) -/inputs/capacity_exogenous/cappayments.csv,/inputs/capacity_exogenous,cappayments,.csv,,,,,, -/inputs/capacity_exogenous/cappayments_ba.csv,/inputs/capacity_exogenous,cappayments_ba,.csv,,,,,, -/inputs/capacity_exogenous/demonstration_plants.csv,/inputs/capacity_exogenous,demonstration_plants,.csv,Nuclear-smr demonstration plants; active when GSw_NuclearDemo=1,"t,r,i,coolingwatertech,ctt,wst,value",,See 'notes' column in the file and https://www.energy.gov/oced/advanced-reactor-demonstration-projects-0,Prescribed capacity,MW -/inputs/capacity_exogenous/exog_cap_geohydro_allkm_reference.csv,/inputs/capacity_exogenous,exog_cap_geohydro_allkm_reference,.csv,,,,,, -/inputs/capacity_exogenous/exog_cap_geohydro_reference.csv,/inputs/capacity_exogenous,exog_cap_geohydro_reference,.csv,,,,,, -/inputs/capacity_exogenous/exog_cap_upv_limited.csv,/inputs/capacity_exogenous,exog_cap_upv_limited,.csv,,,,,, -/inputs/capacity_exogenous/exog_cap_upv_open.csv,/inputs/capacity_exogenous,exog_cap_upv_open,.csv,,,,,, -/inputs/capacity_exogenous/exog_cap_upv_reference.csv,/inputs/capacity_exogenous,exog_cap_upv_reference,.csv,,,,,, -/inputs/capacity_exogenous/exog_cap_wind-ons_limited.csv,/inputs/capacity_exogenous,exog_cap_wind-ons_limited,.csv,,,,,, -/inputs/capacity_exogenous/exog_cap_wind-ons_open.csv,/inputs/capacity_exogenous,exog_cap_wind-ons_open,.csv,,,,,, -/inputs/capacity_exogenous/exog_cap_wind-ons_reference.csv,/inputs/capacity_exogenous,exog_cap_wind-ons_reference,.csv,,,,,, -/inputs/capacity_exogenous/interconnection_queues.csv,/inputs/capacity_exogenous,interconnection_queues,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_limited.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_meshed_limited,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_open.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_meshed_open,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_reference.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_meshed_reference,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_limited.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_radial_limited,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_open.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_radial_open,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_reference.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ofs_radial_reference,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ons_limited.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ons_limited,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ons_open.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ons_open,.csv,,,,,, -/inputs/capacity_exogenous/prescribed_builds_wind-ons_reference.csv,/inputs/capacity_exogenous,prescribed_builds_wind-ons_reference,.csv,,,,,, -/inputs/capacity_exogenous/ReEDS_generator_database_final_EIA-NEMS.csv,/inputs/capacity_exogenous,ReEDS_generator_database_final_EIA-NEMS,.csv,EIA-NEMS database of existing generators,,,,Input, -/inputs/climate/climate_heuristics_finalyear.csv,/inputs/climate,climate_heuristics_finalyear,.csv,,,,,, -/inputs/climate/climate_heuristics_yearfrac.csv,/inputs/climate,climate_heuristics_yearfrac,.csv,,,,,, -/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjann.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario,"r,t",,,,multipliers (unitless) -/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjsea.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario,"r,month,t",,,,multipliers (unitless) -/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMult.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMultAnn.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario,"wst,r,t",,,,multipliers (unitless) -/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterSeaAnnDistr.csv,/inputs/climate/GFDL-ESM2M_RCP4p5_WM,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the GFDL-ESM2M_RCP4p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_RCP2p6,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_RCP2p6,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario,"wst,r,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_RCP2p6,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP2p6 climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjann.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario,"r,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjsea.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario,"r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario,"wst,r,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_rcp45_AT,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp45_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_RCP4p5,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_RCP4p5,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario,"wst,r,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_RCP4p5,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP4p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjann.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario,"r,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjsea.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario,"r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario,"wst,r,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_rcp85_AT,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp85_AT climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMult.csv,/inputs/climate/HadGEM2-ES_RCP8p5,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMultAnn.csv,/inputs/climate/HadGEM2-ES_RCP8p5,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario,"wst,r,t",,,,multipliers (unitless) -/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterSeaAnnDistr.csv,/inputs/climate/HadGEM2-ES_RCP8p5,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP8p5 climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjann.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,hydadjann,.csv,Climate-impact capacity factor multipliers for annual dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"r,t",,,,multipliers (unitless) -/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjsea.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,hydadjsea,.csv,Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"r,month,t",,,,multipliers (unitless) -/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMult.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,UnappWaterMult,.csv,Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMultAnn.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,UnappWaterMultAnn,.csv,Climate-impact water availability multipliers for annual unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"wst,r,t",,,,multipliers (unitless) -/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterSeaAnnDistr.csv,/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM,UnappWaterSeaAnnDistr,.csv,Fractional distribution of unappropriated fresh surface water for each month of a given year for the IPSL-CM5A-LR_RCP8p5_WM climate scenario,"wst,r,month,t",,,,multipliers (unitless) -/inputs/consume/consume_char_low.csv,/inputs/consume,consume_char_low,.csv,"Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Conservative assumptions.","i,t",Units vary based on the parameter - see commented text in b_inputs.gms.,N/A,Inputs, -/inputs/consume/consume_char_ref.csv,/inputs/consume,consume_char_ref,.csv,"Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Reference assumptions.","i,t",Units vary based on the parameter - see commented text in b_inputs.gms.,N/A,Inputs, -/inputs/consume/dac_elec_BVRE_2021_high.csv,/inputs/consume,dac_elec_BVRE_2021_high,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions.","i,t",As specified in inputs/consume/dollaryear,https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a,Inputs, -/inputs/consume/dac_elec_BVRE_2021_low.csv,/inputs/consume,dac_elec_BVRE_2021_low,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions.","i,t",As specified in inputs/consume/dollaryear,https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a,Inputs, -/inputs/consume/dac_elec_BVRE_2021_mid.csv,/inputs/consume,dac_elec_BVRE_2021_mid,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions.","i,t",As specified in inputs/consume/dollaryear,https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a,Inputs, -/inputs/consume/dac_gas_BVRE_2021_high.csv,/inputs/consume,dac_gas_BVRE_2021_high,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions.","i,t",As specified in inputs/consume/dollaryear,https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987,Inputs, -/inputs/consume/dac_gas_BVRE_2021_low.csv,/inputs/consume,dac_gas_BVRE_2021_low,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions.","i,t",As specified in inputs/consume/dollaryear,https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987,Inputs, -/inputs/consume/dac_gas_BVRE_2021_mid.csv,/inputs/consume,dac_gas_BVRE_2021_mid,.csv,"DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions.","i,t",As specified in inputs/consume/dollaryear,https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987,Inputs, -/inputs/consume/dollaryear.csv,/inputs/consume,dollaryear,.csv,Dollar year for various Beyond VRE scenarios. ,N/A,Stated in document.,N/A,Inputs, -/inputs/consume/h2_demand_county_share.csv,/inputs/consume,h2_demand_county_share,.csv,"The fraction of national hydrogen demand in that year that corresponds to each county. Demand estimates come from https://data.openei.org/submissions/5655. 2021 demand shares correspond to the ""Reference"" scenario with light-duty vehicles / biofuels / methanol demand removed and 2050 shares correspond to the ""Low Cost Electrolysis"" scenario.","r,t",N/A,N/A,Inputs, -/inputs/consume/h2_exogenous_demand.csv,/inputs/consume,h2_exogenous_demand,.csv,Exogenous hydrogen demand by industries other than the power sector per year,t,N/A,N/A,Inputs, -/inputs/consume/h2_transport_and_storage_costs.csv,/inputs/consume,h2_transport_and_storage_costs,.csv,Transport and storage costs of hydrogen per year,t,2004,N/A,Inputs, -/inputs/county2zone.csv,/inputs,county2zone,.csv,,,,,, -/inputs/ctus/co2_site_char.csv,/inputs/ctus,co2_site_char,.csv,,,2018,,, -/inputs/ctus/cs.csv,/inputs/ctus,cs,.csv,,,,,, -/inputs/degradation/degradation_annual_default.csv,/inputs/degradation,degradation_annual_default,.csv,,,,,, -/inputs/demand_response/dr_shed_avail_scalar.csv,/inputs/demand_response,dr_shed_avail_scalar,.csv,,,,,, -/inputs/demand_response/dr_shed_capacity_scalar_demo_data_January_2025.csv,/inputs/demand_response,dr_shed_capacity_scalar_demo_data_January_2025,.csv,,,,,, -/inputs/demand_response/dr_shed_hourly.h5,/inputs/demand_response,dr_shed_hourly,.h5,,,,,, -/inputs/demand_response/ev_load_Baseline.h5,/inputs/demand_response,ev_load_Baseline,.h5,Baseline electricity load from EV charging by timeslice h and year t,,,,inputs,MW -/inputs/demand_response/evmc_rsc_Baseline.csv,/inputs/demand_response,evmc_rsc_Baseline,.csv,,,,,, -/inputs/demand_response/evmc_shape_decrease_profile_Baseline.h5,/inputs/demand_response,evmc_shape_decrease_profile_Baseline,.h5,,,,,, -/inputs/demand_response/evmc_shape_increase_profile_Baseline.h5,/inputs/demand_response,evmc_shape_increase_profile_Baseline,.h5,,,,,, -/inputs/demand_response/evmc_storage_decrease_profile_Baseline.h5,/inputs/demand_response,evmc_storage_decrease_profile_Baseline,.h5,,,,,, -/inputs/demand_response/evmc_storage_increase_profile_Baseline.h5,/inputs/demand_response,evmc_storage_increase_profile_Baseline,.h5,,,,,, -/inputs/demand_response/evmc_storage_profile_energy_Baseline.h5,/inputs/demand_response,evmc_storage_profile_energy_Baseline,.h5,,,,,, -/inputs/dgen_model_inputs/stscen2023_electrification/distpvcap_stscen2023_electrification.csv,/inputs/dgen_model_inputs/stscen2023_electrification,distpvcap_stscen2023_electrification,.csv,,,,,, -/inputs/dgen_model_inputs/stscen2023_highng/distpvcap_stscen2023_highng.csv,/inputs/dgen_model_inputs/stscen2023_highng,distpvcap_stscen2023_highng,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with high NG (including distpv) costs,,,,distribution PV inputs , -/inputs/dgen_model_inputs/stscen2023_highre/distpvcap_stscen2023_highre.csv,/inputs/dgen_model_inputs/stscen2023_highre,distpvcap_stscen2023_highre,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with high RE (including distpv) costs,,,,distribution PV inputs , -/inputs/dgen_model_inputs/stscen2023_lowng/distpvcap_stscen2023_lowng.csv,/inputs/dgen_model_inputs/stscen2023_lowng,distpvcap_stscen2023_lowng,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with low NG (including distpv) costs,,,,distribution PV inputs , -/inputs/dgen_model_inputs/stscen2023_lowre/distpvcap_stscen2023_lowre.csv,/inputs/dgen_model_inputs/stscen2023_lowre,distpvcap_stscen2023_lowre,.csv,Setting for distpv scenario capacity - from standard scenarios 2023 with low RE (including distpv) costs,,,,distribution PV inputs , -/inputs/dgen_model_inputs/stscen2023_mid_case/distpvcap_stscen2023_mid_case.csv,/inputs/dgen_model_inputs/stscen2023_mid_case,distpvcap_stscen2023_mid_case,.csv,,,,,distribution PV inputs , -/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035/distpvcap_stscen2023_mid_case_95_by_2035.csv,/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035,distpvcap_stscen2023_mid_case_95_by_2035,.csv,,,,,distribution PV inputs , -/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050/distpvcap_stscen2023_mid_case_95_by_2050.csv,/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050,distpvcap_stscen2023_mid_case_95_by_2050,.csv,,,,,distribution PV inputs , -/inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050/distpvcap_stscen2023_taxcredit_extended2050.csv,/inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050,distpvcap_stscen2023_taxcredit_extended2050,.csv,,,,,distribution PV inputs , -/inputs/disaggregation/county_population.csv,/inputs/disaggregation,county_population,.csv,"The population of each county, relative values are used as multipliers for downselecting data. Data come from the U.S. Census Bureau 2021 county population estimates (https://www.census.gov/data/tables/time-series/demo/popest/2020s-counties-total.html).",FIPS,,,, -/inputs/disaggregation/county_state_lpf.csv,/inputs/disaggregation,county_state_lpf,.csv,,,,,, -/inputs/disaggregation/disagg_hydroexist.csv,/inputs/disaggregation,disagg_hydroexist,.csv,"The hydropower capacity fraction of each county within a given ReEDS BA, used as multipliers for downselecting data",r,,,, -/inputs/emission_constraints/ccs_link.csv,/inputs/emission_constraints,ccs_link,.csv,,,,,, -/inputs/emission_constraints/ccs_link_water.csv,/inputs/emission_constraints,ccs_link_water,.csv,,,,,, -/inputs/emission_constraints/co2_cap.csv,/inputs/emission_constraints,co2_cap,.csv,Annual nationwide carbon cap,,,,, -/inputs/emission_constraints/co2_tax.csv,/inputs/emission_constraints,co2_tax,.csv,Annual co2 tax,,,,, -/inputs/emission_constraints/county_co2_share_egrid_2022.csv,/inputs/emission_constraints,county_co2_share_egrid_2022,.csv,,,,,, -/inputs/emission_constraints/csapr_group1_ex.csv,/inputs/emission_constraints,csapr_group1_ex,.csv,,,,,, -/inputs/emission_constraints/csapr_group2_ex.csv,/inputs/emission_constraints,csapr_group2_ex,.csv,,,,,, -/inputs/emission_constraints/csapr_ozone_season.csv,/inputs/emission_constraints,csapr_ozone_season,.csv,,,,,, -/inputs/emission_constraints/emitrate.csv,/inputs/emission_constraints,emitrate,.csv,Emission rates for thermal generators with values from Table 5 of https://docs.nrel.gov/docs/fy25osti/93005.pdf,"i,e",,,, -/inputs/emission_constraints/gwp.csv,/inputs/emission_constraints,gwp,.csv,,,,,, -/inputs/emission_constraints/h2_leakage_rate.csv,/inputs/emission_constraints,h2_leakage_rate,.csv,,,,,, -/inputs/emission_constraints/methane_leakage_rate.csv,/inputs/emission_constraints,methane_leakage_rate,.csv,,,,,, -/inputs/emission_constraints/ng_crf_penalty.csv,/inputs/emission_constraints,ng_crf_penalty,.csv,Cost adjustment for NG techs in scenarios with national decarbonization targets,allt,N/A,https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220,Inputs,rate (unitless) -/inputs/emission_constraints/rggi_states.csv,/inputs/emission_constraints,rggi_states,.csv,Participating RGGI states,,,https://www.rggi.org/program-overview-and-design/elements,, -/inputs/emission_constraints/rggicon.csv,/inputs/emission_constraints,rggicon,.csv,CO2 caps for RGGI states in metric tons,,,https://www.rggi.org/allowance-tracking/allowance-distribution,, -/inputs/emission_constraints/state_cap.csv,/inputs/emission_constraints,state_cap,.csv,,,,,, -/inputs/financials/cap_penalty.csv,/inputs/financials,cap_penalty,.csv,,,,,, -/inputs/financials/construction_schedules_default.csv,/inputs/financials,construction_schedules_default,.csv,,,,,, -/inputs/financials/construction_times_default.csv,/inputs/financials,construction_times_default,.csv,,,,,, -/inputs/financials/currency_incentives.csv,/inputs/financials,currency_incentives,.csv,,,,,, -/inputs/financials/deflator.csv,/inputs/financials,deflator,.csv,Dollar year deflator to convert values to 2004$,,,,, -/inputs/financials/depreciation_schedules_default.csv,/inputs/financials,depreciation_schedules_default,.csv,,,,,, -/inputs/financials/energy_communities.csv,/inputs/financials,energy_communities,.csv,,,,,, -/inputs/financials/financials_hydrogen.csv,/inputs/financials,financials_hydrogen,.csv,,,,,, -/inputs/financials/financials_sys_ATB2023.csv,/inputs/financials,financials_sys_ATB2023,.csv,,,,,, -/inputs/financials/financials_sys_ATB2024.csv,/inputs/financials,financials_sys_ATB2024,.csv,,,,,, -/inputs/financials/financials_tech_ATB2023.csv,/inputs/financials,financials_tech_ATB2023,.csv,,,,,, -/inputs/financials/financials_tech_ATB2023_CRP20.csv,/inputs/financials,financials_tech_ATB2023_CRP20,.csv,,,,,, -/inputs/financials/financials_tech_ATB2024.csv,/inputs/financials,financials_tech_ATB2024,.csv,,,,,, -/inputs/financials/financials_transmission_30ITC_0pen_2022_2031.csv,/inputs/financials,financials_transmission_30ITC_0pen_2022_2031,.csv,,,,,, -/inputs/financials/financials_transmission_default.csv,/inputs/financials,financials_transmission_default,.csv,,,,,, -/inputs/financials/incentives_annual.csv,/inputs/financials,incentives_annual,.csv,,,,,, -/inputs/financials/incentives_biennial.csv,/inputs/financials,incentives_biennial,.csv,,,,,, -/inputs/financials/incentives_ira.csv,/inputs/financials,incentives_ira,.csv,,,,,, -/inputs/financials/incentives_ira_45q_45v_extension.csv,/inputs/financials,incentives_ira_45q_45v_extension,.csv,,,,,, -/inputs/financials/incentives_noira.csv,/inputs/financials,incentives_noira,.csv,,,,,, -/inputs/financials/incentives_none.csv,/inputs/financials,incentives_none,.csv,,,,,, -/inputs/financials/incentives_obbba.csv,/inputs/financials,incentives_obbba,.csv,,,,,, -/inputs/financials/incentives_obbba_conservative.csv,/inputs/financials,incentives_obbba_conservative,.csv,,,,,, -/inputs/financials/inflation_default.csv,/inputs/financials,inflation_default,.csv,Annual inflation factors from 1914 through 2200; historical values use the avg-avg values from https://www.usinflationcalculator.com/inflation/consumer-price-index-and-annual-percent-changes-from-1913-to-2008/,t,,,, -/inputs/financials/nuclear_energy_communities.csv,/inputs/financials,nuclear_energy_communities,.csv,"""Counties belonging to metropolitan statistical areas (MSAs) for which at least 0.17 percent of direct employment has been related to nuclear power at any point since 2010. These are determined partly by following the process described in Section 2.6 of https://home.treasury.gov/system/files/8861/EnergyCommunities_Data_Documentation.pdf and substituting in the NAICS code for nuclear electric power generation (221113) and partly by determining counties that belong to MSAs where the number of people employed by national labs engaged in nuclear research and development (PNNL, INL, ORNL, SNL, LLNL, Argonne, and LANL) has been at least 0.17 percent of the MSA's total employment at any point since 2010.""",,,,, -/inputs/financials/reg_cap_cost_diff_default.csv,/inputs/financials,reg_cap_cost_diff_default,.csv,region-specific differences for capital cost of all resources. Add to 1 to produce a multiplier,"i,r",,,parameter, -/inputs/financials/retire_penalty.csv,/inputs/financials,retire_penalty,.csv,,,,,, -/inputs/financials/supply_chain_adjust.csv,/inputs/financials,supply_chain_adjust,.csv,,,,,, -/inputs/financials/tc_phaseout_schedule_ira2022.csv,/inputs/financials,tc_phaseout_schedule_ira2022,.csv,,,,,, -/inputs/fuelprices/alpha_AEO_2023_HOG.csv,/inputs/fuelprices,alpha_AEO_2023_HOG,.csv,"High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2023,Input, -/inputs/fuelprices/alpha_AEO_2023_LOG.csv,/inputs/fuelprices,alpha_AEO_2023_LOG,.csv,"Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2023,Input, -/inputs/fuelprices/alpha_AEO_2023_reference.csv,/inputs/fuelprices,alpha_AEO_2023_reference,.csv,"reference census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2023,Input, -/inputs/fuelprices/alpha_AEO_2025_HOG.csv,/inputs/fuelprices,alpha_AEO_2025_HOG,.csv,"High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2025,Input, -/inputs/fuelprices/alpha_AEO_2025_LOG.csv,/inputs/fuelprices,alpha_AEO_2025_LOG,.csv,"Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2025,Input, -/inputs/fuelprices/alpha_AEO_2025_reference.csv,/inputs/fuelprices,alpha_AEO_2025_reference,.csv,"reference census division alpha values, used in the calculation of natural gas demand curves","allt,cendiv",2004,AEO 2025,Input, -/inputs/fuelprices/cd_beta0.csv,/inputs/fuelprices,cd_beta0,.csv,reference census division beta levels electric sector,cendiv,2004,,Input, -/inputs/fuelprices/cd_beta0_allsector.csv,/inputs/fuelprices,cd_beta0_allsector,.csv,reference census division beta levels all sectors,cendiv,2004,,Input, -/inputs/fuelprices/cendivweights.csv,/inputs/fuelprices,cendivweights,.csv,weights to smooth gas prices between census regions to avoid abrupt price changes at the cendiv borders,"r,cendiv",,,, -/inputs/fuelprices/coal_AEO_2023_reference.csv,/inputs/fuelprices,coal_AEO_2023_reference,.csv,reference case census division fuel price of coal,"t,cendiv",2022,,, -/inputs/fuelprices/coal_AEO_2025_reference.csv,/inputs/fuelprices,coal_AEO_2025_reference,.csv,reference case census division fuel price of coal with missing values forward-filled from earlier years,"t,cendiv",2024,,, -/inputs/fuelprices/dollaryear.csv,/inputs/fuelprices,dollaryear,.csv,Dollar year mapping for each fuel price scenario,,,,, -/inputs/fuelprices/h2-combustion_10.csv,/inputs/fuelprices,h2-combustion_10,.csv,price of hydrogen for combustion technologies (h2-ct and cc) at $10/MMBtu for all years,,,,, -/inputs/fuelprices/h2-combustion_30.csv,/inputs/fuelprices,h2-combustion_30,.csv,price of hydrogen for combustion technologies (h2-ct and cc) at $30/MMBtu for all years,,,,, -/inputs/fuelprices/h2-combustion_reference.csv,/inputs/fuelprices,h2-combustion_reference,.csv,price of hydrogen for combustion technologies (h2-ct and cc) at $20/MMBtu for all years,,,,, -/inputs/fuelprices/ng_AEO_2023_HOG.csv,/inputs/fuelprices,ng_AEO_2023_HOG,.csv,High Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu -/inputs/fuelprices/ng_AEO_2023_LOG.csv,/inputs/fuelprices,ng_AEO_2023_LOG,.csv,Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu -/inputs/fuelprices/ng_AEO_2023_reference.csv,/inputs/fuelprices,ng_AEO_2023_reference,.csv,Reference scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu -/inputs/fuelprices/ng_AEO_2025_HOG.csv,/inputs/fuelprices,ng_AEO_2025_HOG,.csv,High Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu -/inputs/fuelprices/ng_AEO_2025_LOG.csv,/inputs/fuelprices,ng_AEO_2025_LOG,.csv,Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu -/inputs/fuelprices/ng_AEO_2025_reference.csv,/inputs/fuelprices,ng_AEO_2025_reference,.csv,Reference scenario census division fuel price of natural gas,"cendiv,t",2004,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,2004$/MMBtu -/inputs/fuelprices/ng_demand_AEO_2023_HOG.csv,/inputs/fuelprices,ng_demand_AEO_2023_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_demand_AEO_2023_LOG.csv,/inputs/fuelprices,ng_demand_AEO_2023_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_demand_AEO_2023_reference.csv,/inputs/fuelprices,ng_demand_AEO_2023_reference,.csv,"Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_demand_AEO_2025_HOG.csv,/inputs/fuelprices,ng_demand_AEO_2025_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_demand_AEO_2025_LOG.csv,/inputs/fuelprices,ng_demand_AEO_2025_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_demand_AEO_2025_reference.csv,/inputs/fuelprices,ng_demand_AEO_2025_reference,.csv,"Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_tot_demand_AEO_2023_HOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2023_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_tot_demand_AEO_2023_LOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2023_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_tot_demand_AEO_2023_reference.csv,/inputs/fuelprices,ng_tot_demand_AEO_2023_reference,.csv,"Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2023: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_tot_demand_AEO_2025_HOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2025_HOG,.csv,"High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_tot_demand_AEO_2025_LOG.csv,/inputs/fuelprices,ng_tot_demand_AEO_2025_LOG,.csv,"Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/ng_tot_demand_AEO_2025_reference.csv,/inputs/fuelprices,ng_tot_demand_AEO_2025_reference,.csv,"Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves","cendiv,t",,AEO2025: https://www.eia.gov/outlooks/aeo/,Input,Quads -/inputs/fuelprices/uranium_AEO_2023_reference.csv,/inputs/fuelprices,uranium_AEO_2023_reference,.csv,,,,,, -/inputs/fuelprices/uranium_AEO_2025_reference.csv,/inputs/fuelprices,uranium_AEO_2025_reference,.csv,,,,,, -/inputs/geothermal/geo_discovery_BAU.csv,/inputs/geothermal,geo_discovery_BAU,.csv,,,,,, -/inputs/geothermal/geo_discovery_factor_ATB_2023.csv,/inputs/geothermal,geo_discovery_factor_ATB_2023,.csv,,,,,, -/inputs/geothermal/geo_discovery_factor_reV.csv,/inputs/geothermal,geo_discovery_factor_reV,.csv,,,,,, -/inputs/geothermal/geo_discovery_TI.csv,/inputs/geothermal,geo_discovery_TI,.csv,,,,,, -/inputs/geothermal/geo_rsc_ATB_2023.csv,/inputs/geothermal,geo_rsc_ATB_2023,.csv,,,,,, -/inputs/growth_constraints/gbin_min.csv,/inputs/growth_constraints,gbin_min,.csv,,,,,, -/inputs/growth_constraints/growth_bin_size_mult.csv,/inputs/growth_constraints,growth_bin_size_mult,.csv,,,,,, -/inputs/growth_constraints/growth_limit_absolute.csv,/inputs/growth_constraints,growth_limit_absolute,.csv,"Maximum expected annual builds for wind, batteries, and UPV from 2024-2026 using observed record builds.",,,,,MW/year -/inputs/growth_constraints/growth_penalty.csv,/inputs/growth_constraints,growth_penalty,.csv,,,,,, -/inputs/hierarchy.csv,/inputs,hierarchy,.csv,,,,,, -/inputs/hierarchy_agg125.csv,/inputs,hierarchy_agg125,.csv,,,,,, -/inputs/hierarchy_agg54.csv,/inputs,hierarchy_agg54,.csv,,,,,, -/inputs/hierarchy_agg69.csv,/inputs,hierarchy_agg69,.csv,,,,,, -/inputs/zones/hierarchy_offshore.csv,/inputs/zones,hierarchy_offshore,.csv,,,,,, -/inputs/hydro/cap_existing_hydro.h5,/inputs/hydro,cap_existing_hydro,.h5,"Annual capacities for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset.",t,,,Input,MW -/inputs/hydro/hyd_fom.csv,/inputs/hydro,hyd_fom,.csv,Regional FOM costs for hydro,,,,, -/inputs/hydro/hydcf_fixed.h5,/inputs/hydro,hydcf_fixed,.h5,Fixed monthly zonal hydro capacity factor data partially created by ORNL and partially derived from ORNL's Existing Hydropower Assets dataset.,"i,month",,,Input,unitless -/inputs/hydro/hydro_mingen.csv,/inputs/hydro,hydro_mingen,.csv,,,,,, -/inputs/hydro/net_gen_existing_hydro.h5,/inputs/hydro,net_gen_existing_hydro,.h5,"Monthly net generation values for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset.","t,month",,,Input,MWh -/inputs/hydro/SeaCapAdj_hy.csv,/inputs/hydro,SeaCapAdj_hy,.csv,,,,,, -/inputs/load/cangrowth.csv,/inputs/load,cangrowth,.csv,Canada load growth multiplier,,,,, -/inputs/load/demand_AEO_2023_high.csv,/inputs/load,demand_AEO_2023_high,.csv,Load growth projection from the AEO2023 High Economic Growth scenario,,,,,unitless -/inputs/load/demand_AEO_2023_low.csv,/inputs/load,demand_AEO_2023_low,.csv,Load growth projection from the AEO2023 Low Economic Growth scenario,,,,,unitless -/inputs/load/demand_AEO_2023_reference.csv,/inputs/load,demand_AEO_2023_reference,.csv,Load growth projection from the AEO2023 Reference scenario,,,,,unitless -/inputs/load/demand_AEO_2025_high.csv,/inputs/load,demand_AEO_2025_high,.csv,Load growth projection from the AEO2025 High Economic Growth scenario,,,,,unitless -/inputs/load/demand_AEO_2025_low.csv,/inputs/load,demand_AEO_2025_low,.csv,Load growth projection from the AEO2025 Low Economic Growth scenario,,,,,unitless -/inputs/load/demand_AEO_2025_reference.csv,/inputs/load,demand_AEO_2025_reference,.csv,Load growth projection from the AEO2025 Reference scenario,,,,,unitless -/inputs/load/EIA_loadbystate.csv,/inputs/load,EIA_loadbystate,.csv,,,,,, -/inputs/load/loadsite_country_test.csv,/inputs/load,loadsite_country_test,.csv,,,,,, -/inputs/load/mex_growth_rate.csv,/inputs/load,mex_growth_rate,.csv,Mexico load growth multiplier,,,,, -/inputs/national_generation/gen_mandate_tech_list.csv,/inputs/national_generation,gen_mandate_tech_list,.csv,,,,,, -/inputs/national_generation/gen_mandate_trajectory.csv,/inputs/national_generation,gen_mandate_trajectory,.csv,,,,,, -/inputs/national_generation/national_rps_frac_allScen.csv,/inputs/national_generation,national_rps_frac_allScen,.csv,,,,,, -/inputs/outages/temperature_celsius-st.h5,/inputs/outages,temperature_celsius-st,.h5,,,,,, -/inputs/plant_characteristics/battery_ATB_2024_advanced.csv,/inputs/plant_characteristics,battery_ATB_2024_advanced,.csv,,,2021,,, -/inputs/plant_characteristics/battery_ATB_2024_conservative.csv,/inputs/plant_characteristics,battery_ATB_2024_conservative,.csv,,,2021,,, -/inputs/plant_characteristics/battery_ATB_2024_moderate.csv,/inputs/plant_characteristics,battery_ATB_2024_moderate,.csv,,,2021,,, -/inputs/plant_characteristics/beccs_BVRE_2021_high.csv,/inputs/plant_characteristics,beccs_BVRE_2021_high,.csv,,,,,, -/inputs/plant_characteristics/beccs_BVRE_2021_low.csv,/inputs/plant_characteristics,beccs_BVRE_2021_low,.csv,,,,,, -/inputs/plant_characteristics/beccs_BVRE_2021_mid.csv,/inputs/plant_characteristics,beccs_BVRE_2021_mid,.csv,,,,,, -/inputs/plant_characteristics/beccs_lowcost.csv,/inputs/plant_characteristics,beccs_lowcost,.csv,,,,,, -/inputs/plant_characteristics/beccs_reference.csv,/inputs/plant_characteristics,beccs_reference,.csv,,,,,, -/inputs/plant_characteristics/biopower_ATB_2024_moderate.csv,/inputs/plant_characteristics,biopower_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/ccsflex_ATB_2020_cost.csv,/inputs/plant_characteristics,ccsflex_ATB_2020_cost,.csv,,,,,, -/inputs/plant_characteristics/ccsflex_ATB_2020_perf.csv,/inputs/plant_characteristics,ccsflex_ATB_2020_perf,.csv,,,,,, -/inputs/plant_characteristics/coal-ccs_ATB_2024_advanced.csv,/inputs/plant_characteristics,coal-ccs_ATB_2024_advanced,.csv,,,,,, -/inputs/plant_characteristics/coal-ccs_ATB_2024_conservative.csv,/inputs/plant_characteristics,coal-ccs_ATB_2024_conservative,.csv,,,,,, -/inputs/plant_characteristics/coal-ccs_ATB_2024_moderate.csv,/inputs/plant_characteristics,coal-ccs_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/coal_ATB_2024_moderate.csv,/inputs/plant_characteristics,coal_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/cost_opres_default.csv,/inputs/plant_characteristics,cost_opres_default,.csv,,,,,, -/inputs/plant_characteristics/cost_opres_market.csv,/inputs/plant_characteristics,cost_opres_market,.csv,,,,,, -/inputs/plant_characteristics/csp_ATB_2023_advanced.csv,/inputs/plant_characteristics,csp_ATB_2023_advanced,.csv,,,,,, -/inputs/plant_characteristics/csp_ATB_2023_conservative.csv,/inputs/plant_characteristics,csp_ATB_2023_conservative,.csv,,,,,, -/inputs/plant_characteristics/csp_ATB_2023_moderate.csv,/inputs/plant_characteristics,csp_ATB_2023_moderate,.csv,,,,,, -/inputs/plant_characteristics/csp_ATB_2024_advanced.csv,/inputs/plant_characteristics,csp_ATB_2024_advanced,.csv,,,,,, -/inputs/plant_characteristics/csp_ATB_2024_conservative.csv,/inputs/plant_characteristics,csp_ATB_2024_conservative,.csv,,,,,, -/inputs/plant_characteristics/csp_ATB_2024_moderate.csv,/inputs/plant_characteristics,csp_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/csp_SunShot2030.csv,/inputs/plant_characteristics,csp_SunShot2030,.csv,Csp costs from the SunShot2030 cost scenario,,,,, -/inputs/plant_characteristics/dollaryear.csv,/inputs/plant_characteristics,dollaryear,.csv,Dollar year mapping for each plant cost scenario,,,,, -/inputs/plant_characteristics/dr_shed_capcost_demo_data_IEF_January_2025.csv,/inputs/plant_characteristics,dr_shed_capcost_demo_data_IEF_January_2025,.csv,,,,,, -/inputs/plant_characteristics/dr_shed_fom.csv,/inputs/plant_characteristics,dr_shed_fom,.csv,,,,,, -/inputs/plant_characteristics/dr_shed_vom.csv,/inputs/plant_characteristics,dr_shed_vom,.csv,,,,,, -/inputs/plant_characteristics/evmc_shape_Baseline.csv,/inputs/plant_characteristics,evmc_shape_Baseline,.csv,,,,,, -/inputs/plant_characteristics/evmc_storage_Baseline.csv,/inputs/plant_characteristics,evmc_storage_Baseline,.csv,,,,,, -/inputs/plant_characteristics/gas-ccs_ATB_2024_advanced.csv,/inputs/plant_characteristics,gas-ccs_ATB_2024_advanced,.csv,,,,,, -/inputs/plant_characteristics/gas-ccs_ATB_2024_conservative.csv,/inputs/plant_characteristics,gas-ccs_ATB_2024_conservative,.csv,,,,,, -/inputs/plant_characteristics/gas-ccs_ATB_2024_moderate.csv,/inputs/plant_characteristics,gas-ccs_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/gas_ATB_2024_moderate.csv,/inputs/plant_characteristics,gas_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/geo_ATB_2023_advanced.csv,/inputs/plant_characteristics,geo_ATB_2023_advanced,.csv,,,,,, -/inputs/plant_characteristics/geo_ATB_2023_conservative.csv,/inputs/plant_characteristics,geo_ATB_2023_conservative,.csv,,,,,, -/inputs/plant_characteristics/geo_ATB_2023_moderate.csv,/inputs/plant_characteristics,geo_ATB_2023_moderate,.csv,,,,,, -/inputs/plant_characteristics/geo_ATB_2024_advanced.csv,/inputs/plant_characteristics,geo_ATB_2024_advanced,.csv,,,,,, -/inputs/plant_characteristics/geo_ATB_2024_conservative.csv,/inputs/plant_characteristics,geo_ATB_2024_conservative,.csv,,,,,, -/inputs/plant_characteristics/geo_ATB_2024_moderate.csv,/inputs/plant_characteristics,geo_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/h2-combustion_ATB_2023.csv,/inputs/plant_characteristics,h2-combustion_ATB_2023,.csv,,,,,, -/inputs/plant_characteristics/h2-combustion_ATB_2024.csv,/inputs/plant_characteristics,h2-combustion_ATB_2024,.csv,Hydrogen CT and CC plant costs generated in preprocessing from moderate case NREL ATB 2024 data,,,,, -/inputs/plant_characteristics/heat_rate_adj.csv,/inputs/plant_characteristics,heat_rate_adj,.csv,Heat rate adjustment multiplier by technology,,,,, -/inputs/plant_characteristics/heat_rate_penalty_spin.csv,/inputs/plant_characteristics,heat_rate_penalty_spin,.csv,,,,,, -/inputs/plant_characteristics/hydro_ATB_2019_constant.csv,/inputs/plant_characteristics,hydro_ATB_2019_constant,.csv,Hydro costs from the 2019 ATB constant cost scenario,,,,, -/inputs/plant_characteristics/hydro_ATB_2019_low.csv,/inputs/plant_characteristics,hydro_ATB_2019_low,.csv,Hydro costs from the 2019 ATB low cost scenario,,,,, -/inputs/plant_characteristics/hydro_ATB_2019_mid.csv,/inputs/plant_characteristics,hydro_ATB_2019_mid,.csv,Hydro costs from the 2019 ATB mid cost scenario,,,,, -/inputs/plant_characteristics/maxage.csv,/inputs/plant_characteristics,maxage,.csv,Maximum age allowed for each technology,,,,, -/inputs/plant_characteristics/maxdailycf.csv,/inputs/plant_characteristics,maxdailycf,.csv,maximum daily capacity factor--dr_shed input supply curves are based on one 4-hour event per day,,,,, -/inputs/plant_characteristics/min_retire_age.csv,/inputs/plant_characteristics,min_retire_age,.csv,Minimum retirement age for given technology,,,,, -/inputs/plant_characteristics/minCF.csv,/inputs/plant_characteristics,minCF,.csv,minimum annual capacity factor for each tech fleet - applied to i-rto,,,,, -/inputs/plant_characteristics/mingen_fixed.csv,/inputs/plant_characteristics,mingen_fixed,.csv,,,,,, -/inputs/plant_characteristics/minloadfrac0.csv,/inputs/plant_characteristics,minloadfrac0,.csv,characteristics/minloadfrac0 database of minloadbed generator cs,,,,, -/inputs/plant_characteristics/mttr.csv,/inputs/plant_characteristics,mttr,.csv,,,,,, -/inputs/plant_characteristics/nuclear-smr_ATB_2024_advanced.csv,/inputs/plant_characteristics,nuclear-smr_ATB_2024_advanced,.csv,,,,,, -/inputs/plant_characteristics/nuclear-smr_ATB_2024_conservative.csv,/inputs/plant_characteristics,nuclear-smr_ATB_2024_conservative,.csv,,,,,, -/inputs/plant_characteristics/nuclear-smr_ATB_2024_moderate.csv,/inputs/plant_characteristics,nuclear-smr_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/nuclear_ATB_2024_advanced.csv,/inputs/plant_characteristics,nuclear_ATB_2024_advanced,.csv,,,,,, -/inputs/plant_characteristics/nuclear_ATB_2024_conservative.csv,/inputs/plant_characteristics,nuclear_ATB_2024_conservative,.csv,,,,,, -/inputs/plant_characteristics/nuclear_ATB_2024_moderate.csv,/inputs/plant_characteristics,nuclear_ATB_2024_moderate,.csv,,,,,, -/inputs/plant_characteristics/ofs-wind_ATB_2023_advanced.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_advanced,.csv,"2023 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2004,,Inputs file, -/inputs/plant_characteristics/ofs-wind_ATB_2023_conservative.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_conservative,.csv,"2023 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2004,,Inputs file, -/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_moderate,.csv,"2023 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2004,,Inputs file, -/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate_noFloating.csv,/inputs/plant_characteristics,ofs-wind_ATB_2023_moderate_noFloating,.csv,,,,,, -/inputs/plant_characteristics/ofs-wind_ATB_2024_advanced.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_advanced,.csv,"2024 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2022,,Inputs file, -/inputs/plant_characteristics/ofs-wind_ATB_2024_conservative.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_conservative,.csv,"2024 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2022,,Inputs file, -/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_moderate,.csv,"2024 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year ",,2022,,Inputs file, -/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate_noFloating.csv,/inputs/plant_characteristics,ofs-wind_ATB_2024_moderate_noFloating,.csv,"2024 moderate_noFloating ofs-wind capital (5x floating capital cost), fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year",,2022,,Inputs file, -/inputs/plant_characteristics/ons-wind_ATB_2023_advanced.csv,/inputs/plant_characteristics,ons-wind_ATB_2023_advanced,.csv,,,,,, -/inputs/plant_characteristics/ons-wind_ATB_2023_conservative.csv,/inputs/plant_characteristics,ons-wind_ATB_2023_conservative,.csv,,,,,, -/inputs/plant_characteristics/ons-wind_ATB_2023_moderate.csv,/inputs/plant_characteristics,ons-wind_ATB_2023_moderate,.csv,,,,,, -/inputs/plant_characteristics/ons-wind_ATB_2024_advanced.csv,/inputs/plant_characteristics,ons-wind_ATB_2024_advanced,.csv,Advanced cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind,,2022,,Inputs file, -/inputs/plant_characteristics/ons-wind_ATB_2024_conservative.csv,/inputs/plant_characteristics,ons-wind_ATB_2024_conservative,.csv,Conservative cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind,,2022,,Inputs file, -/inputs/plant_characteristics/ons-wind_ATB_2024_moderate.csv,/inputs/plant_characteristics,ons-wind_ATB_2024_moderate,.csv,Moderate cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind,,2022,,Inputs file, -/inputs/plant_characteristics/other_plantchar.csv,/inputs/plant_characteristics,other_plantchar,.csv,,,,,, -/inputs/plant_characteristics/outage_forced_static.csv,/inputs/plant_characteristics,outage_forced_static,.csv,Forced outage rates by technology,,,,Inputs file, -/inputs/plant_characteristics/outage_forced_temperature_murphy2019.csv,/inputs/plant_characteristics,outage_forced_temperature_murphy2019,.csv,,,,,, -/inputs/plant_characteristics/outage_scheduled_monthly.csv,/inputs/plant_characteristics,outage_scheduled_monthly,.csv,,,,,, -/inputs/plant_characteristics/outage_scheduled_static.csv,/inputs/plant_characteristics,outage_scheduled_static,.csv,Scheduled outage rate by technology,,,,, -/inputs/plant_characteristics/pvb_benchmark2020.csv,/inputs/plant_characteristics,pvb_benchmark2020,.csv,,,,,, -/inputs/plant_characteristics/ramprate.csv,/inputs/plant_characteristics,ramprate,.csv,Generator ramp rates by technology,,,,, -/inputs/plant_characteristics/startcost.csv,/inputs/plant_characteristics,startcost,.csv,,,,,, -/inputs/plant_characteristics/unitsize_atb.csv,/inputs/plant_characteristics,unitsize_atb,.csv,,,,,, -/inputs/plant_characteristics/upv_ATB_2023_advanced.csv,/inputs/plant_characteristics,upv_ATB_2023_advanced,.csv,"2023 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, -/inputs/plant_characteristics/upv_ATB_2023_conservative.csv,/inputs/plant_characteristics,upv_ATB_2023_conservative,.csv,"2023 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, -/inputs/plant_characteristics/upv_ATB_2023_moderate.csv,/inputs/plant_characteristics,upv_ATB_2023_moderate,.csv,"2023 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, -/inputs/plant_characteristics/upv_ATB_2024_advanced.csv,/inputs/plant_characteristics,upv_ATB_2024_advanced,.csv,"2024 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, -/inputs/plant_characteristics/upv_ATB_2024_conservative.csv,/inputs/plant_characteristics,upv_ATB_2024_conservative,.csv,"2024 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, -/inputs/plant_characteristics/upv_ATB_2024_moderate.csv,/inputs/plant_characteristics,upv_ATB_2024_moderate,.csv,"2024 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year",,2004,,Inputs file, -/inputs/plant_characteristics/years_until_endogenous.csv,/inputs/plant_characteristics,years_until_endogenous,.csv,,,,,, -/inputs/profiles_cf/cf_distpv_county.h5,/inputs/profiles_cf,cf_distpv_county,.h5,,,,,, -/inputs/profiles_cf/cf_upv_limited_ba.h5,/inputs/profiles_cf,cf_upv_limited_ba,.h5,,,,,, -/inputs/profiles_cf/cf_upv_limited_county.h5,/inputs/profiles_cf,cf_upv_limited_county,.h5,,,,,, -/inputs/profiles_cf/cf_upv_open_ba.h5,/inputs/profiles_cf,cf_upv_open_ba,.h5,,,,,, -/inputs/profiles_cf/cf_upv_open_county.h5,/inputs/profiles_cf,cf_upv_open_county,.h5,,,,,, -/inputs/profiles_cf/cf_upv_reference_ba.h5,/inputs/profiles_cf,cf_upv_reference_ba,.h5,,,,,, -/inputs/profiles_cf/cf_upv_reference_county.h5,/inputs/profiles_cf,cf_upv_reference_county,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_meshed_limited_ba.h5,/inputs/profiles_cf,cf_wind-ofs_meshed_limited_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_meshed_open_ba.h5,/inputs/profiles_cf,cf_wind-ofs_meshed_open_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_meshed_reference_ba.h5,/inputs/profiles_cf,cf_wind-ofs_meshed_reference_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_radial_limited_ba.h5,/inputs/profiles_cf,cf_wind-ofs_radial_limited_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_radial_limited_county.h5,/inputs/profiles_cf,cf_wind-ofs_radial_limited_county,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_radial_open_ba.h5,/inputs/profiles_cf,cf_wind-ofs_radial_open_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_radial_open_county.h5,/inputs/profiles_cf,cf_wind-ofs_radial_open_county,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_radial_reference_ba.h5,/inputs/profiles_cf,cf_wind-ofs_radial_reference_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ofs_radial_reference_county.h5,/inputs/profiles_cf,cf_wind-ofs_radial_reference_county,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ons_limited_ba.h5,/inputs/profiles_cf,cf_wind-ons_limited_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ons_limited_county.h5,/inputs/profiles_cf,cf_wind-ons_limited_county,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ons_open_ba.h5,/inputs/profiles_cf,cf_wind-ons_open_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ons_open_county.h5,/inputs/profiles_cf,cf_wind-ons_open_county,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ons_reference_ba.h5,/inputs/profiles_cf,cf_wind-ons_reference_ba,.h5,,,,,, -/inputs/profiles_cf/cf_wind-ons_reference_county.h5,/inputs/profiles_cf,cf_wind-ons_reference_county,.h5,,,,,, -/inputs/profiles_demand/demand_EER2023_100by2050.h5,/inputs/profiles_demand,demand_EER2023_100by2050,.h5,,,,,, -/inputs/profiles_demand/demand_EER2023_Baseline_AEO2022.h5,/inputs/profiles_demand,demand_EER2023_Baseline_AEO2022,.h5,,,,,, -/inputs/profiles_demand/demand_EER2023_IRAlow.h5,/inputs/profiles_demand,demand_EER2023_IRAlow,.h5,,,,,, -/inputs/profiles_demand/demand_EER2023_IRAmoderate.h5,/inputs/profiles_demand,demand_EER2023_IRAmoderate,.h5,,,,,, -/inputs/profiles_demand/demand_EER2025_100by2050.h5,/inputs/profiles_demand,demand_EER2025_100by2050,.h5,,,,,, -/inputs/profiles_demand/demand_EER2025_Baseline_AEO2023.h5,/inputs/profiles_demand,demand_EER2025_Baseline_AEO2023,.h5,,,,,, -/inputs/profiles_demand/demand_EER2025_IRAlow.h5,/inputs/profiles_demand,demand_EER2025_IRAlow,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_Baseline.h5,/inputs/profiles_demand,demand_EFS_Baseline,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_Clean2035.h5,/inputs/profiles_demand,demand_EFS_Clean2035,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_Clean2035_LTS.h5,/inputs/profiles_demand,demand_EFS_Clean2035_LTS,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_Clean2035clip1pct.h5,/inputs/profiles_demand,demand_EFS_Clean2035clip1pct,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_HIGH.h5,/inputs/profiles_demand,demand_EFS_HIGH,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_MEDIUM.h5,/inputs/profiles_demand,demand_EFS_MEDIUM,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_MEDIUMStretch2040.h5,/inputs/profiles_demand,demand_EFS_MEDIUMStretch2040,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_MEDIUMStretch2046.h5,/inputs/profiles_demand,demand_EFS_MEDIUMStretch2046,.h5,,,,,, -/inputs/profiles_demand/demand_EFS_REFERENCE.h5,/inputs/profiles_demand,demand_EFS_REFERENCE,.h5,,,,,, -/inputs/profiles_demand/demand_historic.h5,/inputs/profiles_demand,demand_historic,.h5,,,,,, -/inputs/remote/cf_distpv_county_18421977.h5,/inputs/remote,cf_distpv_county_18421977,.h5,,,,,, -/inputs/remote/cf_upv_limited_ba_18407660.h5,/inputs/remote,cf_upv_limited_ba_18407660,.h5,,,,,, -/inputs/remote/cf_upv_limited_county_18407660.h5,/inputs/remote,cf_upv_limited_county_18407660,.h5,,,,,, -/inputs/remote/cf_upv_open_ba_18407660.h5,/inputs/remote,cf_upv_open_ba_18407660,.h5,,,,,, -/inputs/remote/cf_upv_open_county_18407660.h5,/inputs/remote,cf_upv_open_county_18407660,.h5,,,,,, -/inputs/remote/cf_upv_reference_ba_18407660.h5,/inputs/remote,cf_upv_reference_ba_18407660,.h5,,,,,, -/inputs/remote/cf_upv_reference_county_18407660.h5,/inputs/remote,cf_upv_reference_county_18407660,.h5,,,,,, -/inputs/remote/cf_wind-ofs_meshed_limited_ba_18423723.h5,/inputs/remote,cf_wind-ofs_meshed_limited_ba_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_meshed_open_ba_18423723.h5,/inputs/remote,cf_wind-ofs_meshed_open_ba_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_meshed_reference_ba_18423723.h5,/inputs/remote,cf_wind-ofs_meshed_reference_ba_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_radial_limited_ba_18423723.h5,/inputs/remote,cf_wind-ofs_radial_limited_ba_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_radial_limited_county_18423723.h5,/inputs/remote,cf_wind-ofs_radial_limited_county_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_radial_open_ba_18423723.h5,/inputs/remote,cf_wind-ofs_radial_open_ba_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_radial_open_county_18423723.h5,/inputs/remote,cf_wind-ofs_radial_open_county_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_radial_reference_ba_18423723.h5,/inputs/remote,cf_wind-ofs_radial_reference_ba_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ofs_radial_reference_county_18423723.h5,/inputs/remote,cf_wind-ofs_radial_reference_county_18423723,.h5,,,,,, -/inputs/remote/cf_wind-ons_limited_ba_18422200.h5,/inputs/remote,cf_wind-ons_limited_ba_18422200,.h5,,,,,, -/inputs/remote/cf_wind-ons_limited_county_18422200.h5,/inputs/remote,cf_wind-ons_limited_county_18422200,.h5,,,,,, -/inputs/remote/cf_wind-ons_open_ba_18422200.h5,/inputs/remote,cf_wind-ons_open_ba_18422200,.h5,,,,,, -/inputs/remote/cf_wind-ons_open_county_18422200.h5,/inputs/remote,cf_wind-ons_open_county_18422200,.h5,,,,,, -/inputs/remote/cf_wind-ons_reference_ba_18422200.h5,/inputs/remote,cf_wind-ons_reference_ba_18422200,.h5,,,,,, -/inputs/remote/cf_wind-ons_reference_county_18422200.h5,/inputs/remote,cf_wind-ons_reference_county_18422200,.h5,,,,,, -/inputs/remote/demand_EER2023_100by2050_18423998.h5,/inputs/remote,demand_EER2023_100by2050_18423998,.h5,,,,,, -/inputs/remote/demand_EER2023_Baseline_AEO2022_18423998.h5,/inputs/remote,demand_EER2023_Baseline_AEO2022_18423998,.h5,,,,,, -/inputs/remote/demand_EER2023_IRAlow_18423998.h5,/inputs/remote,demand_EER2023_IRAlow_18423998,.h5,,,,,, -/inputs/remote/demand_EER2023_IRAmoderate_18423998.h5,/inputs/remote,demand_EER2023_IRAmoderate_18423998,.h5,,,,,, -/inputs/remote/demand_EER2025_100by2050_18435264.h5,/inputs/remote,demand_EER2025_100by2050_18435264,.h5,,,,,, -/inputs/remote/demand_EER2025_Baseline_AEO2023_18435264.h5,/inputs/remote,demand_EER2025_Baseline_AEO2023_18435264,.h5,,,,,, -/inputs/remote/demand_EER2025_IRAlow_18435264.h5,/inputs/remote,demand_EER2025_IRAlow_18435264,.h5,,,,,, -/inputs/remote/demand_EFS_Baseline_18461543.h5,/inputs/remote,demand_EFS_Baseline_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_Clean2035_18461543.h5,/inputs/remote,demand_EFS_Clean2035_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_Clean2035_LTS_18461543.h5,/inputs/remote,demand_EFS_Clean2035_LTS_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_Clean2035clip1pct_18461543.h5,/inputs/remote,demand_EFS_Clean2035clip1pct_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_HIGH_18461543.h5,/inputs/remote,demand_EFS_HIGH_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_MEDIUM_18461543.h5,/inputs/remote,demand_EFS_MEDIUM_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_MEDIUMStretch2040_18461543.h5,/inputs/remote,demand_EFS_MEDIUMStretch2040_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_MEDIUMStretch2046_18461543.h5,/inputs/remote,demand_EFS_MEDIUMStretch2046_18461543,.h5,,,,,, -/inputs/remote/demand_EFS_REFERENCE_18461543.h5,/inputs/remote,demand_EFS_REFERENCE_18461543,.h5,,,,,, -/inputs/remote/demand_historic_18462671.h5,/inputs/remote,demand_historic_18462671,.h5,,,,,, -/inputs/remote_files.csv,/inputs,remote_files,.csv,,,,,, -/inputs/reserves/ccseason_dates.csv,/inputs/reserves,ccseason_dates,.csv,,,,,, -/inputs/reserves/opres_periods.csv,/inputs/reserves,opres_periods,.csv,,,,,, -/inputs/reserves/orperc.csv,/inputs/reserves,orperc,.csv,,,,,, -/inputs/reserves/peak_net_imports.csv,/inputs/reserves,peak_net_imports,.csv,,,,,, -/inputs/reserves/prm_annual.csv,/inputs/reserves,prm_annual,.csv,Annual planning reserve margin by NERC region,,,,, -/inputs/reserves/ramptime.csv,/inputs/reserves,ramptime,.csv,,,,,, -/inputs/scalars.csv,/inputs,scalars,.csv,,,,,, -/inputs/sets/aclike.csv,/inputs/sets,aclike,.csv,set of AC transmission capacity types,,,,GAMS set, -/inputs/sets/allt.csv,/inputs/sets,allt,.csv,set of all potential years,,,,GAMS set, -/inputs/sets/bioclass.csv,/inputs/sets,bioclass,.csv,set of bio tech classes,,,,GAMS set, -/inputs/sets/ccsflex_cat.csv,/inputs/sets,ccsflex_cat,.csv,set of flexible ccs performance parameter categories,,,,GAMS set, -/inputs/sets/climate_param.csv,/inputs/sets,climate_param,.csv,set of parameters defined in climate_heuristics_finalyear,,,,GAMS set, -/inputs/sets/consumecat.csv,/inputs/sets,consumecat,.csv,set of categories for consuming facility characteristics,,,,GAMS set, -/inputs/sets/csapr_cat.csv,/inputs/sets,csapr_cat,.csv,set of CSAPR regulation categories,,,,GAMS set, -/inputs/sets/csapr_group.csv,/inputs/sets,csapr_group,.csv,set of CSAPR trading groups,,,,GAMS set, -/inputs/sets/ctt.csv,/inputs/sets,ctt,.csv,set of cooling technology types,,,,GAMS set, -/inputs/sets/e.csv,/inputs/sets,e,.csv,set of emission categories used in model,,,,GAMS set, -/inputs/sets/eall.csv,/inputs/sets,eall,.csv,set of emission categories used in reporting,,,,GAMS set, -/inputs/sets/etype.csv,/inputs/sets,etype,.csv,,,,,, -/inputs/sets/f.csv,/inputs/sets,f,.csv,set of fuel types,,,,GAMS set, -/inputs/sets/flex_type.csv,/inputs/sets,flex_type,.csv,set of demand flexibility types,,,,GAMS set, -/inputs/sets/fuel2tech.csv,/inputs/sets,fuel2tech,.csv,mapping between fuel types and generations,,,,GAMS set, -/inputs/sets/fuelbin.csv,/inputs/sets,fuelbin,.csv,set of gas usage brackets,,,,GAMS set, -/inputs/sets/gb.csv,/inputs/sets,gb,.csv,set of gas price bins,,,,GAMS set, -/inputs/sets/gbin.csv,/inputs/sets,gbin,.csv,set of growth bins,,,,GAMS set, -/inputs/sets/geotech.csv,/inputs/sets,geotech,.csv,set of geothermal technology categories,,,,GAMS set, -/inputs/sets/h2_st.csv,/inputs/sets,h2_st,.csv,defines investments needed to store and transport H2,,,,GAMS set, -/inputs/sets/h2_stor.csv,/inputs/sets,h2_stor,.csv,set of H2 storage options,,,,GAMS set, -/inputs/sets/hintage_char.csv,/inputs/sets,hintage_char,.csv,set of characteristics available in hintage_data,,,,GAMS set, -/inputs/sets/i.csv,/inputs/sets,i,.csv,set of technologies,,,,GAMS set, -/inputs/sets/i_geotech.csv,/inputs/sets,i_geotech,.csv,crosswalk between an individual geothermal technology and its category,,,,GAMS set, -/inputs/sets/i_h2_ptc_gen.csv,/inputs/sets,i_h2_ptc_gen,.csv,set of technologies which can produce energy for electrolyzers claiming the hydrogen production tax credit due to their low lifecycle carbon emissions,,,,GAMS set, -/inputs/sets/i_p.csv,/inputs/sets,i_p,.csv,mapping from technologies to the products they produce,,,,GAMS set, -/inputs/sets/i_subtech.csv,/inputs/sets,i_subtech,.csv,set of categories for subtechs,,,,GAMS set, -/inputs/sets/i_water_nocooling.csv,/inputs/sets,i_water_nocooling,.csv,"set of technologies that use water, but are not differentiated by cooling tech and water source",,,,GAMS set, -/inputs/sets/lcclike.csv,/inputs/sets,lcclike,.csv,set of transmission capacity types where lines are bundled with AC/DC converters,,,,GAMS set, -/inputs/sets/month.csv,/inputs/sets,month,.csv,,,,,GAMS set, -/inputs/sets/noretire.csv,/inputs/sets,noretire,.csv,set of technologies that will never be retired,,,,GAMS set, -/inputs/sets/notvsc.csv,/inputs/sets,notvsc,.csv,set of transmission capacity types that are not VSC,,,,GAMS set, -/inputs/sets/ofstype.csv,/inputs/sets,ofstype,.csv,set of offshore types used in offshore requirement constraint (eq_RPS_OFSWind),,,,GAMS set, -/inputs/sets/ofstype_i.csv,/inputs/sets,ofstype_i,.csv,crosswalk between ofstype and i,,,,GAMS set, -/inputs/sets/orcat.csv,/inputs/sets,orcat,.csv,set of operating reserve categories,,,,GAMS set, -/inputs/sets/ortype.csv,/inputs/sets,ortype,.csv,set of types of operating reserve constraints,,,,GAMS set, -/inputs/sets/p.csv,/inputs/sets,p,.csv,set of products produced,,,,GAMS set, -/inputs/sets/pcat.csv,/inputs/sets,pcat,.csv,set of prescribed technology categories,,,,GAMS set, -/inputs/sets/plantcat.csv,/inputs/sets,plantcat,.csv,set of categories for plant characteristics,,,,GAMS set, -/inputs/sets/prepost.csv,/inputs/sets,prepost,.csv,,,,,GAMS set, -/inputs/sets/prescriptivelink0.csv,/inputs/sets,prescriptivelink0,.csv,initial set of prescribed categories and their technologies - used in assigning prescribed builds,,,,GAMS set, -/inputs/sets/pvb_agg.csv,/inputs/sets,pvb_agg,.csv,crosswalk between hybrid pv+battery configurations and technology options,,,,GAMS set, -/inputs/sets/pvb_config.csv,/inputs/sets,pvb_config,.csv,set of hybrid pv+battery configurations,,,,GAMS set, -/inputs/sets/quarter.csv,/inputs/sets,quarter,.csv,,,,,GAMS set, -/inputs/sets/resourceclass.csv,/inputs/sets,resourceclass,.csv,set of renewable resource classes,,,,GAMS set, -/inputs/sets/RPSCat.csv,/inputs/sets,RPSCat,.csv,"set of RPS constraint categories, including clean energy standards",,,,GAMS set, -/inputs/sets/sc_cat.csv,/inputs/sets,sc_cat,.csv,set of supply curve categories (capacity and cost),,,,GAMS set, -/inputs/sets/sdbin.csv,/inputs/sets,sdbin,.csv,set of storage durage bins,,,,GAMS set, -/inputs/sets/sw.csv,/inputs/sets,sw,.csv,set of surface water types where access is based on consumption not withdrawal,,,,GAMS set, -/inputs/sets/tg.csv,/inputs/sets,tg,.csv,set of technology groups,,,,GAMS set, -/inputs/sets/tg_rsc_cspagg.csv,/inputs/sets,tg_rsc_cspagg,.csv,set of csp technologies that belong to the same class,,,,GAMS set, -/inputs/sets/tg_rsc_upvagg.csv,/inputs/sets,tg_rsc_upvagg,.csv,set of pv and pvb technologies that belong to the same class,,,,GAMS set, -/inputs/sets/trancap_fut_cat.csv,/inputs/sets,trancap_fut_cat,.csv,set of categories of near-term transmission projects that describe the likelihood of being completed,,,,GAMS set, -/inputs/sets/trtype.csv,/inputs/sets,trtype,.csv,set of transmission capacity types,,,,GAMS set, -/inputs/sets/unitspec_upgrades.csv,/inputs/sets,unitspec_upgrades,.csv,set of upgraded technologies that get unit-specific characteristics,,,,GAMS set, -/inputs/sets/upgrade_hintage_char.csv,/inputs/sets,upgrade_hintage_char,.csv,set to operate over in extension of hintage_data characteristics when sw_upgrades = 1,,,,GAMS set, -/inputs/sets/w.csv,/inputs/sets,w,.csv,set of water withdrawal or consumption options for water techs,,,,GAMS set, -/inputs/sets/wst.csv,/inputs/sets,wst,.csv,set of water source types,,,,GAMS set, -/inputs/sets/wst_climate.csv,/inputs/sets,wst_climate,.csv,set of water sources affected by climate change,,,,GAMS set, -/inputs/sets/yearafter.csv,/inputs/sets,yearafter,.csv,set to loop over for the final year calculation,,,,GAMS set, -/inputs/shapefiles/state_fips_codes.csv,/inputs/shapefiles,state_fips_codes,.csv,Mapping of states to FIPS codes and postcal code abbreviations,,,,, -/inputs/state_policies/acp_disallowed.csv,/inputs/state_policies,acp_disallowed,.csv,List of states which do not allow alternative compliance payments in place of meeting RPS or CES requirements ,,,,, -/inputs/state_policies/acp_prices.csv,/inputs/state_policies,acp_prices,.csv,,,,,, -/inputs/state_policies/ces_fraction.csv,/inputs/state_policies,ces_fraction,.csv,Annual compliance for states with a CES policy,,,,, -/inputs/state_policies/forced_retirements.csv,/inputs/state_policies,forced_retirements,.csv,List of regions with mandatory retirement policies for certain technologies,,,,, -/inputs/state_policies/hydrofrac_policy.csv,/inputs/state_policies,hydrofrac_policy,.csv,,,,,, -/inputs/state_policies/ng_crf_penalty_st.csv,/inputs/state_policies,ng_crf_penalty_st,.csv,Cost adjustment for NG techs in states where all NG techs must be retired by a certain year,"allt,st",N/A,https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220,Inputs,rate (unitless) -/inputs/state_policies/nuclear_subsidies.csv,/inputs/state_policies,nuclear_subsidies,.csv,,,,,, -/inputs/state_policies/offshore_req_default.csv,/inputs/state_policies,offshore_req_default,.csv,"default state mandates of offshore wind capacity, updated in November 2025","st,allt",,,Inputs,MW -/inputs/state_policies/oosfrac.csv,/inputs/state_policies,oosfrac,.csv,Defines the fraction of renewable and clean energy credits can be purchased from out of state (oos). Applied for RPS and CES,,,,, -/inputs/state_policies/recstyle.csv,/inputs/state_policies,recstyle,.csv,"Indication for how to apply state requirement (0 = end-use sales, 1 = bus-bar sales, 2 = generation). Default is 0.",,,,, -/inputs/state_policies/rectable.csv,/inputs/state_policies,rectable,.csv,Table defining which states are allowed to trade RECs,,,,, -/inputs/state_policies/rps_fraction.csv,/inputs/state_policies,rps_fraction,.csv,Indicates what fraction of sales or generation (based on recstyle.csv) must be from renewable energy ,,,,, -/inputs/state_policies/storage_mandates.csv,/inputs/state_policies,storage_mandates,.csv,Energy storage mandates by region,,,,, -/inputs/state_policies/techs_banned_ces.csv,/inputs/state_policies,techs_banned_ces,.csv,Indicates which technolgies are not eligible to contribute to CES ,,,,, -/inputs/state_policies/techs_banned_imports_rps.csv,/inputs/state_policies,techs_banned_imports_rps,.csv,,,,,, -/inputs/state_policies/techs_banned_rps.csv,/inputs/state_policies,techs_banned_rps,.csv,Indicates which technolgies are not eligible to contribute to RPS,,,,, -/inputs/state_policies/unbundled_limit_ces.csv,/inputs/state_policies,unbundled_limit_ces,.csv,Limit on fraction of credits towards CES which can be purchased unbundled from other states ,,,,, -/inputs/state_policies/unbundled_limit_rps.csv,/inputs/state_policies,unbundled_limit_rps,.csv,Limit on fraction of credits towards RPS which can be purchased unbundled from other states ,,,,, -/inputs/storage/cap_existing_psh.csv,/inputs/storage,cap_existing_psh,.csv,"County-wide PSH operational capacity, pump capacity, and max energy, based on plant-level data from https://www.hydropower.org/hydropower-pumped-storage-tool",,,,,MW/MWh -/inputs/storage/PSH_supply_curves_durations.csv,/inputs/storage,PSH_supply_curves_durations,.csv,,,,,, -/inputs/storage/storage_duration.csv,/inputs/storage,storage_duration,.csv,,,,,, -/inputs/supply_curve/bio_supplycurve.csv,/inputs/supply_curve,bio_supplycurve,.csv,Regional biomass supply and costs by resource class,,2015,,, -/inputs/supply_curve/dollaryear.csv,/inputs/supply_curve,dollaryear,.csv,,,,,, -/inputs/supply_curve/dr_shed_cap.csv,/inputs/supply_curve,dr_shed_cap,.csv,,,,,, -/inputs/supply_curve/dr_shed_cost.csv,/inputs/supply_curve,dr_shed_cost,.csv,,,,,, -/inputs/supply_curve/hyd_add_upg_cap.csv,/inputs/supply_curve,hyd_add_upg_cap,.csv,,,,,, -/inputs/supply_curve/hydcap.csv,/inputs/supply_curve,hydcap,.csv,,,,,, -/inputs/supply_curve/hydcost.csv,/inputs/supply_curve,hydcost,.csv,,,,,, -/inputs/supply_curve/interconnection_land.h5,/inputs/supply_curve,interconnection_land,.h5,,,,,, -/inputs/supply_curve/interconnection_offshore.h5,/inputs/supply_curve,interconnection_offshore,.h5,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_ref_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_ref_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wEphemeral_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024,.csv,PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_ref_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_ref_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wEphemeral_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024,.csv,PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_ref_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_ref_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wEphemeral_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024,.csv,PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_ref_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_ref_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wEphemeral_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_10hr_wExist_wEph_mar2024,.csv,PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_ref_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_ref_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wEphemeral_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_12hr_wExist_wEph_mar2024,.csv,PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_ref_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_ref_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEphemeral_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wEphemeral_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_apr2025.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_wEph_apr2025,.csv,,,,,, -/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_mar2024.csv,/inputs/supply_curve,PSH_supply_curves_cost_8hr_wExist_wEph_mar2024,.csv,PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline,,2004,https://www.nrel.gov/gis/psh-supply-curves.html,, -/inputs/supply_curve/rev_paths.csv,/inputs/supply_curve,rev_paths,.csv,,,,,, -/inputs/supply_curve/sc_point_gid_old2new.csv,/inputs/supply_curve,sc_point_gid_old2new,.csv,,,,,, -/inputs/supply_curve/sitemap.h5,/inputs/supply_curve,sitemap,.h5,,,,,, -/inputs/supply_curve/supplycurve_egs-reference.csv,/inputs/supply_curve,supplycurve_egs-reference,.csv,,,,,, -/inputs/supply_curve/supplycurve_upv-limited.csv,/inputs/supply_curve,supplycurve_upv-limited,.csv,UPV supply curve from reV for the limited siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,,capacity numbers are in MW_DC and cost numbers are in $/MW_AC -/inputs/supply_curve/supplycurve_upv-open.csv,/inputs/supply_curve,supplycurve_upv-open,.csv,UPV supply curve from reV for the open siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,,capacity numbers are in MW_DC and cost numbers are in $/MW_AC -/inputs/supply_curve/supplycurve_upv-reference.csv,/inputs/supply_curve,supplycurve_upv-reference,.csv,UPV supply curve from reV for the reference siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,,capacity numbers are in MW_DC and cost numbers are in $/MW_AC -/inputs/supply_curve/supplycurve_wind-ofs-limited.csv,/inputs/supply_curve,supplycurve_wind-ofs-limited,.csv,Offshore sind supply curve from reV for the limited siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, -/inputs/supply_curve/supplycurve_wind-ofs-open.csv,/inputs/supply_curve,supplycurve_wind-ofs-open,.csv,Offshore wind supply curve from reV for the open siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, -/inputs/supply_curve/supplycurve_wind-ofs-reference.csv,/inputs/supply_curve,supplycurve_wind-ofs-reference,.csv,Offshore wind supply curve from reV for the reference siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, -/inputs/supply_curve/supplycurve_wind-ons-limited.csv,/inputs/supply_curve,supplycurve_wind-ons-limited,.csv,Land-based wind supply curve from reV for the limited siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, -/inputs/supply_curve/supplycurve_wind-ons-open.csv,/inputs/supply_curve,supplycurve_wind-ons-open,.csv,Land-based wind supply curve from reV for the open siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, -/inputs/supply_curve/supplycurve_wind-ons-reference.csv,/inputs/supply_curve,supplycurve_wind-ons-reference,.csv,Land-based wind supply curve from reV for the reference siting scenario,,specified in inputs/supply_curve/dollaryear.csv,https://docs.nrel.gov/docs/fy25osti/91900.pdf,, -/inputs/supply_curve/trans_intra_cost_adder.csv,/inputs/supply_curve,trans_intra_cost_adder,.csv,,,,,, -/inputs/tech-subset-table.csv,/inputs,tech-subset-table,.csv,Maps all technologies to specific subsets of technologies,,,,, -/inputs/techs/tech_resourceclass.csv,/inputs/techs,tech_resourceclass,.csv,,,,,, -/inputs/techs/techs_default.csv,/inputs/techs,techs_default,.csv,List of technologies to be used in the model,,,,, -/inputs/techs/techs_subsetForTesting.csv,/inputs/techs,techs_subsetForTesting,.csv,Short list of technologies for testing,,,,, -/inputs/temporal/month2quarter.csv,/inputs/temporal,month2quarter,.csv,,,,,, -/inputs/temporal/period_szn_user.csv,/inputs/temporal,period_szn_user,.csv,,,,,, -/inputs/temporal/reeds_region_tz_map.csv,/inputs/temporal,reeds_region_tz_map,.csv,,,,,, -/inputs/temporal/stressperiods_user.csv,/inputs/temporal,stressperiods_user,.csv,,,,,, -/inputs/transmission/cost_hurdle_country.csv,/inputs/transmission,cost_hurdle_country,.csv,Cost for transmission hurdle rate by country,country,2004,,GAMS set, -/inputs/transmission/cost_hurdle_intra.csv,/inputs/transmission,cost_hurdle_intra,.csv,,,,,, -/inputs/transmission/rev_transmission_basecost.csv,/inputs/transmission,rev_transmission_basecost,.csv,Unweighted average base cost across the four regions for which we have transmission cost data.,Transreg,2004,,inputs, -/inputs/transmission/transmission_capacity_future_ba_baseline.csv,/inputs/transmission,transmission_capacity_future_ba_baseline,.csv,Future transmission capacity additions for the baseline case at BA resolution,"r,rr",,,inputs, -/inputs/transmission/transmission_capacity_future_ba_default.csv,/inputs/transmission,transmission_capacity_future_ba_default,.csv,Future transmission capacity additions for the default case at BA resolution,"r,rr",,,inputs, -/inputs/transmission/transmission_capacity_future_ba_LCC_all.csv,/inputs/transmission,transmission_capacity_future_ba_LCC_all,.csv,Future transmission capacity additions for the LCC_all case at BA resolution,"r,rr",,,inputs, -/inputs/transmission/transmission_capacity_future_ba_VSC_all.csv,/inputs/transmission,transmission_capacity_future_ba_VSC_all,.csv,Future transmission capacity additions for the VSC_all_case at BA resolution,"r,rr",,,inputs, -/inputs/transmission/transmission_capacity_future_county_baseline.csv,/inputs/transmission,transmission_capacity_future_county_baseline,.csv,Future transmission capacity additions for the baseline case at county resolution,"r,rr",,,inputs, -/inputs/transmission/transmission_capacity_future_county_default.csv,/inputs/transmission,transmission_capacity_future_county_default,.csv,Future transmission capacity additions for the default case at county resolution,"r,rr",,,inputs, -/inputs/transmission/transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629.csv,/inputs/transmission,transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629,.csv,Future transmission capacity additions for the LCC_1000miles_demand1_wind1_subferc_20230629 case at BA resolution,"r,rr",,,inputs, -/inputs/transmission/transmission_cost_ac_500kv_ba.h5,/inputs/transmission,transmission_cost_ac_500kv_ba,.h5,Transmission costs for new 500 kV AC at BA resolution,,,,, -/inputs/transmission/transmission_cost_ac_500kv_county.h5,/inputs/transmission,transmission_cost_ac_500kv_county,.h5,Transmission costs for new 500 kV AC at county resolution,,,,, -/inputs/transmission/transmission_cost_dc_ba.csv,/inputs/transmission,transmission_cost_dc_ba,.csv,Transmission costs for new 500 kV DC at BA resolution,,,,, -/inputs/transmission/transmission_cost_dc_county.csv,/inputs/transmission,transmission_cost_dc_county,.csv,Transmission costs for new 500 kV DC at county resolution,,,,, -/inputs/transmission/transmission_distance_ba.h5,/inputs/transmission,transmission_distance_ba,.h5,Length of least-cost transmission paths between zones at BA resolution,,,,, -/inputs/transmission/transmission_distance_county.h5,/inputs/transmission,transmission_distance_county,.h5,Length of least-cost transmission paths between zones at county resolution,,,,, -/inputs/upgrades/i_coolingtech_watersource_upgrades.csv,/inputs/upgrades,i_coolingtech_watersource_upgrades,.csv,List of cooling technologies for water sources that can be upgraded.,i,N/A,N/A,Inputs, -/inputs/upgrades/i_coolingtech_watersource_upgrades_link.csv,/inputs/upgrades,i_coolingtech_watersource_upgrades_link,.csv,"List of cooling technologies for water sources that can be upgraded + their to, from, ctt (cooling technology type) and wst (water source type)","i, ctt, wst",N/A,N/A,Inputs, -/inputs/upgrades/upgrade_costs_ccs_coal.csv,/inputs/upgrades,upgrade_costs_ccs_coal,.csv,,,,,, -/inputs/upgrades/upgrade_costs_ccs_gas.csv,/inputs/upgrades,upgrade_costs_ccs_gas,.csv,,,,,, -/inputs/upgrades/upgrade_link.csv,/inputs/upgrades,upgrade_link,.csv,"Techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta.",i,N/A,N/A,Inputs, -/inputs/upgrades/upgrade_mult_atb23_ccs_adv.csv,/inputs/upgrades,upgrade_mult_atb23_ccs_adv,.csv,Cost adjustment (advanced) over various years for upgrade technologies,"i,t",N/A,N/A,Inputs, -/inputs/upgrades/upgrade_mult_atb23_ccs_con.csv,/inputs/upgrades,upgrade_mult_atb23_ccs_con,.csv,Cost adjustment (conservative) over various years for upgrade technologies,"i,t",N/A,N/A,Inputs, -/inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv,/inputs/upgrades,upgrade_mult_atb23_ccs_mid,.csv,Cost adjustment (Mid) over various years for upgrade technologies,"i,t",N/A,N/A,Inputs, -/inputs/upgrades/upgradelink_water.csv,/inputs/upgrades,upgradelink_water,.csv,"Water techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta",i,N/A,N/A,Inputs, -/inputs/userinput/futurefiles.csv,/inputs/userinput,futurefiles,.csv,,,,,, -/inputs/userinput/ivt_default.csv,/inputs/userinput,ivt_default,.csv,,,,,, -/inputs/userinput/ivt_small.csv,/inputs/userinput,ivt_small,.csv,,,,,, -/inputs/userinput/ivt_step.csv,/inputs/userinput,ivt_step,.csv,ivt steps for endyears beyond 2050,,,,, -/inputs/userinput/modeled_regions.csv,/inputs/userinput,modeled_regions,.csv,Sets of BA regions that a user can model in a run. Each column is a different region option and can be specified in cases using GSw_Region.,,,,, -/inputs/userinput/windows_2100.csv,/inputs/userinput,windows_2100,.csv,Window size for using window solve method to 2100,,,,, -/inputs/userinput/windows_default.csv,/inputs/userinput,windows_default,.csv,Window size for using window solve method,,,,, -/inputs/userinput/windows_step10.csv,/inputs/userinput,windows_step10,.csv,Window size for beyond2050step10,,,,, -/inputs/userinput/windows_step5.csv,/inputs/userinput,windows_step5,.csv,Window size for beyond2050step5,,,,, -/inputs/valuestreams/var_map.csv,/inputs/valuestreams,var_map,.csv,,,,,, -/inputs/waterclimate/cost_cap_mult.csv,/inputs/waterclimate,cost_cap_mult,.csv,,,,,, -/inputs/waterclimate/cost_vom_mult.csv,/inputs/waterclimate,cost_vom_mult,.csv,,,,,, -/inputs/waterclimate/heat_rate_mult.csv,/inputs/waterclimate,heat_rate_mult,.csv,,,,,, -/inputs/waterclimate/i_coolingtech_watersource.csv,/inputs/waterclimate,i_coolingtech_watersource,.csv,,,,,, -/inputs/waterclimate/i_coolingtech_watersource_link.csv,/inputs/waterclimate,i_coolingtech_watersource_link,.csv,,,,,, -/inputs/waterclimate/tg_rsc_cspagg_tmp.csv,/inputs/waterclimate,tg_rsc_cspagg_tmp,.csv,,,,,, -/inputs/waterclimate/unapp_water_sea_distr.csv,/inputs/waterclimate,unapp_water_sea_distr,.csv,,,,,, -/inputs/waterclimate/wat_access_cap_cost.csv,/inputs/waterclimate,wat_access_cap_cost,.csv,,,,,, -/inputs/waterclimate/water_req_psh_10h_1_51.csv,/inputs/waterclimate,water_req_psh_10h_1_51,.csv,,,,,, -/inputs/waterclimate/water_with_cons_rate.csv,/inputs/waterclimate,water_with_cons_rate,.csv,,,,,, -/postprocessing/air_quality/rcm_data/counties_ACS_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,counties_ACS_high_stack_2017,.csv,,,,,, -/postprocessing/air_quality/rcm_data/counties_H6C_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,counties_H6C_high_stack_2017,.csv,,,,,, -/postprocessing/air_quality/rcm_data/states_ACS_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,states_ACS_high_stack_2017,.csv,,,,,, -/postprocessing/air_quality/rcm_data/states_H6C_high_stack_2017.csv,/postprocessing/air_quality/rcm_data,states_H6C_high_stack_2017,.csv,,,,,, -/postprocessing/air_quality/scenarios.csv,/postprocessing/air_quality,scenarios,.csv,,,,,, -/postprocessing/bokehpivot/in/example_custom_styles.csv,/postprocessing/bokehpivot/in,example_custom_styles,.csv,Examples of custom styles used for bokehpivot,,,,, -/postprocessing/bokehpivot/in/example_data_US_electric_power_generation.csv,/postprocessing/bokehpivot/in,example_data_US_electric_power_generation,.csv,Example data for US electric power generation,,,,, -/postprocessing/bokehpivot/in/gis_centroid_rb.csv,/postprocessing/bokehpivot/in,gis_centroid_rb,.csv,,,,,, -/postprocessing/bokehpivot/in/gis_nercr.csv,/postprocessing/bokehpivot/in,gis_nercr,.csv,,,,,, -/postprocessing/bokehpivot/in/gis_nercr_new.csv,/postprocessing/bokehpivot/in,gis_nercr_new,.csv,,,,,, -/postprocessing/bokehpivot/in/gis_rb.csv,/postprocessing/bokehpivot/in,gis_rb,.csv,,,,,, -/postprocessing/bokehpivot/in/gis_rs.csv,/postprocessing/bokehpivot/in,gis_rs,.csv,,,,,, -/postprocessing/bokehpivot/in/gis_rto.csv,/postprocessing/bokehpivot/in,gis_rto,.csv,,,,,, -/postprocessing/bokehpivot/in/gis_st.csv,/postprocessing/bokehpivot/in,gis_st,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/class_map.csv,/postprocessing/bokehpivot/in/reeds2,class_map,.csv,Class mapping for bokehpivot postprocessing,,,,, -/postprocessing/bokehpivot/in/reeds2/class_style.csv,/postprocessing/bokehpivot/in/reeds2,class_style,.csv,Custom styles for classes in bokehpivot ,,,,, -/postprocessing/bokehpivot/in/reeds2/con_adj_map.csv,/postprocessing/bokehpivot/in/reeds2,con_adj_map,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/con_adj_style.csv,/postprocessing/bokehpivot/in/reeds2,con_adj_style,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/cost_cat_map.csv,/postprocessing/bokehpivot/in/reeds2,cost_cat_map,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/cost_cat_style.csv,/postprocessing/bokehpivot/in/reeds2,cost_cat_style,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/ctt_map.csv,/postprocessing/bokehpivot/in/reeds2,ctt_map,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/ctt_style.csv,/postprocessing/bokehpivot/in/reeds2,ctt_style,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/hours.csv,/postprocessing/bokehpivot/in/reeds2,hours,.csv,Hours for each of the 17 timeslices,,,,, -/postprocessing/bokehpivot/in/reeds2/m_bar_width.csv,/postprocessing/bokehpivot/in/reeds2,m_bar_width,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/m_map.csv,/postprocessing/bokehpivot/in/reeds2,m_map,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/m_style.csv,/postprocessing/bokehpivot/in/reeds2,m_style,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/process_style.csv,/postprocessing/bokehpivot/in/reeds2,process_style,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/tech_ctt_wst.csv,/postprocessing/bokehpivot/in/reeds2,tech_ctt_wst,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/tech_map.csv,/postprocessing/bokehpivot/in/reeds2,tech_map,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/tech_style.csv,/postprocessing/bokehpivot/in/reeds2,tech_style,.csv,Custom colors for each technology used by bokehpivot,,,,, -/postprocessing/bokehpivot/in/reeds2/trtype_map.csv,/postprocessing/bokehpivot/in/reeds2,trtype_map,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/trtype_style.csv,/postprocessing/bokehpivot/in/reeds2,trtype_style,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/wst_map.csv,/postprocessing/bokehpivot/in/reeds2,wst_map,.csv,,,,,, -/postprocessing/bokehpivot/in/reeds2/wst_style.csv,/postprocessing/bokehpivot/in/reeds2,wst_style,.csv,,,,,, -/postprocessing/bokehpivot/in/state_code.csv,/postprocessing/bokehpivot/in,state_code,.csv,Abbreviation and code for each state,,,,, -/postprocessing/bokehpivot/reeds_scenarios.csv,/postprocessing/bokehpivot,reeds_scenarios,.csv,"Example data for ReEDS scenarios, each scenario with a custom style ",,,,, -/postprocessing/combine_runs/combinefiles.csv,/postprocessing/combine_runs,combinefiles,.csv,,,,,, -/postprocessing/combine_runs/runlist.csv,/postprocessing/combine_runs,runlist,.csv,,,,,, -/postprocessing/example.csv,/postprocessing,example,.csv,,,,,, -/postprocessing/land_use/inputs/federal_land_categories.csv,/postprocessing/land_use/inputs,federal_land_categories,.csv,,,,,, -/postprocessing/land_use/inputs/field_definitions.csv,/postprocessing/land_use/inputs,field_definitions,.csv,,,,,, -/postprocessing/land_use/inputs/nlcd_categories.csv,/postprocessing/land_use/inputs,nlcd_categories,.csv,,,,,, -/postprocessing/land_use/inputs/nlcd_combined_categories.csv,/postprocessing/land_use/inputs,nlcd_combined_categories,.csv,,,,,, -/postprocessing/land_use/inputs/usgs_categories.csv,/postprocessing/land_use/inputs,usgs_categories,.csv,,,,,, -/postprocessing/land_use/inputs/usgs_combined_categories.csv,/postprocessing/land_use/inputs,usgs_combined_categories,.csv,,,,,, -/postprocessing/plots/scghg_annual.csv,/postprocessing/plots,scghg_annual,.csv,,,,,, -/postprocessing/plots/transmission-interface-coords.csv,/postprocessing/plots,transmission-interface-coords,.csv,,,,,, -/postprocessing/retail_rate_module/calc_historical_capex/existing_transmission_cost_bystate_USD2024.csv,/postprocessing/retail_rate_module/calc_historical_capex,existing_transmission_cost_bystate_USD2024,.csv,,,,,, -/postprocessing/retail_rate_module/capital_financing_assumptions.csv,/postprocessing/retail_rate_module,capital_financing_assumptions,.csv,,,,,, -/postprocessing/retail_rate_module/df_f861_contiguous.csv,/postprocessing/retail_rate_module,df_f861_contiguous,.csv,,,,,, -/postprocessing/retail_rate_module/df_f861_state.csv,/postprocessing/retail_rate_module,df_f861_state,.csv,,,,,, -/postprocessing/retail_rate_module/inputs.csv,/postprocessing/retail_rate_module,inputs,.csv,,,,,, -/postprocessing/retail_rate_module/inputs/Electric O & M Expenses-IOU-1993-2019.csv,/postprocessing/retail_rate_module/inputs,Electric O & M Expenses-IOU-1993-2019,.csv,values taken from FERC Form 1 -- see https://docs.nrel.gov/docs/fy22osti/78224.pdf sections 2.2.2 and 2.2.3,,,,, -/postprocessing/retail_rate_module/inputs/Electric Operating Revenues-IOU-1993-2019.csv,/postprocessing/retail_rate_module/inputs,Electric Operating Revenues-IOU-1993-2019,.csv,values taken from FERC Form 1 -- see https://docs.nrel.gov/docs/fy22osti/78224.pdf sections 2.2.2 and 2.2.3,,,,, -/postprocessing/retail_rate_module/inputs/Electric Plant in Service-IOU-1993-2019.csv,/postprocessing/retail_rate_module/inputs,Electric Plant in Service-IOU-1993-2019,.csv,values taken from FERC Form 1 -- see https://docs.nrel.gov/docs/fy22osti/78224.pdf sections 2.2.2 and 2.2.3,,,,, -/postprocessing/retail_rate_module/inputs/f861_cust_counts.csv,/postprocessing/retail_rate_module/inputs,f861_cust_counts,.csv,,,,,, -/postprocessing/retail_rate_module/inputs/overwrite-utility-energy_sales.csv,/postprocessing/retail_rate_module/inputs,overwrite-utility-energy_sales,.csv,,,,,, -/postprocessing/retail_rate_module/inputs/state-meanbiaserror_rate-aggregation.csv,/postprocessing/retail_rate_module/inputs,state-meanbiaserror_rate-aggregation,.csv,,,,,, -/postprocessing/retail_rate_module/inputs/Table_9.8_Average_Retail_Prices_of_Electricity.xlsx,/postprocessing/retail_rate_module/inputs,Table_9.8_Average_Retail_Prices_of_Electricity,.xlsx,Historical EIA861 rates (annual and monthly),,,,, -/postprocessing/retail_rate_module/inputs_default.csv,/postprocessing/retail_rate_module,inputs_default,.csv,,,,,, -/postprocessing/retail_rate_module/load_by_state_eia.csv,/postprocessing/retail_rate_module,load_by_state_eia,.csv,End use load by state since 1960,,,,, -/postprocessing/retail_rate_module/map_i_to_tech.csv,/postprocessing/retail_rate_module,map_i_to_tech,.csv,Maps i to tech with custom coloring for each,,,,, -/postprocessing/reValue/scenarios.csv,/postprocessing/reValue,scenarios,.csv,,,,,, -/postprocessing/tableau/tables_to_aggregate.csv,/postprocessing/tableau,tables_to_aggregate,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/batt_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,batt_plant_char_format,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/conv_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,conv_plant_char_format,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/csp_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,csp_plant_char_format,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/geo_fom_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,geo_fom_plant_char_format,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/h2-combustion_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,h2-combustion_plant_char_format,.csv,Plant characteristics for which the H2-CC and CT ATB estimates are made using Gas-CC and CT data in preprocessing,,,,, -/preprocessing/atb_updates_processing/input_files/ofs-wind_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,ofs-wind_plant_char_format,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/ofs-wind_rsc_mult_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,ofs-wind_rsc_mult_plant_char_format,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/ons-wind_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,ons-wind_plant_char_format,.csv,,,,,, -/preprocessing/atb_updates_processing/input_files/upv_plant_char_format.csv,/preprocessing/atb_updates_processing/input_files,upv_plant_char_format,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/cases_reeds2pras.csv,/reeds2pras/test/reeds_cases/test,cases_reeds2pras,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/hydcapadj.csv,/reeds2pras/test/reeds_cases/test/inputs_case,hydcapadj,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/hydcf.csv,/reeds2pras/test/reeds_cases/test/inputs_case,hydcf,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/mttr.csv,/reeds2pras/test/reeds_cases/test/inputs_case,mttr,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_hourly.h5,/reeds2pras/test/reeds_cases/test/inputs_case,outage_forced_hourly,.h5,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_static.csv,/reeds2pras/test/reeds_cases/test/inputs_case,outage_forced_static,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/outage_scheduled_hourly.h5,/reeds2pras/test/reeds_cases/test/inputs_case,outage_scheduled_hourly,.h5,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/resources.csv,/reeds2pras/test/reeds_cases/test/inputs_case,resources,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/tech-subset-table.csv,/reeds2pras/test/reeds_cases/test/inputs_case,tech-subset-table,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/unitdata.csv,/reeds2pras/test/reeds_cases/test/inputs_case,unitdata,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/inputs_case/unitsize.csv,/reeds2pras/test/reeds_cases/test/inputs_case,unitsize,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/meta.csv,/reeds2pras/test/reeds_cases/test,meta,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/cap_converter_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,cap_converter_2035,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/charge_eff_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,charge_eff_2035,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/discharge_eff_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,discharge_eff_2035,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/energy_cap_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,energy_cap_2035,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_cap_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,max_cap_2035,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_unitsize_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,max_unitsize_2035,.csv,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_load_2035.h5,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,pras_load_2035,.h5,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_vre_gen_2035.h5,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,pras_vre_gen_2035,.h5,,,,,, -/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/tran_cap_2035.csv,/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data,tran_cap_2035,.csv,,,,,, -/ReEDS_Augur/augur_switches.csv,/ReEDS_Augur,augur_switches,.csv,,,,,, -/runfiles.csv,/,runfiles,.csv,Contains the locations of input data that is copied from the repository into the runs folder for each respective case.,,,,, -/sources.csv,/,sources,.csv,"CSV file containing a list of all input files (csv, h5, csv.gz)",,,,, -/tests/data/county/csp.h5,/tests/data/county,csp,.h5,Subset of county-level data for the github runner county test,,,,, -/tests/data/county/distpv.h5,/tests/data/county,distpv,.h5,Subset of county-level data for the github runner county test,,,,, -/tests/data/county/upv.h5,/tests/data/county,upv,.h5,Subset of county-level data for the github runner county test,,,,, -/tests/data/county/wind-ofs.h5,/tests/data/county,wind-ofs,.h5,Subset of county-level data for the github runner county test,,,,, -/tests/data/county/wind-ons.h5,/tests/data/county,wind-ons,.h5,Subset of county-level data for the github runner county test,,,,, diff --git a/sources_documentation.md b/sources_documentation.md deleted file mode 100644 index 2604c5a5..00000000 --- a/sources_documentation.md +++ /dev/null @@ -1,4008 +0,0 @@ -## Table of Contents - - - - [hourlize](#hourlize) - - [inputs](#hourlize-inputs) - - [load](#hourlize-inputs-load) - - [resource](#hourlize-inputs-resource) - - [tests](#hourlize-tests) - - [data](#hourlize-tests-data) - - [r2r_expanded](#hourlize-tests-data-r2r-expanded) - - [r2r_from_config](#hourlize-tests-data-r2r-from-config) - - [r2r_integration](#hourlize-tests-data-r2r-integration) - - [r2r_integration_geothermal](#hourlize-tests-data-r2r-integration-geothermal) - - [inputs](#inputs) - - [canada_imports](#inputs-canada-imports) - - [capacity_exogenous](#inputs-capacity-exogenous) - - [climate](#inputs-climate) - - [GFDL-ESM2M_RCP4p5_WM](#inputs-climate-gfdl-esm2m-rcp4p5-wm) - - [HadGEM2-ES_RCP2p6](#inputs-climate-hadgem2-es-rcp2p6) - - [HadGEM2-ES_rcp45_AT](#inputs-climate-hadgem2-es-rcp45-at) - - [HadGEM2-ES_RCP4p5](#inputs-climate-hadgem2-es-rcp4p5) - - [HadGEM2-ES_rcp85_AT](#inputs-climate-hadgem2-es-rcp85-at) - - [HadGEM2-ES_RCP8p5](#inputs-climate-hadgem2-es-rcp8p5) - - [IPSL-CM5A-LR_RCP8p5_WM](#inputs-climate-ipsl-cm5a-lr-rcp8p5-wm) - - [consume](#inputs-consume) - - [ctus](#inputs-ctus) - - [degradation](#inputs-degradation) - - [demand_response](#inputs-demand-response) - - [dgen_model_inputs](#inputs-dgen-model-inputs) - - [stscen2023_electrification](#inputs-dgen-model-inputs-stscen2023-electrification) - - [stscen2023_highng](#inputs-dgen-model-inputs-stscen2023-highng) - - [stscen2023_highre](#inputs-dgen-model-inputs-stscen2023-highre) - - [stscen2023_lowng](#inputs-dgen-model-inputs-stscen2023-lowng) - - [stscen2023_lowre](#inputs-dgen-model-inputs-stscen2023-lowre) - - [stscen2023_mid_case](#inputs-dgen-model-inputs-stscen2023-mid-case) - - [stscen2023_mid_case_95_by_2035](#inputs-dgen-model-inputs-stscen2023-mid-case-95-by-2035) - - [stscen2023_mid_case_95_by_2050](#inputs-dgen-model-inputs-stscen2023-mid-case-95-by-2050) - - [stscen2023_taxcredit_extended2050](#inputs-dgen-model-inputs-stscen2023-taxcredit-extended2050) - - [disaggregation](#inputs-disaggregation) - - [emission_constraints](#inputs-emission-constraints) - - [financials](#inputs-financials) - - [fuelprices](#inputs-fuelprices) - - [geothermal](#inputs-geothermal) - - [growth_constraints](#inputs-growth-constraints) - - [hydro](#inputs-hydro) - - [load](#inputs-load) - - [national_generation](#inputs-national-generation) - - [outages](#inputs-outages) - - [plant_characteristics](#inputs-plant-characteristics) - - [profiles_cf](#inputs-profiles-cf) - - [profiles_demand](#inputs-profiles-demand) - - [remote](#inputs-remote) - - [reserves](#inputs-reserves) - - [sets](#inputs-sets) - - [shapefiles](#inputs-shapefiles) - - [state_policies](#inputs-state-policies) - - [storage](#inputs-storage) - - [supply_curve](#inputs-supply-curve) - - [techs](#inputs-techs) - - [temporal](#inputs-temporal) - - [transmission](#inputs-transmission) - - [upgrades](#inputs-upgrades) - - [userinput](#inputs-userinput) - - [valuestreams](#inputs-valuestreams) - - [waterclimate](#inputs-waterclimate) - - [postprocessing](#postprocessing) - - [air_quality](#postprocessing-air-quality) - - [rcm_data](#postprocessing-air-quality-rcm-data) - - [bokehpivot](#postprocessing-bokehpivot) - - [in](#postprocessing-bokehpivot-in) - - [reeds2](#postprocessing-bokehpivot-in-reeds2) - - [combine_runs](#postprocessing-combine-runs) - - [land_use](#postprocessing-land-use) - - [inputs](#postprocessing-land-use-inputs) - - [plots](#postprocessing-plots) - - [retail_rate_module](#postprocessing-retail-rate-module) - - [calc_historical_capex](#postprocessing-retail-rate-module-calc-historical-capex) - - [inputs](#postprocessing-retail-rate-module-inputs) - - [reValue](#postprocessing-revalue) - - [tableau](#postprocessing-tableau) - - [preprocessing](#preprocessing) - - [atb_updates_processing](#preprocessing-atb-updates-processing) - - [input_files](#preprocessing-atb-updates-processing-input-files) - - [reeds2pras](#reeds2pras) - - [test](#reeds2pras-test) - - [reeds_cases](#reeds2pras-test-reeds-cases) - - [test](#reeds2pras-test-reeds-cases-test) - - [ReEDS_Augur](#reeds-augur) - - [tests](#tests) - - [data](#tests-data) - - [county](#tests-data-county) - - -## Input Files -- [cases.csv](/cases.csv) - - **File Type:** Switches file - - **Description:** Contains the configuration settings for the ReEDS run(s). - - **Dollar year:** 2004 - - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv](https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv) ---- - -- [cases_county.csv](/cases_county.csv) ---- - -- [cases_examples.csv](/cases_examples.csv) ---- - -- [cases_small.csv](/cases_small.csv) - - **Description:** Contains settings to run ReEDS at a smaller scale to test operability of the ReEDS model. Turns off several technologies and reduces the model size to significantly improve solve times. ---- - -- [cases_standardscenarios.csv](/cases_standardscenarios.csv) - - **File Type:** StdScen Cases file - - **Description:** Contains the configuration settings for the Standard Scenarios ReEDS runs. ---- - -- [cases_test.csv](/cases_test.csv) - - **Description:** Contains the configuration settings for doing test runs including the default Pacific census division test case. ---- - -- [e_report_params.csv](/e_report_params.csv) - - **Description:** Contains a parameter list used in the model along with descriptions of what they are and units used. ---- - -- [runfiles.csv](/runfiles.csv) - - **Description:** Contains the locations of input data that is copied from the repository into the runs folder for each respective case. ---- - -- [sources.csv](/sources.csv) - - **Description:** CSV file containing a list of all input files (csv, h5, csv.gz) ---- - - - -### hourlize - - - -#### hourlize/inputs - - - -##### hourlize/inputs/load - - - [dummy_agg_op_datacenters.csv](/hourlize/inputs/load/dummy_agg_op_datacenters.csv) ---- - - - [legacy_ba_state_map.csv](/hourlize/inputs/load/legacy_ba_state_map.csv) ---- - - - [legacy_ba_timezone.csv](/hourlize/inputs/load/legacy_ba_timezone.csv) ---- - - - -##### hourlize/inputs/resource - - - [egs_resource_classes.csv](/hourlize/inputs/resource/egs_resource_classes.csv) ---- - - - [geohydro_resource_classes.csv](/hourlize/inputs/resource/geohydro_resource_classes.csv) ---- - - - [offshore_zone_names.csv](/hourlize/inputs/resource/offshore_zone_names.csv) ---- - - - [rev_sc_columns.csv](/hourlize/inputs/resource/rev_sc_columns.csv) ---- - - - [state_abbrev.csv](/hourlize/inputs/resource/state_abbrev.csv) - - **Description:** Contains state names and codesfor the US. ---- - - - [upv_resource_classes.csv](/hourlize/inputs/resource/upv_resource_classes.csv) - - **Description:** Contains information related to UPV class segregation based on mean irradiance levels. ---- - - - [wind-ofs_resource_classes.csv](/hourlize/inputs/resource/wind-ofs_resource_classes.csv) - - **File Type:** supply curve input - - **Description:** Contains information related to Offshore wind class segregation and turbine type (fixed vs floating) based on water depth and site lcoe - - **Indices:** n/a ---- - - - [wind-ons_resource_classes.csv](/hourlize/inputs/resource/wind-ons_resource_classes.csv) - - **Description:** Contains information related to Onshore wind class segregation based on mean wind speeds. ---- - - - -#### hourlize/tests - - - -##### hourlize/tests/data - - - -###### hourlize/tests/data/r2r_expanded - - - -###### hourlize/tests/data/r2r_expanded/upv_case_1 - - - -###### hourlize/tests/data/r2r_expanded/upv_case_1/expected_results - - - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/expected_results/df_sc_out_upv_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_1/reeds - - - -###### hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case - - - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/hierarchy_original.csv) ---- - - - [maxage.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/maxage.csv) ---- - - - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/rev_paths.csv) ---- - - - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/site_bin_map.csv) ---- - - - [switches.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/inputs_case/switches.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs - - - [cap.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap.csv) ---- - - - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_exog.csv) ---- - - - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_bin_out.csv) ---- - - - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/cap_new_ivrt_refurb.csv) ---- - - - [systemcost.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/reeds/outputs/systemcost.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves - - - [upv_supply_curve_raw_unpacked.csv](/hourlize/tests/data/r2r_expanded/upv_case_1/supply_curves/upv_supply_curve_raw_unpacked.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_2 - - - -###### hourlize/tests/data/r2r_expanded/upv_case_2/expected_results - - - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/expected_results/df_sc_out_upv_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_2/reeds - - - -###### hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case - - - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/hierarchy_original.csv) ---- - - - [maxage.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/maxage.csv) ---- - - - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/rev_paths.csv) ---- - - - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/site_bin_map.csv) ---- - - - [switches.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/inputs_case/switches.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs - - - [cap.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap.csv) ---- - - - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_exog.csv) ---- - - - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_bin_out.csv) ---- - - - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/cap_new_ivrt_refurb.csv) ---- - - - [systemcost.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/reeds/outputs/systemcost.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves - - - [upv_supply_curve_raw_unpacked.csv](/hourlize/tests/data/r2r_expanded/upv_case_2/supply_curves/upv_supply_curve_raw_unpacked.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_3 - - - -###### hourlize/tests/data/r2r_expanded/upv_case_3/expected_results - - - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/expected_results/df_sc_out_upv_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_3/reeds - - - -###### hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case - - - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/hierarchy_original.csv) ---- - - - [maxage.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/maxage.csv) ---- - - - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/rev_paths.csv) ---- - - - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/site_bin_map.csv) ---- - - - [switches.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/inputs_case/switches.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs - - - [cap.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap.csv) ---- - - - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_exog.csv) ---- - - - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_bin_out.csv) ---- - - - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/cap_new_ivrt_refurb.csv) ---- - - - [systemcost.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/reeds/outputs/systemcost.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves - - - [upv_supply_curve_raw_unpacked.csv](/hourlize/tests/data/r2r_expanded/upv_case_3/supply_curves/upv_supply_curve_raw_unpacked.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/wind-ons_case_1 - - - -###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results - - - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/expected_results/df_sc_out_wind-ons_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds - - - -###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case - - - [hierarchy_original.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/hierarchy_original.csv) ---- - - - [maxage.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/maxage.csv) ---- - - - [rev_paths.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/rev_paths.csv) ---- - - - [site_bin_map.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/site_bin_map.csv) ---- - - - [switches.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/switches.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata - - - [rev_supply_curves.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/inputs_case/supplycurve_metadata/rev_supply_curves.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs - - - [cap.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap.csv) ---- - - - [cap_exog.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_exog.csv) ---- - - - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_bin_out.csv) ---- - - - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/cap_new_ivrt_refurb.csv) ---- - - - [systemcost.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/reeds/outputs/systemcost.csv) ---- - - - -###### hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves - - - [wind-ons_supply_curve_raw.csv](/hourlize/tests/data/r2r_expanded/wind-ons_case_1/supply_curves/wind-ons_supply_curve_raw.csv) ---- - - - -###### hourlize/tests/data/r2r_from_config - - - -###### hourlize/tests/data/r2r_from_config/expected_results - - - -###### hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs - - - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_upv_reduced.csv) ---- - - - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ofs_reduced.csv) ---- - - - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/multiple_priority_inputs/df_sc_out_wind-ons_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint - - - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_upv_reduced.csv) ---- - - - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ofs_reduced.csv) ---- - - - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/no_bin_constraint/df_sc_out_wind-ons_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_from_config/expected_results/priority_inputs - - - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_upv_reduced.csv) ---- - - - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ofs_reduced.csv) ---- - - - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_from_config/expected_results/priority_inputs/df_sc_out_wind-ons_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_integration - - - -###### hourlize/tests/data/r2r_integration/expected_results - - - [df_sc_out_upv.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv.csv) ---- - - - [df_sc_out_upv_reduced.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced.csv) ---- - - - [df_sc_out_upv_reduced_simul_fill.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_upv_reduced_simul_fill.csv) ---- - - - [df_sc_out_wind-ofs.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs.csv) ---- - - - [df_sc_out_wind-ofs_reduced.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ofs_reduced.csv) ---- - - - [df_sc_out_wind-ons.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons.csv) ---- - - - [df_sc_out_wind-ons_reduced.csv](/hourlize/tests/data/r2r_integration/expected_results/df_sc_out_wind-ons_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_integration/reeds - - - -###### hourlize/tests/data/r2r_integration/reeds/inputs_case - - - [hierarchy_original.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/hierarchy_original.csv) ---- - - - [maxage.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/maxage.csv) ---- - - - [rev_paths.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/rev_paths.csv) ---- - - - [site_bin_map.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/site_bin_map.csv) ---- - - - [switches.csv](/hourlize/tests/data/r2r_integration/reeds/inputs_case/switches.csv) ---- - - - -###### hourlize/tests/data/r2r_integration/reeds/outputs - - - [cap.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap.csv) ---- - - - [cap_exog.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap_exog.csv) ---- - - - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_bin_out.csv) ---- - - - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/cap_new_ivrt_refurb.csv) ---- - - - [systemcost.csv](/hourlize/tests/data/r2r_integration/reeds/outputs/systemcost.csv) ---- - - - -###### hourlize/tests/data/r2r_integration/supply_curves - - - -###### hourlize/tests/data/r2r_integration/supply_curves/upv_reference - - - -###### hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results - - - [upv_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration/supply_curves/upv_reference/results/upv_supply_curve_raw.csv) ---- - - - -###### hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate - - - -###### hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results - - - [wind-ofs_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration/supply_curves/wind-ofs_0_open_moderate/results/wind-ofs_supply_curve_raw.csv) ---- - - - -###### hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference - - - -###### hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results - - - [wind-ons_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration/supply_curves/wind-ons_reference/results/wind-ons_supply_curve_raw.csv) ---- - - - -###### hourlize/tests/data/r2r_integration_geothermal - - - -###### hourlize/tests/data/r2r_integration_geothermal/expected_results - - - [df_sc_out_egs_allkm.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm.csv) ---- - - - [df_sc_out_egs_allkm_reduced.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_egs_allkm_reduced.csv) ---- - - - [df_sc_out_geohydro_allkm.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm.csv) ---- - - - [df_sc_out_geohydro_allkm_reduced.csv](/hourlize/tests/data/r2r_integration_geothermal/expected_results/df_sc_out_geohydro_allkm_reduced.csv) ---- - - - -###### hourlize/tests/data/r2r_integration_geothermal/reeds - - - -###### hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case - - - [hierarchy_original.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/hierarchy_original.csv) ---- - - - [maxage.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/maxage.csv) ---- - - - [rev_paths.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/rev_paths.csv) ---- - - - [site_bin_map.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/site_bin_map.csv) ---- - - - [switches.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/inputs_case/switches.csv) ---- - - - -###### hourlize/tests/data/r2r_integration_geothermal/reeds/outputs - - - [cap.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap.csv) ---- - - - [cap_exog.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_exog.csv) ---- - - - [cap_new_bin_out.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_bin_out.csv) ---- - - - [cap_new_ivrt_refurb.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/cap_new_ivrt_refurb.csv) ---- - - - [systemcost.csv](/hourlize/tests/data/r2r_integration_geothermal/reeds/outputs/systemcost.csv) ---- - - - -###### hourlize/tests/data/r2r_integration_geothermal/supply_curves - - - [egs_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration_geothermal/supply_curves/egs_supply_curve_raw.csv) ---- - - - [geohydro_supply_curve_raw.csv](/hourlize/tests/data/r2r_integration_geothermal/supply_curves/geohydro_supply_curve_raw.csv) ---- - - - -### inputs - - - [county2zone.csv](/inputs/county2zone.csv) ---- - - - [hierarchy.csv](/inputs/hierarchy.csv) ---- - - - [hierarchy_agg125.csv](/inputs/hierarchy_agg125.csv) ---- - - - [hierarchy_agg54.csv](/inputs/hierarchy_agg54.csv) ---- - - - [hierarchy_agg69.csv](/inputs/hierarchy_agg69.csv) ---- - - - [hierarchy_offshore.csv](/inputs/zones/hierarchy_offshore.csv) ---- - - - [remote_files.csv](/inputs/remote_files.csv) ---- - - - [scalars.csv](/inputs/scalars.csv) ---- - - - [tech-subset-table.csv](/inputs/tech-subset-table.csv) - - **Description:** Maps all technologies to specific subsets of technologies ---- - - - -#### inputs/canada_imports - - - [can_exports.csv](/inputs/canada_imports/can_exports.csv) - - **File Type:** Input - - **Description:** Annual exports to Canada by BA - - **Indices:** r,t - - **Units:** MWh - ---- - - - [can_exports_szn_frac.csv](/inputs/canada_imports/can_exports_szn_frac.csv) - - **File Type:** Input - - **Description:** Fraction of annual exports to Canada by season - - **Indices:** N/A - - **Units:** rate (unitless) - ---- - - - [can_imports.csv](/inputs/canada_imports/can_imports.csv) - - **File Type:** Input - - **Description:** Annual imports from Canada by BA - - **Indices:** r,t - - **Units:** MWh - ---- - - - [can_imports_quarter_frac.csv](/inputs/canada_imports/can_imports_quarter_frac.csv) - - **File Type:** Input - - **Description:** Fraction of annual imports from Canada by season - - **Indices:** N/A - - **Units:** rate (unitless) - ---- - - - -#### inputs/capacity_exogenous - - - [cappayments.csv](/inputs/capacity_exogenous/cappayments.csv) ---- - - - [cappayments_ba.csv](/inputs/capacity_exogenous/cappayments_ba.csv) ---- - - - [demonstration_plants.csv](/inputs/capacity_exogenous/demonstration_plants.csv) - - **File Type:** Prescribed capacity - - **Description:** Nuclear-smr demonstration plants; active when GSw_NuclearDemo=1 - - **Indices:** t,r,i,coolingwatertech,ctt,wst,value - - **Citation:** See 'notes' column in the file and https://www.energy.gov/oced/advanced-reactor-demonstration-projects-0 - - **Units:** MW - ---- - - - [exog_cap_geohydro_allkm_reference.csv](/inputs/capacity_exogenous/exog_cap_geohydro_allkm_reference.csv) ---- - - - [exog_cap_geohydro_reference.csv](/inputs/capacity_exogenous/exog_cap_geohydro_reference.csv) ---- - - - [exog_cap_upv_limited.csv](/inputs/capacity_exogenous/exog_cap_upv_limited.csv) ---- - - - [exog_cap_upv_open.csv](/inputs/capacity_exogenous/exog_cap_upv_open.csv) ---- - - - [exog_cap_upv_reference.csv](/inputs/capacity_exogenous/exog_cap_upv_reference.csv) ---- - - - [exog_cap_wind-ons_limited.csv](/inputs/capacity_exogenous/exog_cap_wind-ons_limited.csv) ---- - - - [exog_cap_wind-ons_open.csv](/inputs/capacity_exogenous/exog_cap_wind-ons_open.csv) ---- - - - [exog_cap_wind-ons_reference.csv](/inputs/capacity_exogenous/exog_cap_wind-ons_reference.csv) ---- - - - [interconnection_queues.csv](/inputs/capacity_exogenous/interconnection_queues.csv) ---- - - - [prescribed_builds_wind-ofs_meshed_limited.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_limited.csv) ---- - - - [prescribed_builds_wind-ofs_meshed_open.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_open.csv) ---- - - - [prescribed_builds_wind-ofs_meshed_reference.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_meshed_reference.csv) ---- - - - [prescribed_builds_wind-ofs_radial_limited.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_limited.csv) ---- - - - [prescribed_builds_wind-ofs_radial_open.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_open.csv) ---- - - - [prescribed_builds_wind-ofs_radial_reference.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ofs_radial_reference.csv) ---- - - - [prescribed_builds_wind-ons_limited.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ons_limited.csv) ---- - - - [prescribed_builds_wind-ons_open.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ons_open.csv) ---- - - - [prescribed_builds_wind-ons_reference.csv](/inputs/capacity_exogenous/prescribed_builds_wind-ons_reference.csv) ---- - - - [ReEDS_generator_database_final_EIA-NEMS.csv](/inputs/capacity_exogenous/ReEDS_generator_database_final_EIA-NEMS.csv) - - **File Type:** Input - - **Description:** EIA-NEMS database of existing generators ---- - - - -#### inputs/climate - - - [climate_heuristics_finalyear.csv](/inputs/climate/climate_heuristics_finalyear.csv) ---- - - - [climate_heuristics_yearfrac.csv](/inputs/climate/climate_heuristics_yearfrac.csv) ---- - - - - -##### inputs/climate/GFDL-ESM2M_RCP4p5_WM - - - [HDDCDD.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/HDDCDD.csv) ---- - - - [hydadjann.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjann.csv) - - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario - - **Indices:** r,t - - **Units:** multipliers (unitless) - ---- - - - [hydadjsea.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/hydadjsea.csv) - - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the GFDL-ESM2M_RCP4p5_WM climate scenario - - **Indices:** r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMult.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMult.csv) - - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMultAnn.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterMultAnn.csv) - - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the GFDL-ESM2M_RCP4p5_WM climate scenario - - **Indices:** wst,r,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterSeaAnnDistr.csv](/inputs/climate/GFDL-ESM2M_RCP4p5_WM/UnappWaterSeaAnnDistr.csv) - - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the GFDL-ESM2M_RCP4p5_WM climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - -##### inputs/climate/HadGEM2-ES_RCP2p6 - - - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_RCP2p6/HDDCDD.csv) ---- - - - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMult.csv) - - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterMultAnn.csv) - - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP2p6 climate scenario - - **Indices:** wst,r,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_RCP2p6/UnappWaterSeaAnnDistr.csv) - - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP2p6 climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - -##### inputs/climate/HadGEM2-ES_rcp45_AT - - - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/HDDCDD.csv) ---- - - - [hydadjann.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjann.csv) - - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario - - **Indices:** r,t - - **Units:** multipliers (unitless) - ---- - - - [hydadjsea.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/hydadjsea.csv) - - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp45_AT climate scenario - - **Indices:** r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMult.csv) - - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterMultAnn.csv) - - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp45_AT climate scenario - - **Indices:** wst,r,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_rcp45_AT/UnappWaterSeaAnnDistr.csv) - - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp45_AT climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - -##### inputs/climate/HadGEM2-ES_RCP4p5 - - - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_RCP4p5/HDDCDD.csv) ---- - - - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMult.csv) - - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterMultAnn.csv) - - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP4p5 climate scenario - - **Indices:** wst,r,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_RCP4p5/UnappWaterSeaAnnDistr.csv) - - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP4p5 climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - -##### inputs/climate/HadGEM2-ES_rcp85_AT - - - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/HDDCDD.csv) ---- - - - [hydadjann.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjann.csv) - - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario - - **Indices:** r,t - - **Units:** multipliers (unitless) - ---- - - - [hydadjsea.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/hydadjsea.csv) - - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the HadGEM2-ES_rcp85_AT climate scenario - - **Indices:** r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMult.csv) - - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterMultAnn.csv) - - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_rcp85_AT climate scenario - - **Indices:** wst,r,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_rcp85_AT/UnappWaterSeaAnnDistr.csv) - - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_rcp85_AT climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - -##### inputs/climate/HadGEM2-ES_RCP8p5 - - - [HDDCDD.csv](/inputs/climate/HadGEM2-ES_RCP8p5/HDDCDD.csv) ---- - - - [UnappWaterMult.csv](/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMult.csv) - - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMultAnn.csv](/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterMultAnn.csv) - - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the HadGEM2-ES_RCP8p5 climate scenario - - **Indices:** wst,r,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterSeaAnnDistr.csv](/inputs/climate/HadGEM2-ES_RCP8p5/UnappWaterSeaAnnDistr.csv) - - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the HadGEM2-ES_RCP8p5 climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - -##### inputs/climate/IPSL-CM5A-LR_RCP8p5_WM - - - [HDDCDD.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/HDDCDD.csv) ---- - - - [hydadjann.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjann.csv) - - **Description:** Climate-impact capacity factor multipliers for annual dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario - - **Indices:** r,t - - **Units:** multipliers (unitless) - ---- - - - [hydadjsea.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/hydadjsea.csv) - - **Description:** Climate-impact capacity factor multipliers for annual/monthly non-dispatchable hydropower for the IPSL-CM5A-LR_RCP8p5_WM climate scenario - - **Indices:** r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMult.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMult.csv) - - **Description:** Climate-impact water availability multipliers for annual/monthly unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterMultAnn.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterMultAnn.csv) - - **Description:** Climate-impact water availability multipliers for annual unappropriated fresh surface water for the IPSL-CM5A-LR_RCP8p5_WM climate scenario - - **Indices:** wst,r,t - - **Units:** multipliers (unitless) - ---- - - - [UnappWaterSeaAnnDistr.csv](/inputs/climate/IPSL-CM5A-LR_RCP8p5_WM/UnappWaterSeaAnnDistr.csv) - - **Description:** Fractional distribution of unappropriated fresh surface water for each month of a given year for the IPSL-CM5A-LR_RCP8p5_WM climate scenario - - **Indices:** wst,r,month,t - - **Units:** multipliers (unitless) - ---- - - - -#### inputs/consume - - - [consume_char_low.csv](/inputs/consume/consume_char_low.csv) - - **File Type:** Inputs - - **Description:** Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Conservative assumptions. - - **Indices:** i,t - - **Dollar year:** Units vary based on the parameter - see commented text in b_inputs.gms. - - **Citation:** N/A ---- - - - [consume_char_ref.csv](/inputs/consume/consume_char_ref.csv) - - **File Type:** Inputs - - **Description:** Cost (capex, FOM, VOM) and efficiency (gas and electrical) as well as storage and transmission adder (stortran_adder) inputs for various H2 producing technologies, under Reference assumptions. - - **Indices:** i,t - - **Dollar year:** Units vary based on the parameter - see commented text in b_inputs.gms. - - **Citation:** N/A ---- - - - [dac_elec_BVRE_2021_high.csv](/inputs/consume/dac_elec_BVRE_2021_high.csv) - - **File Type:** Inputs - - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions. - - **Indices:** i,t - - **Dollar year:** As specified in inputs/consume/dollaryear - - **Citation:** [https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a](https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a) ---- - - - [dac_elec_BVRE_2021_low.csv](/inputs/consume/dac_elec_BVRE_2021_low.csv) - - **File Type:** Inputs - - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions. - - **Indices:** i,t - - **Dollar year:** As specified in inputs/consume/dollaryear - - **Citation:** [https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a](https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a) ---- - - - [dac_elec_BVRE_2021_mid.csv](/inputs/consume/dac_elec_BVRE_2021_mid.csv) - - **File Type:** Inputs - - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions. - - **Indices:** i,t - - **Dollar year:** As specified in inputs/consume/dollaryear - - **Citation:** [https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a](https://www.netl.doe.gov/energy-analysis/details?id=d5860604-fbc7-44bb-a756-76db47d8b85a) ---- - - - [dac_gas_BVRE_2021_high.csv](/inputs/consume/dac_gas_BVRE_2021_high.csv) - - **File Type:** Inputs - - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using High assumptions. - - **Indices:** i,t - - **Dollar year:** As specified in inputs/consume/dollaryear - - **Citation:** [https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987](https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987) ---- - - - [dac_gas_BVRE_2021_low.csv](/inputs/consume/dac_gas_BVRE_2021_low.csv) - - **File Type:** Inputs - - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Low assumptions. - - **Indices:** i,t - - **Dollar year:** As specified in inputs/consume/dollaryear - - **Citation:** [https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987](https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987) ---- - - - [dac_gas_BVRE_2021_mid.csv](/inputs/consume/dac_gas_BVRE_2021_mid.csv) - - **File Type:** Inputs - - **Description:** DAC costs (capex [$/(metric ton CO2/hr)], FOM [$/(metric ton CO2/hr)/yr], VOM [$/metric ton CO2]) and conversion rate, over time, using Mid assumptions. - - **Indices:** i,t - - **Dollar year:** As specified in inputs/consume/dollaryear - - **Citation:** [https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987](https://netl.doe.gov/energy-analysis/details?id=36385f18-3eaa-4f96-9983-6e2b607f6987) ---- - - - [dollaryear.csv](/inputs/consume/dollaryear.csv) - - **File Type:** Inputs - - **Description:** Dollar year for various Beyond VRE scenarios. - - **Indices:** N/A - - **Dollar year:** Stated in document. - - **Citation:** N/A ---- - - - [h2_demand_county_share.csv](/inputs/consume/h2_demand_county_share.csv) - - **File Type:** Inputs - - **Description:** The fraction of national hydrogen demand in that year that corresponds to each county. Demand estimates come from https://data.openei.org/submissions/5655. 2021 demand shares correspond to the "Reference" scenario with light-duty vehicles / biofuels / methanol demand removed and 2050 shares correspond to the "Low Cost Electrolysis" scenario. - - **Indices:** r,t - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [h2_exogenous_demand.csv](/inputs/consume/h2_exogenous_demand.csv) - - **File Type:** Inputs - - **Description:** Exogenous hydrogen demand by industries other than the power sector per year - - **Indices:** t - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [h2_transport_and_storage_costs.csv](/inputs/consume/h2_transport_and_storage_costs.csv) - - **File Type:** Inputs - - **Description:** Transport and storage costs of hydrogen per year - - **Indices:** t - - **Dollar year:** 2004 - - **Citation:** N/A ---- - - - -#### inputs/ctus - - - [co2_site_char.csv](/inputs/ctus/co2_site_char.csv) - - **Dollar year:** 2018 ---- - - - [cs.csv](/inputs/ctus/cs.csv) ---- - - - -#### inputs/degradation - - - [degradation_annual_default.csv](/inputs/degradation/degradation_annual_default.csv) ---- - - - -#### inputs/demand_response - - - [dr_shed_avail_scalar.csv](/inputs/demand_response/dr_shed_avail_scalar.csv) ---- - - - [dr_shed_capacity_scalar_demo_data_IEF_January_2025.csv](/inputs/demand_response/dr_shed_capacity_scalar_demo_data_IEF_January_2025.csv) ---- - - - [dr_shed_hourly.h5](/inputs/demand_response/dr_shed_hourly.h5) ---- - - - [ev_load_Baseline.h5](/inputs/demand_response/ev_load_Baseline.h5) - - **File Type:** inputs - - **Description:** Baseline electricity load from EV charging by timeslice h and year t - - **Units:** MW - ---- - - - [evmc_rsc_Baseline.csv](/inputs/demand_response/evmc_rsc_Baseline.csv) ---- - - - [evmc_shape_decrease_profile_Baseline.h5](/inputs/demand_response/evmc_shape_decrease_profile_Baseline.h5) ---- - - - [evmc_shape_increase_profile_Baseline.h5](/inputs/demand_response/evmc_shape_increase_profile_Baseline.h5) ---- - - - [evmc_storage_decrease_profile_Baseline.h5](/inputs/demand_response/evmc_storage_decrease_profile_Baseline.h5) ---- - - - [evmc_storage_increase_profile_Baseline.h5](/inputs/demand_response/evmc_storage_increase_profile_Baseline.h5) ---- - - - [evmc_storage_profile_energy_Baseline.h5](/inputs/demand_response/evmc_storage_profile_energy_Baseline.h5) ---- - - - -#### inputs/dgen_model_inputs - - - -##### inputs/dgen_model_inputs/stscen2023_electrification - - - [distpvcap_stscen2023_electrification.csv](/inputs/dgen_model_inputs/stscen2023_electrification/distpvcap_stscen2023_electrification.csv) ---- - - - -##### inputs/dgen_model_inputs/stscen2023_highng - - - [distpvcap_stscen2023_highng.csv](/inputs/dgen_model_inputs/stscen2023_highng/distpvcap_stscen2023_highng.csv) - - **File Type:** distribution PV inputs - - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with high NG (including distpv) costs ---- - - - -##### inputs/dgen_model_inputs/stscen2023_highre - - - [distpvcap_stscen2023_highre.csv](/inputs/dgen_model_inputs/stscen2023_highre/distpvcap_stscen2023_highre.csv) - - **File Type:** distribution PV inputs - - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with high RE (including distpv) costs ---- - - - -##### inputs/dgen_model_inputs/stscen2023_lowng - - - [distpvcap_stscen2023_lowng.csv](/inputs/dgen_model_inputs/stscen2023_lowng/distpvcap_stscen2023_lowng.csv) - - **File Type:** distribution PV inputs - - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with low NG (including distpv) costs ---- - - - -##### inputs/dgen_model_inputs/stscen2023_lowre - - - [distpvcap_stscen2023_lowre.csv](/inputs/dgen_model_inputs/stscen2023_lowre/distpvcap_stscen2023_lowre.csv) - - **File Type:** distribution PV inputs - - **Description:** Setting for distpv scenario capacity - from standard scenarios 2023 with low RE (including distpv) costs ---- - - - -##### inputs/dgen_model_inputs/stscen2023_mid_case - - - [distpvcap_stscen2023_mid_case.csv](/inputs/dgen_model_inputs/stscen2023_mid_case/distpvcap_stscen2023_mid_case.csv) - - **File Type:** distribution PV inputs ---- - - - -##### inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035 - - - [distpvcap_stscen2023_mid_case_95_by_2035.csv](/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2035/distpvcap_stscen2023_mid_case_95_by_2035.csv) - - **File Type:** distribution PV inputs ---- - - - -##### inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050 - - - [distpvcap_stscen2023_mid_case_95_by_2050.csv](/inputs/dgen_model_inputs/stscen2023_mid_case_95_by_2050/distpvcap_stscen2023_mid_case_95_by_2050.csv) - - **File Type:** distribution PV inputs ---- - - - -##### inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050 - - - [distpvcap_stscen2023_taxcredit_extended2050.csv](/inputs/dgen_model_inputs/stscen2023_taxcredit_extended2050/distpvcap_stscen2023_taxcredit_extended2050.csv) - - **File Type:** distribution PV inputs ---- - - - -#### inputs/disaggregation - - - [county_population.csv](/inputs/disaggregation/county_population.csv) - - **Description:** The population of each county, relative values are used as multipliers for downselecting data. Data come from the U.S. Census Bureau 2021 county population estimates (https://www.census.gov/data/tables/time-series/demo/popest/2020s-counties-total.html). - - **Indices:** FIPS ---- - - - [county_state_lpf.csv](/inputs/disaggregation/county_state_lpf.csv) ---- - - - [disagg_hydroexist.csv](/inputs/disaggregation/disagg_hydroexist.csv) - - **Description:** The hydropower capacity fraction of each county within a given ReEDS BA, used as multipliers for downselecting data - - **Indices:** r ---- - - - -#### inputs/emission_constraints - - - [ccs_link.csv](/inputs/emission_constraints/ccs_link.csv) ---- - - - [ccs_link_water.csv](/inputs/emission_constraints/ccs_link_water.csv) ---- - - - [co2_cap.csv](/inputs/emission_constraints/co2_cap.csv) - - **Description:** Annual nationwide carbon cap ---- - - - [co2_tax.csv](/inputs/emission_constraints/co2_tax.csv) - - **Description:** Annual co2 tax ---- - - - [county_co2_share_egrid_2022.csv](/inputs/emission_constraints/county_co2_share_egrid_2022.csv) ---- - - - [csapr_group1_ex.csv](/inputs/emission_constraints/csapr_group1_ex.csv) ---- - - - [csapr_group2_ex.csv](/inputs/emission_constraints/csapr_group2_ex.csv) ---- - - - [csapr_ozone_season.csv](/inputs/emission_constraints/csapr_ozone_season.csv) ---- - - - [emitrate.csv](/inputs/emission_constraints/emitrate.csv) - - **Description:** Emission rates for thermal generators with values from Table 5 of https://docs.nrel.gov/docs/fy25osti/93005.pdf - - **Indices:** i,e ---- - - - [gwp.csv](/inputs/emission_constraints/gwp.csv) ---- - - - [h2_leakage_rate.csv](/inputs/emission_constraints/h2_leakage_rate.csv) ---- - - - [methane_leakage_rate.csv](/inputs/emission_constraints/methane_leakage_rate.csv) ---- - - - [ng_crf_penalty.csv](/inputs/emission_constraints/ng_crf_penalty.csv) - - **File Type:** Inputs - - **Description:** Cost adjustment for NG techs in scenarios with national decarbonization targets - - **Indices:** allt - - **Dollar year:** N/A - - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220](https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220) - - **Units:** rate (unitless) - ---- - - - [rggi_states.csv](/inputs/emission_constraints/rggi_states.csv) - - **Description:** Participating RGGI states - - **Citation:** [https://www.rggi.org/program-overview-and-design/elements](https://www.rggi.org/program-overview-and-design/elements) ---- - - - [rggicon.csv](/inputs/emission_constraints/rggicon.csv) - - **Description:** CO2 caps for RGGI states in metric tons - - **Citation:** [https://www.rggi.org/allowance-tracking/allowance-distribution](https://www.rggi.org/allowance-tracking/allowance-distribution) ---- - - - [state_cap.csv](/inputs/emission_constraints/state_cap.csv) ---- - - - -#### inputs/financials - - - [cap_penalty.csv](/inputs/financials/cap_penalty.csv) ---- - - - [construction_schedules_default.csv](/inputs/financials/construction_schedules_default.csv) ---- - - - [construction_times_default.csv](/inputs/financials/construction_times_default.csv) ---- - - - [currency_incentives.csv](/inputs/financials/currency_incentives.csv) ---- - - - [deflator.csv](/inputs/financials/deflator.csv) - - **Description:** Dollar year deflator to convert values to 2004$ ---- - - - [depreciation_schedules_default.csv](/inputs/financials/depreciation_schedules_default.csv) ---- - - - [energy_communities.csv](/inputs/financials/energy_communities.csv) ---- - - - [financials_hydrogen.csv](/inputs/financials/financials_hydrogen.csv) ---- - - - [financials_sys_ATB2023.csv](/inputs/financials/financials_sys_ATB2023.csv) ---- - - - [financials_sys_ATB2024.csv](/inputs/financials/financials_sys_ATB2024.csv) ---- - - - [financials_tech_ATB2023.csv](/inputs/financials/financials_tech_ATB2023.csv) ---- - - - [financials_tech_ATB2023_CRP20.csv](/inputs/financials/financials_tech_ATB2023_CRP20.csv) ---- - - - [financials_tech_ATB2024.csv](/inputs/financials/financials_tech_ATB2024.csv) ---- - - - [financials_transmission_30ITC_0pen_2022_2031.csv](/inputs/financials/financials_transmission_30ITC_0pen_2022_2031.csv) ---- - - - [financials_transmission_default.csv](/inputs/financials/financials_transmission_default.csv) ---- - - - [incentives_annual.csv](/inputs/financials/incentives_annual.csv) ---- - - - [incentives_biennial.csv](/inputs/financials/incentives_biennial.csv) ---- - - - [incentives_ira.csv](/inputs/financials/incentives_ira.csv) ---- - - - [incentives_ira_45q_45v_extension.csv](/inputs/financials/incentives_ira_45q_45v_extension.csv) ---- - - - [incentives_noira.csv](/inputs/financials/incentives_noira.csv) ---- - - - [incentives_none.csv](/inputs/financials/incentives_none.csv) ---- - - - [incentives_obbba.csv](/inputs/financials/incentives_obbba.csv) ---- - - - [incentives_obbba_conservative.csv](/inputs/financials/incentives_obbba_conservative.csv) ---- - - - [inflation_default.csv](/inputs/financials/inflation_default.csv) - - **Description:** Annual inflation factors from 1914 through 2200; historical values use the avg-avg values from https://www.usinflationcalculator.com/inflation/consumer-price-index-and-annual-percent-changes-from-1913-to-2008/ - - **Indices:** t ---- - - - [nuclear_energy_communities.csv](/inputs/financials/nuclear_energy_communities.csv) - - **Description:** "Counties belonging to metropolitan statistical areas (MSAs) for which at least 0.17 percent of direct employment has been related to nuclear power at any point since 2010. These are determined partly by following the process described in Section 2.6 of https://home.treasury.gov/system/files/8861/EnergyCommunities_Data_Documentation.pdf and substituting in the NAICS code for nuclear electric power generation (221113) and partly by determining counties that belong to MSAs where the number of people employed by national labs engaged in nuclear research and development (PNNL, INL, ORNL, SNL, LLNL, Argonne, and LANL) has been at least 0.17 percent of the MSA's total employment at any point since 2010." ---- - - - [reg_cap_cost_diff_default.csv](/inputs/financials/reg_cap_cost_diff_default.csv) - - **File Type:** parameter - - **Description:** region-specific differences for capital cost of all resources. Add to 1 to produce a multiplier - - **Indices:** i,r ---- - - - [retire_penalty.csv](/inputs/financials/retire_penalty.csv) ---- - - - [supply_chain_adjust.csv](/inputs/financials/supply_chain_adjust.csv) ---- - - - [tc_phaseout_schedule_ira2022.csv](/inputs/financials/tc_phaseout_schedule_ira2022.csv) ---- - - - -#### inputs/fuelprices - - - [alpha_AEO_2023_HOG.csv](/inputs/fuelprices/alpha_AEO_2023_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves - - **Indices:** allt,cendiv - - **Dollar year:** 2004 - - **Citation:** AEO 2023 ---- - - - [alpha_AEO_2023_LOG.csv](/inputs/fuelprices/alpha_AEO_2023_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves - - **Indices:** allt,cendiv - - **Dollar year:** 2004 - - **Citation:** AEO 2023 ---- - - - [alpha_AEO_2023_reference.csv](/inputs/fuelprices/alpha_AEO_2023_reference.csv) - - **File Type:** Input - - **Description:** reference census division alpha values, used in the calculation of natural gas demand curves - - **Indices:** allt,cendiv - - **Dollar year:** 2004 - - **Citation:** AEO 2023 ---- - - - [alpha_AEO_2025_HOG.csv](/inputs/fuelprices/alpha_AEO_2025_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves - - **Indices:** allt,cendiv - - **Dollar year:** 2004 - - **Citation:** AEO 2025 ---- - - - [alpha_AEO_2025_LOG.csv](/inputs/fuelprices/alpha_AEO_2025_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology scenario census division alpha values, used in the calculation of natural gas demand curves - - **Indices:** allt,cendiv - - **Dollar year:** 2004 - - **Citation:** AEO 2025 ---- - - - [alpha_AEO_2025_reference.csv](/inputs/fuelprices/alpha_AEO_2025_reference.csv) - - **File Type:** Input - - **Description:** reference census division alpha values, used in the calculation of natural gas demand curves - - **Indices:** allt,cendiv - - **Dollar year:** 2004 - - **Citation:** AEO 2025 ---- - - - [cd_beta0.csv](/inputs/fuelprices/cd_beta0.csv) - - **File Type:** Input - - **Description:** reference census division beta levels electric sector - - **Indices:** cendiv - - **Dollar year:** 2004 ---- - - - [cd_beta0_allsector.csv](/inputs/fuelprices/cd_beta0_allsector.csv) - - **File Type:** Input - - **Description:** reference census division beta levels all sectors - - **Indices:** cendiv - - **Dollar year:** 2004 ---- - - - [cendivweights.csv](/inputs/fuelprices/cendivweights.csv) - - **Description:** weights to smooth gas prices between census regions to avoid abrupt price changes at the cendiv borders - - **Indices:** r,cendiv ---- - - - [coal_AEO_2023_reference.csv](/inputs/fuelprices/coal_AEO_2023_reference.csv) - - **Description:** reference case census division fuel price of coal - - **Indices:** t,cendiv - - **Dollar year:** 2022 ---- - - - [coal_AEO_2025_reference.csv](/inputs/fuelprices/coal_AEO_2025_reference.csv) - - **Description:** reference case census division fuel price of coal with missing values forward-filled from earlier years - - **Indices:** t,cendiv - - **Dollar year:** 2024 ---- - - - [dollaryear.csv](/inputs/fuelprices/dollaryear.csv) - - **Description:** Dollar year mapping for each fuel price scenario ---- - - - [h2-combustion_10.csv](/inputs/fuelprices/h2-combustion_10.csv) - - **Description:** price of hydrogen for combustion technologies (h2-ct and cc) at $10/MMBtu for all years ---- - - - [h2-combustion_30.csv](/inputs/fuelprices/h2-combustion_30.csv) - - **Description:** price of hydrogen for combustion technologies (h2-ct and cc) at $30/MMBtu for all years ---- - - - [h2-combustion_reference.csv](/inputs/fuelprices/h2-combustion_reference.csv) - - **Description:** price of hydrogen for combustion technologies (h2-ct and cc) at $20/MMBtu for all years ---- - - - [ng_AEO_2023_HOG.csv](/inputs/fuelprices/ng_AEO_2023_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology scenario census division fuel price of natural gas - - **Indices:** cendiv,t - - **Dollar year:** 2004 - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** 2004$/MMBtu - ---- - - - [ng_AEO_2023_LOG.csv](/inputs/fuelprices/ng_AEO_2023_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas - - **Indices:** cendiv,t - - **Dollar year:** 2004 - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** 2004$/MMBtu - ---- - - - [ng_AEO_2023_reference.csv](/inputs/fuelprices/ng_AEO_2023_reference.csv) - - **File Type:** Input - - **Description:** Reference scenario census division fuel price of natural gas - - **Indices:** cendiv,t - - **Dollar year:** 2004 - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** 2004$/MMBtu - ---- - - - [ng_AEO_2025_HOG.csv](/inputs/fuelprices/ng_AEO_2025_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology scenario census division fuel price of natural gas - - **Indices:** cendiv,t - - **Dollar year:** 2004 - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** 2004$/MMBtu - ---- - - - [ng_AEO_2025_LOG.csv](/inputs/fuelprices/ng_AEO_2025_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology scenario census division fuel price of natural gas - - **Indices:** cendiv,t - - **Dollar year:** 2004 - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** 2004$/MMBtu - ---- - - - [ng_AEO_2025_reference.csv](/inputs/fuelprices/ng_AEO_2025_reference.csv) - - **File Type:** Input - - **Description:** Reference scenario census division fuel price of natural gas - - **Indices:** cendiv,t - - **Dollar year:** 2004 - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** 2004$/MMBtu - ---- - - - [ng_demand_AEO_2023_HOG.csv](/inputs/fuelprices/ng_demand_AEO_2023_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_demand_AEO_2023_LOG.csv](/inputs/fuelprices/ng_demand_AEO_2023_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_demand_AEO_2023_reference.csv](/inputs/fuelprices/ng_demand_AEO_2023_reference.csv) - - **File Type:** Input - - **Description:** Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_demand_AEO_2025_HOG.csv](/inputs/fuelprices/ng_demand_AEO_2025_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_demand_AEO_2025_LOG.csv](/inputs/fuelprices/ng_demand_AEO_2025_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_demand_AEO_2025_reference.csv](/inputs/fuelprices/ng_demand_AEO_2025_reference.csv) - - **File Type:** Input - - **Description:** Reference census division natural gas demand for the electric sector, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_tot_demand_AEO_2023_HOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2023_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_tot_demand_AEO_2023_LOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2023_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_tot_demand_AEO_2023_reference.csv](/inputs/fuelprices/ng_tot_demand_AEO_2023_reference.csv) - - **File Type:** Input - - **Description:** Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2023: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_tot_demand_AEO_2025_HOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2025_HOG.csv) - - **File Type:** Input - - **Description:** High Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_tot_demand_AEO_2025_LOG.csv](/inputs/fuelprices/ng_tot_demand_AEO_2025_LOG.csv) - - **File Type:** Input - - **Description:** Low Oil and Gas Resource and Technology census division natural gas demand across all sectors, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [ng_tot_demand_AEO_2025_reference.csv](/inputs/fuelprices/ng_tot_demand_AEO_2025_reference.csv) - - **File Type:** Input - - **Description:** Reference census division natural gas demand across all sectors, used in the calculation of natural gas demand curves - - **Indices:** cendiv,t - - **Citation:** AEO2025: https://www.eia.gov/outlooks/aeo/ - - **Units:** Quads - ---- - - - [uranium_AEO_2023_reference.csv](/inputs/fuelprices/uranium_AEO_2023_reference.csv) ---- - - - [uranium_AEO_2025_reference.csv](/inputs/fuelprices/uranium_AEO_2025_reference.csv) ---- - - - -#### inputs/geothermal - - - [geo_discovery_BAU.csv](/inputs/geothermal/geo_discovery_BAU.csv) ---- - - - [geo_discovery_factor_ATB_2023.csv](/inputs/geothermal/geo_discovery_factor_ATB_2023.csv) ---- - - - [geo_discovery_factor_reV.csv](/inputs/geothermal/geo_discovery_factor_reV.csv) ---- - - - [geo_discovery_TI.csv](/inputs/geothermal/geo_discovery_TI.csv) ---- - - - [geo_rsc_ATB_2023.csv](/inputs/geothermal/geo_rsc_ATB_2023.csv) ---- - - - -#### inputs/growth_constraints - - - [gbin_min.csv](/inputs/growth_constraints/gbin_min.csv) ---- - - - [growth_bin_size_mult.csv](/inputs/growth_constraints/growth_bin_size_mult.csv) ---- - - - [growth_limit_absolute.csv](/inputs/growth_constraints/growth_limit_absolute.csv) - - **Description:** Maximum expected annual builds for wind, batteries, and UPV from 2024-2026 using observed record builds. - - **Units:** MW/year - ---- - - - [growth_penalty.csv](/inputs/growth_constraints/growth_penalty.csv) ---- - - - -#### inputs/hydro - - - [cap_existing_hydro.h5](/inputs/hydro/cap_existing_hydro.h5) - - **File Type:** Input - - **Description:** Annual capacities for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset. - - **Indices:** t - - **Units:** MW - ---- - - - [hyd_fom.csv](/inputs/hydro/hyd_fom.csv) - - **Description:** Regional FOM costs for hydro ---- - - - [hydcf_fixed.h5](/inputs/hydro/hydcf_fixed.h5) - - **File Type:** Input - - **Description:** Fixed monthly zonal hydro capacity factor data partially created by ORNL and partially derived from ORNL's Existing Hydropower Assets dataset. - - **Indices:** i,month - - **Units:** unitless - ---- - - - [hydro_mingen.csv](/inputs/hydro/hydro_mingen.csv) ---- - - - [net_gen_existing_hydro.h5](/inputs/hydro/net_gen_existing_hydro.h5) - - **File Type:** Input - - **Description:** Monthly net generation values for hydro plants spanning 2007-2022, which come from ORNL's Existing Hydropower Assets dataset. - - **Indices:** t,month - - **Units:** MWh - ---- - - - [SeaCapAdj_hy.csv](/inputs/hydro/SeaCapAdj_hy.csv) ---- - - - -#### inputs/load - - - [cangrowth.csv](/inputs/load/cangrowth.csv) - - **Description:** Canada load growth multiplier ---- - - - [demand_AEO_2023_high.csv](/inputs/load/demand_AEO_2023_high.csv) - - **Description:** Load growth projection from the AEO2023 High Economic Growth scenario - - **Units:** unitless - ---- - - - [demand_AEO_2023_low.csv](/inputs/load/demand_AEO_2023_low.csv) - - **Description:** Load growth projection from the AEO2023 Low Economic Growth scenario - - **Units:** unitless - ---- - - - [demand_AEO_2023_reference.csv](/inputs/load/demand_AEO_2023_reference.csv) - - **Description:** Load growth projection from the AEO2023 Reference scenario - - **Units:** unitless - ---- - - - [demand_AEO_2025_high.csv](/inputs/load/demand_AEO_2025_high.csv) - - **Description:** Load growth projection from the AEO2025 High Economic Growth scenario - - **Units:** unitless - ---- - - - [demand_AEO_2025_low.csv](/inputs/load/demand_AEO_2025_low.csv) - - **Description:** Load growth projection from the AEO2025 Low Economic Growth scenario - - **Units:** unitless - ---- - - - [demand_AEO_2025_reference.csv](/inputs/load/demand_AEO_2025_reference.csv) - - **Description:** Load growth projection from the AEO2025 Reference scenario - - **Units:** unitless - ---- - - - [EIA_loadbystate.csv](/inputs/load/EIA_loadbystate.csv) ---- - - - [loadsite_country_test.csv](/inputs/load/loadsite_country_test.csv) ---- - - - [mex_growth_rate.csv](/inputs/load/mex_growth_rate.csv) - - **Description:** Mexico load growth multiplier ---- - - - -#### inputs/national_generation - - - [gen_mandate_tech_list.csv](/inputs/national_generation/gen_mandate_tech_list.csv) ---- - - - [gen_mandate_trajectory.csv](/inputs/national_generation/gen_mandate_trajectory.csv) ---- - - - [national_rps_frac_allScen.csv](/inputs/national_generation/national_rps_frac_allScen.csv) ---- - - - -#### inputs/outages - - - [temperature_celsius-st.h5](/inputs/outages/temperature_celsius-st.h5) ---- - - - -#### inputs/plant_characteristics - - - [battery_ATB_2024_advanced.csv](/inputs/plant_characteristics/battery_ATB_2024_advanced.csv) - - **Dollar year:** 2021 ---- - - - [battery_ATB_2024_conservative.csv](/inputs/plant_characteristics/battery_ATB_2024_conservative.csv) - - **Dollar year:** 2021 ---- - - - [battery_ATB_2024_moderate.csv](/inputs/plant_characteristics/battery_ATB_2024_moderate.csv) - - **Dollar year:** 2021 ---- - - - [beccs_BVRE_2021_high.csv](/inputs/plant_characteristics/beccs_BVRE_2021_high.csv) ---- - - - [beccs_BVRE_2021_low.csv](/inputs/plant_characteristics/beccs_BVRE_2021_low.csv) ---- - - - [beccs_BVRE_2021_mid.csv](/inputs/plant_characteristics/beccs_BVRE_2021_mid.csv) ---- - - - [beccs_lowcost.csv](/inputs/plant_characteristics/beccs_lowcost.csv) ---- - - - [beccs_reference.csv](/inputs/plant_characteristics/beccs_reference.csv) ---- - - - [biopower_ATB_2024_moderate.csv](/inputs/plant_characteristics/biopower_ATB_2024_moderate.csv) ---- - - - [ccsflex_ATB_2020_cost.csv](/inputs/plant_characteristics/ccsflex_ATB_2020_cost.csv) ---- - - - [ccsflex_ATB_2020_perf.csv](/inputs/plant_characteristics/ccsflex_ATB_2020_perf.csv) ---- - - - [coal-ccs_ATB_2024_advanced.csv](/inputs/plant_characteristics/coal-ccs_ATB_2024_advanced.csv) ---- - - - [coal-ccs_ATB_2024_conservative.csv](/inputs/plant_characteristics/coal-ccs_ATB_2024_conservative.csv) ---- - - - [coal-ccs_ATB_2024_moderate.csv](/inputs/plant_characteristics/coal-ccs_ATB_2024_moderate.csv) ---- - - - [coal_ATB_2024_moderate.csv](/inputs/plant_characteristics/coal_ATB_2024_moderate.csv) ---- - - - [cost_opres_default.csv](/inputs/plant_characteristics/cost_opres_default.csv) ---- - - - [cost_opres_market.csv](/inputs/plant_characteristics/cost_opres_market.csv) ---- - - - [csp_ATB_2023_advanced.csv](/inputs/plant_characteristics/csp_ATB_2023_advanced.csv) ---- - - - [csp_ATB_2023_conservative.csv](/inputs/plant_characteristics/csp_ATB_2023_conservative.csv) ---- - - - [csp_ATB_2023_moderate.csv](/inputs/plant_characteristics/csp_ATB_2023_moderate.csv) ---- - - - [csp_ATB_2024_advanced.csv](/inputs/plant_characteristics/csp_ATB_2024_advanced.csv) ---- - - - [csp_ATB_2024_conservative.csv](/inputs/plant_characteristics/csp_ATB_2024_conservative.csv) ---- - - - [csp_ATB_2024_moderate.csv](/inputs/plant_characteristics/csp_ATB_2024_moderate.csv) ---- - - - [csp_SunShot2030.csv](/inputs/plant_characteristics/csp_SunShot2030.csv) - - **Description:** Csp costs from the SunShot2030 cost scenario ---- - - - [dollaryear.csv](/inputs/plant_characteristics/dollaryear.csv) - - **Description:** Dollar year mapping for each plant cost scenario ---- - - - [dr_shed_capcost_demo_data_IEF_January_2025.csv](/inputs/plant_characteristics/dr_shed_capcost_demo_data_IEF_January_2025.csv) ---- - - - [dr_shed_fom.csv](/inputs/plant_characteristics/dr_shed_fom.csv) ---- - - - [dr_shed_vom.csv](/inputs/plant_characteristics/dr_shed_vom.csv) ---- - - - [evmc_shape_Baseline.csv](/inputs/plant_characteristics/evmc_shape_Baseline.csv) ---- - - - [evmc_storage_Baseline.csv](/inputs/plant_characteristics/evmc_storage_Baseline.csv) ---- - - - [gas-ccs_ATB_2024_advanced.csv](/inputs/plant_characteristics/gas-ccs_ATB_2024_advanced.csv) ---- - - - [gas-ccs_ATB_2024_conservative.csv](/inputs/plant_characteristics/gas-ccs_ATB_2024_conservative.csv) ---- - - - [gas-ccs_ATB_2024_moderate.csv](/inputs/plant_characteristics/gas-ccs_ATB_2024_moderate.csv) ---- - - - [gas_ATB_2024_moderate.csv](/inputs/plant_characteristics/gas_ATB_2024_moderate.csv) ---- - - - [geo_ATB_2023_advanced.csv](/inputs/plant_characteristics/geo_ATB_2023_advanced.csv) ---- - - - [geo_ATB_2023_conservative.csv](/inputs/plant_characteristics/geo_ATB_2023_conservative.csv) ---- - - - [geo_ATB_2023_moderate.csv](/inputs/plant_characteristics/geo_ATB_2023_moderate.csv) ---- - - - [geo_ATB_2024_advanced.csv](/inputs/plant_characteristics/geo_ATB_2024_advanced.csv) ---- - - - [geo_ATB_2024_conservative.csv](/inputs/plant_characteristics/geo_ATB_2024_conservative.csv) ---- - - - [geo_ATB_2024_moderate.csv](/inputs/plant_characteristics/geo_ATB_2024_moderate.csv) ---- - - - [h2-combustion_ATB_2023.csv](/inputs/plant_characteristics/h2-combustion_ATB_2023.csv) ---- - - - [h2-combustion_ATB_2024.csv](/inputs/plant_characteristics/h2-combustion_ATB_2024.csv) - - **Description:** Hydrogen CT and CC plant costs generated in preprocessing from moderate case NREL ATB 2024 data ---- - - - [heat_rate_adj.csv](/inputs/plant_characteristics/heat_rate_adj.csv) - - **Description:** Heat rate adjustment multiplier by technology ---- - - - [heat_rate_penalty_spin.csv](/inputs/plant_characteristics/heat_rate_penalty_spin.csv) ---- - - - [hydro_ATB_2019_constant.csv](/inputs/plant_characteristics/hydro_ATB_2019_constant.csv) - - **Description:** Hydro costs from the 2019 ATB constant cost scenario ---- - - - [hydro_ATB_2019_low.csv](/inputs/plant_characteristics/hydro_ATB_2019_low.csv) - - **Description:** Hydro costs from the 2019 ATB low cost scenario ---- - - - [hydro_ATB_2019_mid.csv](/inputs/plant_characteristics/hydro_ATB_2019_mid.csv) - - **Description:** Hydro costs from the 2019 ATB mid cost scenario ---- - - - [maxage.csv](/inputs/plant_characteristics/maxage.csv) - - **Description:** Maximum age allowed for each technology ---- - - - [maxdailycf.csv](/inputs/plant_characteristics/maxdailycf.csv) - - **Description:** maximum daily capacity factor--dr_shed input supply curves are based on one 4-hour event per day ---- - - - [min_retire_age.csv](/inputs/plant_characteristics/min_retire_age.csv) - - **Description:** Minimum retirement age for given technology ---- - - - [minCF.csv](/inputs/plant_characteristics/minCF.csv) - - **Description:** minimum annual capacity factor for each tech fleet - applied to i-rto ---- - - - [mingen_fixed.csv](/inputs/plant_characteristics/mingen_fixed.csv) ---- - - - [minloadfrac0.csv](/inputs/plant_characteristics/minloadfrac0.csv) - - **Description:** characteristics/minloadfrac0 database of minloadbed generator cs ---- - - - [mttr.csv](/inputs/plant_characteristics/mttr.csv) ---- - - - [nuclear-smr_ATB_2024_advanced.csv](/inputs/plant_characteristics/nuclear-smr_ATB_2024_advanced.csv) ---- - - - [nuclear-smr_ATB_2024_conservative.csv](/inputs/plant_characteristics/nuclear-smr_ATB_2024_conservative.csv) ---- - - - [nuclear-smr_ATB_2024_moderate.csv](/inputs/plant_characteristics/nuclear-smr_ATB_2024_moderate.csv) ---- - - - [nuclear_ATB_2024_advanced.csv](/inputs/plant_characteristics/nuclear_ATB_2024_advanced.csv) ---- - - - [nuclear_ATB_2024_conservative.csv](/inputs/plant_characteristics/nuclear_ATB_2024_conservative.csv) ---- - - - [nuclear_ATB_2024_moderate.csv](/inputs/plant_characteristics/nuclear_ATB_2024_moderate.csv) ---- - - - [ofs-wind_ATB_2023_advanced.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_advanced.csv) - - **File Type:** Inputs file - - **Description:** 2023 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year - - **Dollar year:** 2004 ---- - - - [ofs-wind_ATB_2023_conservative.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_conservative.csv) - - **File Type:** Inputs file - - **Description:** 2023 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year - - **Dollar year:** 2004 ---- - - - [ofs-wind_ATB_2023_moderate.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate.csv) - - **File Type:** Inputs file - - **Description:** 2023 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year - - **Dollar year:** 2004 ---- - - - [ofs-wind_ATB_2023_moderate_noFloating.csv](/inputs/plant_characteristics/ofs-wind_ATB_2023_moderate_noFloating.csv) ---- - - - [ofs-wind_ATB_2024_advanced.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_advanced.csv) - - **File Type:** Inputs file - - **Description:** 2024 advanced ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year - - **Dollar year:** 2022 ---- - - - [ofs-wind_ATB_2024_conservative.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_conservative.csv) - - **File Type:** Inputs file - - **Description:** 2024 conservative ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year - - **Dollar year:** 2022 ---- - - - [ofs-wind_ATB_2024_moderate.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate.csv) - - **File Type:** Inputs file - - **Description:** 2024 moderate ofs-wind capital, fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year - - **Dollar year:** 2022 ---- - - - [ofs-wind_ATB_2024_moderate_noFloating.csv](/inputs/plant_characteristics/ofs-wind_ATB_2024_moderate_noFloating.csv) - - **File Type:** Inputs file - - **Description:** 2024 moderate_noFloating ofs-wind capital (5x floating capital cost), fixed O&M, var O&M costs and rsc_mult (SC cost reduction mult) by class and year - - **Dollar year:** 2022 ---- - - - [ons-wind_ATB_2023_advanced.csv](/inputs/plant_characteristics/ons-wind_ATB_2023_advanced.csv) ---- - - - [ons-wind_ATB_2023_conservative.csv](/inputs/plant_characteristics/ons-wind_ATB_2023_conservative.csv) ---- - - - [ons-wind_ATB_2023_moderate.csv](/inputs/plant_characteristics/ons-wind_ATB_2023_moderate.csv) ---- - - - [ons-wind_ATB_2024_advanced.csv](/inputs/plant_characteristics/ons-wind_ATB_2024_advanced.csv) - - **File Type:** Inputs file - - **Description:** Advanced cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind - - **Dollar year:** 2022 ---- - - - [ons-wind_ATB_2024_conservative.csv](/inputs/plant_characteristics/ons-wind_ATB_2024_conservative.csv) - - **File Type:** Inputs file - - **Description:** Conservative cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind - - **Dollar year:** 2022 ---- - - - [ons-wind_ATB_2024_moderate.csv](/inputs/plant_characteristics/ons-wind_ATB_2024_moderate.csv) - - **File Type:** Inputs file - - **Description:** Moderate cost and performance inputs from the 2024 Annual Technology Baseline for land-based wind - - **Dollar year:** 2022 ---- - - - [other_plantchar.csv](/inputs/plant_characteristics/other_plantchar.csv) ---- - - - [outage_forced_static.csv](/inputs/plant_characteristics/outage_forced_static.csv) - - **File Type:** Inputs file - - **Description:** Forced outage rates by technology ---- - - - [outage_forced_temperature_murphy2019.csv](/inputs/plant_characteristics/outage_forced_temperature_murphy2019.csv) ---- - - - [outage_scheduled_monthly.csv](/inputs/plant_characteristics/outage_scheduled_monthly.csv) ---- - - - [outage_scheduled_static.csv](/inputs/plant_characteristics/outage_scheduled_static.csv) - - **Description:** Scheduled outage rate by technology ---- - - - [pvb_benchmark2020.csv](/inputs/plant_characteristics/pvb_benchmark2020.csv) ---- - - - [ramprate.csv](/inputs/plant_characteristics/ramprate.csv) - - **Description:** Generator ramp rates by technology ---- - - - [startcost.csv](/inputs/plant_characteristics/startcost.csv) ---- - - - [unitsize_atb.csv](/inputs/plant_characteristics/unitsize_atb.csv) ---- - - - [upv_ATB_2023_advanced.csv](/inputs/plant_characteristics/upv_ATB_2023_advanced.csv) - - **File Type:** Inputs file - - **Description:** 2023 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year - - **Dollar year:** 2004 ---- - - - [upv_ATB_2023_conservative.csv](/inputs/plant_characteristics/upv_ATB_2023_conservative.csv) - - **File Type:** Inputs file - - **Description:** 2023 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year - - **Dollar year:** 2004 ---- - - - [upv_ATB_2023_moderate.csv](/inputs/plant_characteristics/upv_ATB_2023_moderate.csv) - - **File Type:** Inputs file - - **Description:** 2023 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year - - **Dollar year:** 2004 ---- - - - [upv_ATB_2024_advanced.csv](/inputs/plant_characteristics/upv_ATB_2024_advanced.csv) - - **File Type:** Inputs file - - **Description:** 2024 advanced UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year - - **Dollar year:** 2004 ---- - - - [upv_ATB_2024_conservative.csv](/inputs/plant_characteristics/upv_ATB_2024_conservative.csv) - - **File Type:** Inputs file - - **Description:** 2024 conservative UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year - - **Dollar year:** 2004 ---- - - - [upv_ATB_2024_moderate.csv](/inputs/plant_characteristics/upv_ATB_2024_moderate.csv) - - **File Type:** Inputs file - - **Description:** 2024 moderate UPV capital, FOM and VOM costs, and capacity factor improvement multipliers by year - - **Dollar year:** 2004 ---- - - - [years_until_endogenous.csv](/inputs/plant_characteristics/years_until_endogenous.csv) ---- - - - -#### inputs/profiles_cf - - - [cf_distpv_county.h5](/inputs/profiles_cf/cf_distpv_county.h5) ---- - - - [cf_upv_limited_ba.h5](/inputs/profiles_cf/cf_upv_limited_ba.h5) ---- - - - [cf_upv_limited_county.h5](/inputs/profiles_cf/cf_upv_limited_county.h5) ---- - - - [cf_upv_open_ba.h5](/inputs/profiles_cf/cf_upv_open_ba.h5) ---- - - - [cf_upv_open_county.h5](/inputs/profiles_cf/cf_upv_open_county.h5) ---- - - - [cf_upv_reference_ba.h5](/inputs/profiles_cf/cf_upv_reference_ba.h5) ---- - - - [cf_upv_reference_county.h5](/inputs/profiles_cf/cf_upv_reference_county.h5) ---- - - - [cf_wind-ofs_meshed_limited_ba.h5](/inputs/profiles_cf/cf_wind-ofs_meshed_limited_ba.h5) ---- - - - [cf_wind-ofs_meshed_open_ba.h5](/inputs/profiles_cf/cf_wind-ofs_meshed_open_ba.h5) ---- - - - [cf_wind-ofs_meshed_reference_ba.h5](/inputs/profiles_cf/cf_wind-ofs_meshed_reference_ba.h5) ---- - - - [cf_wind-ofs_radial_limited_ba.h5](/inputs/profiles_cf/cf_wind-ofs_radial_limited_ba.h5) ---- - - - [cf_wind-ofs_radial_limited_county.h5](/inputs/profiles_cf/cf_wind-ofs_radial_limited_county.h5) ---- - - - [cf_wind-ofs_radial_open_ba.h5](/inputs/profiles_cf/cf_wind-ofs_radial_open_ba.h5) ---- - - - [cf_wind-ofs_radial_open_county.h5](/inputs/profiles_cf/cf_wind-ofs_radial_open_county.h5) ---- - - - [cf_wind-ofs_radial_reference_ba.h5](/inputs/profiles_cf/cf_wind-ofs_radial_reference_ba.h5) ---- - - - [cf_wind-ofs_radial_reference_county.h5](/inputs/profiles_cf/cf_wind-ofs_radial_reference_county.h5) ---- - - - [cf_wind-ons_limited_ba.h5](/inputs/profiles_cf/cf_wind-ons_limited_ba.h5) ---- - - - [cf_wind-ons_limited_county.h5](/inputs/profiles_cf/cf_wind-ons_limited_county.h5) ---- - - - [cf_wind-ons_open_ba.h5](/inputs/profiles_cf/cf_wind-ons_open_ba.h5) ---- - - - [cf_wind-ons_open_county.h5](/inputs/profiles_cf/cf_wind-ons_open_county.h5) ---- - - - [cf_wind-ons_reference_ba.h5](/inputs/profiles_cf/cf_wind-ons_reference_ba.h5) ---- - - - [cf_wind-ons_reference_county.h5](/inputs/profiles_cf/cf_wind-ons_reference_county.h5) ---- - - - -#### inputs/profiles_demand - - - [demand_EER2023_100by2050.h5](/inputs/profiles_demand/demand_EER2023_100by2050.h5) ---- - - - [demand_EER2023_Baseline_AEO2022.h5](/inputs/profiles_demand/demand_EER2023_Baseline_AEO2022.h5) ---- - - - [demand_EER2023_IRAlow.h5](/inputs/profiles_demand/demand_EER2023_IRAlow.h5) ---- - - - [demand_EER2023_IRAmoderate.h5](/inputs/profiles_demand/demand_EER2023_IRAmoderate.h5) ---- - - - [demand_EER2025_100by2050.h5](/inputs/profiles_demand/demand_EER2025_100by2050.h5) ---- - - - [demand_EER2025_Baseline_AEO2023.h5](/inputs/profiles_demand/demand_EER2025_Baseline_AEO2023.h5) ---- - - - [demand_EER2025_IRAlow.h5](/inputs/profiles_demand/demand_EER2025_IRAlow.h5) ---- - - - [demand_EFS_Baseline.h5](/inputs/profiles_demand/demand_EFS_Baseline.h5) ---- - - - [demand_EFS_Clean2035.h5](/inputs/profiles_demand/demand_EFS_Clean2035.h5) ---- - - - [demand_EFS_Clean2035_LTS.h5](/inputs/profiles_demand/demand_EFS_Clean2035_LTS.h5) ---- - - - [demand_EFS_Clean2035clip1pct.h5](/inputs/profiles_demand/demand_EFS_Clean2035clip1pct.h5) ---- - - - [demand_EFS_HIGH.h5](/inputs/profiles_demand/demand_EFS_HIGH.h5) ---- - - - [demand_EFS_MEDIUM.h5](/inputs/profiles_demand/demand_EFS_MEDIUM.h5) ---- - - - [demand_EFS_MEDIUMStretch2040.h5](/inputs/profiles_demand/demand_EFS_MEDIUMStretch2040.h5) ---- - - - [demand_EFS_MEDIUMStretch2046.h5](/inputs/profiles_demand/demand_EFS_MEDIUMStretch2046.h5) ---- - - - [demand_EFS_REFERENCE.h5](/inputs/profiles_demand/demand_EFS_REFERENCE.h5) ---- - - - [demand_historic.h5](/inputs/profiles_demand/demand_historic.h5) ---- - - - -#### inputs/remote - - - [cf_distpv_county_18421977.h5](/inputs/remote/cf_distpv_county_18421977.h5) ---- - - - [cf_upv_limited_ba_18407660.h5](/inputs/remote/cf_upv_limited_ba_18407660.h5) ---- - - - [cf_upv_limited_county_18407660.h5](/inputs/remote/cf_upv_limited_county_18407660.h5) ---- - - - [cf_upv_open_ba_18407660.h5](/inputs/remote/cf_upv_open_ba_18407660.h5) ---- - - - [cf_upv_open_county_18407660.h5](/inputs/remote/cf_upv_open_county_18407660.h5) ---- - - - [cf_upv_reference_ba_18407660.h5](/inputs/remote/cf_upv_reference_ba_18407660.h5) ---- - - - [cf_upv_reference_county_18407660.h5](/inputs/remote/cf_upv_reference_county_18407660.h5) ---- - - - [cf_wind-ofs_meshed_limited_ba_18423723.h5](/inputs/remote/cf_wind-ofs_meshed_limited_ba_18423723.h5) ---- - - - [cf_wind-ofs_meshed_open_ba_18423723.h5](/inputs/remote/cf_wind-ofs_meshed_open_ba_18423723.h5) ---- - - - [cf_wind-ofs_meshed_reference_ba_18423723.h5](/inputs/remote/cf_wind-ofs_meshed_reference_ba_18423723.h5) ---- - - - [cf_wind-ofs_radial_limited_ba_18423723.h5](/inputs/remote/cf_wind-ofs_radial_limited_ba_18423723.h5) ---- - - - [cf_wind-ofs_radial_limited_county_18423723.h5](/inputs/remote/cf_wind-ofs_radial_limited_county_18423723.h5) ---- - - - [cf_wind-ofs_radial_open_ba_18423723.h5](/inputs/remote/cf_wind-ofs_radial_open_ba_18423723.h5) ---- - - - [cf_wind-ofs_radial_open_county_18423723.h5](/inputs/remote/cf_wind-ofs_radial_open_county_18423723.h5) ---- - - - [cf_wind-ofs_radial_reference_ba_18423723.h5](/inputs/remote/cf_wind-ofs_radial_reference_ba_18423723.h5) ---- - - - [cf_wind-ofs_radial_reference_county_18423723.h5](/inputs/remote/cf_wind-ofs_radial_reference_county_18423723.h5) ---- - - - [cf_wind-ons_limited_ba_18422200.h5](/inputs/remote/cf_wind-ons_limited_ba_18422200.h5) ---- - - - [cf_wind-ons_limited_county_18422200.h5](/inputs/remote/cf_wind-ons_limited_county_18422200.h5) ---- - - - [cf_wind-ons_open_ba_18422200.h5](/inputs/remote/cf_wind-ons_open_ba_18422200.h5) ---- - - - [cf_wind-ons_open_county_18422200.h5](/inputs/remote/cf_wind-ons_open_county_18422200.h5) ---- - - - [cf_wind-ons_reference_ba_18422200.h5](/inputs/remote/cf_wind-ons_reference_ba_18422200.h5) ---- - - - [cf_wind-ons_reference_county_18422200.h5](/inputs/remote/cf_wind-ons_reference_county_18422200.h5) ---- - - - [demand_EER2023_100by2050_18423998.h5](/inputs/remote/demand_EER2023_100by2050_18423998.h5) ---- - - - [demand_EER2023_Baseline_AEO2022_18423998.h5](/inputs/remote/demand_EER2023_Baseline_AEO2022_18423998.h5) ---- - - - [demand_EER2023_IRAlow_18423998.h5](/inputs/remote/demand_EER2023_IRAlow_18423998.h5) ---- - - - [demand_EER2023_IRAmoderate_18423998.h5](/inputs/remote/demand_EER2023_IRAmoderate_18423998.h5) ---- - - - [demand_EER2025_100by2050_18435264.h5](/inputs/remote/demand_EER2025_100by2050_18435264.h5) ---- - - - [demand_EER2025_Baseline_AEO2023_18435264.h5](/inputs/remote/demand_EER2025_Baseline_AEO2023_18435264.h5) ---- - - - [demand_EER2025_IRAlow_18435264.h5](/inputs/remote/demand_EER2025_IRAlow_18435264.h5) ---- - - - [demand_EFS_Baseline_18461543.h5](/inputs/remote/demand_EFS_Baseline_18461543.h5) ---- - - - [demand_EFS_Clean2035_18461543.h5](/inputs/remote/demand_EFS_Clean2035_18461543.h5) ---- - - - [demand_EFS_Clean2035_LTS_18461543.h5](/inputs/remote/demand_EFS_Clean2035_LTS_18461543.h5) ---- - - - [demand_EFS_Clean2035clip1pct_18461543.h5](/inputs/remote/demand_EFS_Clean2035clip1pct_18461543.h5) ---- - - - [demand_EFS_HIGH_18461543.h5](/inputs/remote/demand_EFS_HIGH_18461543.h5) ---- - - - [demand_EFS_MEDIUM_18461543.h5](/inputs/remote/demand_EFS_MEDIUM_18461543.h5) ---- - - - [demand_EFS_MEDIUMStretch2040_18461543.h5](/inputs/remote/demand_EFS_MEDIUMStretch2040_18461543.h5) ---- - - - [demand_EFS_MEDIUMStretch2046_18461543.h5](/inputs/remote/demand_EFS_MEDIUMStretch2046_18461543.h5) ---- - - - [demand_EFS_REFERENCE_18461543.h5](/inputs/remote/demand_EFS_REFERENCE_18461543.h5) ---- - - - [demand_historic_18462671.h5](/inputs/remote/demand_historic_18462671.h5) ---- - - - -#### inputs/reserves - - - [ccseason_dates.csv](/inputs/reserves/ccseason_dates.csv) ---- - - - [opres_periods.csv](/inputs/reserves/opres_periods.csv) ---- - - - [orperc.csv](/inputs/reserves/orperc.csv) ---- - - - [peak_net_imports.csv](/inputs/reserves/peak_net_imports.csv) ---- - - - [prm_annual.csv](/inputs/reserves/prm_annual.csv) - - **Description:** Annual planning reserve margin by NERC region ---- - - - [ramptime.csv](/inputs/reserves/ramptime.csv) ---- - - - -#### inputs/sets - - - [aclike.csv](/inputs/sets/aclike.csv) - - **File Type:** GAMS set - - **Description:** set of AC transmission capacity types ---- - - - [allt.csv](/inputs/sets/allt.csv) - - **File Type:** GAMS set - - **Description:** set of all potential years ---- - - - [bioclass.csv](/inputs/sets/bioclass.csv) - - **File Type:** GAMS set - - **Description:** set of bio tech classes ---- - - - [ccsflex_cat.csv](/inputs/sets/ccsflex_cat.csv) - - **File Type:** GAMS set - - **Description:** set of flexible ccs performance parameter categories ---- - - - [climate_param.csv](/inputs/sets/climate_param.csv) - - **File Type:** GAMS set - - **Description:** set of parameters defined in climate_heuristics_finalyear ---- - - - [consumecat.csv](/inputs/sets/consumecat.csv) - - **File Type:** GAMS set - - **Description:** set of categories for consuming facility characteristics ---- - - - [csapr_cat.csv](/inputs/sets/csapr_cat.csv) - - **File Type:** GAMS set - - **Description:** set of CSAPR regulation categories ---- - - - [csapr_group.csv](/inputs/sets/csapr_group.csv) - - **File Type:** GAMS set - - **Description:** set of CSAPR trading groups ---- - - - [ctt.csv](/inputs/sets/ctt.csv) - - **File Type:** GAMS set - - **Description:** set of cooling technology types ---- - - - [e.csv](/inputs/sets/e.csv) - - **File Type:** GAMS set - - **Description:** set of emission categories used in model ---- - - - [eall.csv](/inputs/sets/eall.csv) - - **File Type:** GAMS set - - **Description:** set of emission categories used in reporting ---- - - - [etype.csv](/inputs/sets/etype.csv) ---- - - - [f.csv](/inputs/sets/f.csv) - - **File Type:** GAMS set - - **Description:** set of fuel types ---- - - - [flex_type.csv](/inputs/sets/flex_type.csv) - - **File Type:** GAMS set - - **Description:** set of demand flexibility types ---- - - - [fuel2tech.csv](/inputs/sets/fuel2tech.csv) - - **File Type:** GAMS set - - **Description:** mapping between fuel types and generations ---- - - - [fuelbin.csv](/inputs/sets/fuelbin.csv) - - **File Type:** GAMS set - - **Description:** set of gas usage brackets ---- - - - [gb.csv](/inputs/sets/gb.csv) - - **File Type:** GAMS set - - **Description:** set of gas price bins ---- - - - [gbin.csv](/inputs/sets/gbin.csv) - - **File Type:** GAMS set - - **Description:** set of growth bins ---- - - - [geotech.csv](/inputs/sets/geotech.csv) - - **File Type:** GAMS set - - **Description:** set of geothermal technology categories ---- - - - [h2_st.csv](/inputs/sets/h2_st.csv) - - **File Type:** GAMS set - - **Description:** defines investments needed to store and transport H2 ---- - - - [h2_stor.csv](/inputs/sets/h2_stor.csv) - - **File Type:** GAMS set - - **Description:** set of H2 storage options ---- - - - [hintage_char.csv](/inputs/sets/hintage_char.csv) - - **File Type:** GAMS set - - **Description:** set of characteristics available in hintage_data ---- - - - [i.csv](/inputs/sets/i.csv) - - **File Type:** GAMS set - - **Description:** set of technologies ---- - - - [i_geotech.csv](/inputs/sets/i_geotech.csv) - - **File Type:** GAMS set - - **Description:** crosswalk between an individual geothermal technology and its category ---- - - - [i_h2_ptc_gen.csv](/inputs/sets/i_h2_ptc_gen.csv) - - **File Type:** GAMS set - - **Description:** set of technologies which can produce energy for electrolyzers claiming the hydrogen production tax credit due to their low lifecycle carbon emissions ---- - - - [i_p.csv](/inputs/sets/i_p.csv) - - **File Type:** GAMS set - - **Description:** mapping from technologies to the products they produce ---- - - - [i_subtech.csv](/inputs/sets/i_subtech.csv) - - **File Type:** GAMS set - - **Description:** set of categories for subtechs ---- - - - [i_water_nocooling.csv](/inputs/sets/i_water_nocooling.csv) - - **File Type:** GAMS set - - **Description:** set of technologies that use water, but are not differentiated by cooling tech and water source ---- - - - [lcclike.csv](/inputs/sets/lcclike.csv) - - **File Type:** GAMS set - - **Description:** set of transmission capacity types where lines are bundled with AC/DC converters ---- - - - [month.csv](/inputs/sets/month.csv) - - **File Type:** GAMS set ---- - - - [noretire.csv](/inputs/sets/noretire.csv) - - **File Type:** GAMS set - - **Description:** set of technologies that will never be retired ---- - - - [notvsc.csv](/inputs/sets/notvsc.csv) - - **File Type:** GAMS set - - **Description:** set of transmission capacity types that are not VSC ---- - - - [ofstype.csv](/inputs/sets/ofstype.csv) - - **File Type:** GAMS set - - **Description:** set of offshore types used in offshore requirement constraint (eq_RPS_OFSWind) ---- - - - [ofstype_i.csv](/inputs/sets/ofstype_i.csv) - - **File Type:** GAMS set - - **Description:** crosswalk between ofstype and i ---- - - - [orcat.csv](/inputs/sets/orcat.csv) - - **File Type:** GAMS set - - **Description:** set of operating reserve categories ---- - - - [ortype.csv](/inputs/sets/ortype.csv) - - **File Type:** GAMS set - - **Description:** set of types of operating reserve constraints ---- - - - [p.csv](/inputs/sets/p.csv) - - **File Type:** GAMS set - - **Description:** set of products produced ---- - - - [pcat.csv](/inputs/sets/pcat.csv) - - **File Type:** GAMS set - - **Description:** set of prescribed technology categories ---- - - - [plantcat.csv](/inputs/sets/plantcat.csv) - - **File Type:** GAMS set - - **Description:** set of categories for plant characteristics ---- - - - [prepost.csv](/inputs/sets/prepost.csv) - - **File Type:** GAMS set ---- - - - [prescriptivelink0.csv](/inputs/sets/prescriptivelink0.csv) - - **File Type:** GAMS set - - **Description:** initial set of prescribed categories and their technologies - used in assigning prescribed builds ---- - - - [pvb_agg.csv](/inputs/sets/pvb_agg.csv) - - **File Type:** GAMS set - - **Description:** crosswalk between hybrid pv+battery configurations and technology options ---- - - - [pvb_config.csv](/inputs/sets/pvb_config.csv) - - **File Type:** GAMS set - - **Description:** set of hybrid pv+battery configurations ---- - - - [quarter.csv](/inputs/sets/quarter.csv) - - **File Type:** GAMS set ---- - - - [resourceclass.csv](/inputs/sets/resourceclass.csv) - - **File Type:** GAMS set - - **Description:** set of renewable resource classes ---- - - - [RPSCat.csv](/inputs/sets/RPSCat.csv) - - **File Type:** GAMS set - - **Description:** set of RPS constraint categories, including clean energy standards ---- - - - [sc_cat.csv](/inputs/sets/sc_cat.csv) - - **File Type:** GAMS set - - **Description:** set of supply curve categories (capacity and cost) ---- - - - [sdbin.csv](/inputs/sets/sdbin.csv) - - **File Type:** GAMS set - - **Description:** set of storage durage bins ---- - - - [sw.csv](/inputs/sets/sw.csv) - - **File Type:** GAMS set - - **Description:** set of surface water types where access is based on consumption not withdrawal ---- - - - [tg.csv](/inputs/sets/tg.csv) - - **File Type:** GAMS set - - **Description:** set of technology groups ---- - - - [tg_rsc_cspagg.csv](/inputs/sets/tg_rsc_cspagg.csv) - - **File Type:** GAMS set - - **Description:** set of csp technologies that belong to the same class ---- - - - [tg_rsc_upvagg.csv](/inputs/sets/tg_rsc_upvagg.csv) - - **File Type:** GAMS set - - **Description:** set of pv and pvb technologies that belong to the same class ---- - - - [trancap_fut_cat.csv](/inputs/sets/trancap_fut_cat.csv) - - **File Type:** GAMS set - - **Description:** set of categories of near-term transmission projects that describe the likelihood of being completed ---- - - - [trtype.csv](/inputs/sets/trtype.csv) - - **File Type:** GAMS set - - **Description:** set of transmission capacity types ---- - - - [unitspec_upgrades.csv](/inputs/sets/unitspec_upgrades.csv) - - **File Type:** GAMS set - - **Description:** set of upgraded technologies that get unit-specific characteristics ---- - - - [upgrade_hintage_char.csv](/inputs/sets/upgrade_hintage_char.csv) - - **File Type:** GAMS set - - **Description:** set to operate over in extension of hintage_data characteristics when sw_upgrades = 1 ---- - - - [w.csv](/inputs/sets/w.csv) - - **File Type:** GAMS set - - **Description:** set of water withdrawal or consumption options for water techs ---- - - - [wst.csv](/inputs/sets/wst.csv) - - **File Type:** GAMS set - - **Description:** set of water source types ---- - - - [wst_climate.csv](/inputs/sets/wst_climate.csv) - - **File Type:** GAMS set - - **Description:** set of water sources affected by climate change ---- - - - [yearafter.csv](/inputs/sets/yearafter.csv) - - **File Type:** GAMS set - - **Description:** set to loop over for the final year calculation ---- - - - -#### inputs/shapefiles - - - [state_fips_codes.csv](/inputs/shapefiles/state_fips_codes.csv) - - **Description:** Mapping of states to FIPS codes and postcal code abbreviations ---- - - - -#### inputs/state_policies - - - [acp_disallowed.csv](/inputs/state_policies/acp_disallowed.csv) - - **Description:** List of states which do not allow alternative compliance payments in place of meeting RPS or CES requirements ---- - - - [acp_prices.csv](/inputs/state_policies/acp_prices.csv) ---- - - - [ces_fraction.csv](/inputs/state_policies/ces_fraction.csv) - - **Description:** Annual compliance for states with a CES policy ---- - - - [forced_retirements.csv](/inputs/state_policies/forced_retirements.csv) - - **Description:** List of regions with mandatory retirement policies for certain technologies ---- - - - [hydrofrac_policy.csv](/inputs/state_policies/hydrofrac_policy.csv) ---- - - - [ng_crf_penalty_st.csv](/inputs/state_policies/ng_crf_penalty_st.csv) - - **File Type:** Inputs - - **Description:** Cost adjustment for NG techs in states where all NG techs must be retired by a certain year - - **Indices:** allt,st - - **Dollar year:** N/A - - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220](https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220) - - **Units:** rate (unitless) - ---- - - - [nuclear_subsidies.csv](/inputs/state_policies/nuclear_subsidies.csv) ---- - - - [offshore_req_default.csv](/inputs/state_policies/offshore_req_default.csv) - - **File Type:** Inputs - - **Description:** default state mandates of offshore wind capacity, updated in November 2025 - - **Indices:** st,allt - - **Units:** MW - ---- - - - [oosfrac.csv](/inputs/state_policies/oosfrac.csv) - - **Description:** Defines the fraction of renewable and clean energy credits can be purchased from out of state (oos). Applied for RPS and CES ---- - - - [recstyle.csv](/inputs/state_policies/recstyle.csv) - - **Description:** Indication for how to apply state requirement (0 = end-use sales, 1 = bus-bar sales, 2 = generation). Default is 0. ---- - - - [rectable.csv](/inputs/state_policies/rectable.csv) - - **Description:** Table defining which states are allowed to trade RECs ---- - - - [rps_fraction.csv](/inputs/state_policies/rps_fraction.csv) - - **Description:** Indicates what fraction of sales or generation (based on recstyle.csv) must be from renewable energy ---- - - - [storage_mandates.csv](/inputs/state_policies/storage_mandates.csv) - - **Description:** Energy storage mandates by region ---- - - - [techs_banned_ces.csv](/inputs/state_policies/techs_banned_ces.csv) - - **Description:** Indicates which technolgies are not eligible to contribute to CES ---- - - - [techs_banned_imports_rps.csv](/inputs/state_policies/techs_banned_imports_rps.csv) ---- - - - [techs_banned_rps.csv](/inputs/state_policies/techs_banned_rps.csv) - - **Description:** Indicates which technolgies are not eligible to contribute to RPS ---- - - - [unbundled_limit_ces.csv](/inputs/state_policies/unbundled_limit_ces.csv) - - **Description:** Limit on fraction of credits towards CES which can be purchased unbundled from other states ---- - - - [unbundled_limit_rps.csv](/inputs/state_policies/unbundled_limit_rps.csv) - - **Description:** Limit on fraction of credits towards RPS which can be purchased unbundled from other states ---- - - - -#### inputs/storage - - - [cap_existing_psh.csv](/inputs/storage/cap_existing_psh.csv) - - **Description:** County-wide PSH operational capacity, pump capacity, and max energy, based on plant-level data from https://www.hydropower.org/hydropower-pumped-storage-tool - - **Units:** MW/MWh ---- - - - [PSH_supply_curves_durations.csv](/inputs/storage/PSH_supply_curves_durations.csv) ---- - - - [storinmaxfrac.csv](/inputs/storage/storinmaxfrac.csv) ---- - - - -#### inputs/supply_curve - - - [bio_supplycurve.csv](/inputs/supply_curve/bio_supplycurve.csv) - - **Description:** Regional biomass supply and costs by resource class - - **Dollar year:** 2015 ---- - - - [dollaryear.csv](/inputs/supply_curve/dollaryear.csv) ---- - - - [dr_shed_cap.csv](/inputs/supply_curve/dr_shed_cap.csv) ---- - - - [dr_shed_cost.csv](/inputs/supply_curve/dr_shed_cost.csv) ---- - - - [hyd_add_upg_cap.csv](/inputs/supply_curve/hyd_add_upg_cap.csv) ---- - - - [hydcap.csv](/inputs/supply_curve/hydcap.csv) ---- - - - [hydcost.csv](/inputs/supply_curve/hydcost.csv) ---- - - - [interconnection_land.h5](/inputs/supply_curve/interconnection_land.h5) ---- - - - [interconnection_offshore.h5](/inputs/supply_curve/interconnection_offshore.h5) ---- - - - [PSH_supply_curves_capacity_10hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_10hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_ref_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_10hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_10hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wEphemeral_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_10hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_10hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_10hr_wExist_wEph_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_12hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_12hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_ref_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_12hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_12hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wEphemeral_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_12hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_12hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_12hr_wExist_wEph_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_8hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_8hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_ref_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_8hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_8hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wEphemeral_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_8hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_8hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_capacity_8hr_wExist_wEph_mar2024.csv) - - **Description:** PSH supply curve capacity assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_10hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_apr2025.csv) ---- - - - [PSH_supply_curves_cost_10hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_ref_mar2024.csv) - - **Description:** PSH supply curve cost assuming 10 hour duration and reference exclusions as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_10hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_cost_10hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wEphemeral_mar2024.csv) - - **Description:** PSH supply curve cost assuming 10 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_10hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_apr2025.csv) ---- - - - [PSH_supply_curves_cost_10hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_mar2024.csv) - - **Description:** PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_10hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_cost_10hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_10hr_wExist_wEph_mar2024.csv) - - **Description:** PSH supply curve cost assuming 10 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_12hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_apr2025.csv) ---- - - - [PSH_supply_curves_cost_12hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_ref_mar2024.csv) - - **Description:** PSH supply curve cost assuming 12 hour duration and reference exclusions as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_12hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_cost_12hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wEphemeral_mar2024.csv) - - **Description:** PSH supply curve cost assuming 12 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_12hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_apr2025.csv) ---- - - - [PSH_supply_curves_cost_12hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_mar2024.csv) - - **Description:** PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_12hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_cost_12hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_12hr_wExist_wEph_mar2024.csv) - - **Description:** PSH supply curve cost assuming 12 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_8hr_ref_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_apr2025.csv) ---- - - - [PSH_supply_curves_cost_8hr_ref_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_ref_mar2024.csv) - - **Description:** PSH supply curve cost assuming 8 hour duration and reference exclusions as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_8hr_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_cost_8hr_wEphemeral_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wEphemeral_mar2024.csv) - - **Description:** PSH supply curve cost assuming 8 hour duration and allowing sites on ephemeral streams as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_8hr_wExist_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_apr2025.csv) ---- - - - [PSH_supply_curves_cost_8hr_wExist_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_mar2024.csv) - - **Description:** PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [PSH_supply_curves_cost_8hr_wExist_wEph_apr2025.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_apr2025.csv) ---- - - - [PSH_supply_curves_cost_8hr_wExist_wEph_mar2024.csv](/inputs/supply_curve/PSH_supply_curves_cost_8hr_wExist_wEph_mar2024.csv) - - **Description:** PSH supply curve cost assuming 8 hour duration and allowing sites using existing reservoirs and on ephemeral streams as used in 2024 Annual Technology Baseline - - **Dollar year:** 2004 - - **Citation:** [https://www.nrel.gov/gis/psh-supply-curves.html](https://www.nrel.gov/gis/psh-supply-curves.html) ---- - - - [rev_paths.csv](/inputs/supply_curve/rev_paths.csv) ---- - - - [sc_point_gid_old2new.csv](/inputs/supply_curve/sc_point_gid_old2new.csv) ---- - - - [sitemap.h5](/inputs/supply_curve/sitemap.h5) ---- - - - [supplycurve_egs-reference.csv](/inputs/supply_curve/supplycurve_egs-reference.csv) ---- - - - [supplycurve_upv-limited.csv](/inputs/supply_curve/supplycurve_upv-limited.csv) - - **Description:** UPV supply curve from reV for the limited siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) - - **Units:** capacity numbers are in MW_DC and cost numbers are in $/MW_AC - ---- - - - [supplycurve_upv-open.csv](/inputs/supply_curve/supplycurve_upv-open.csv) - - **Description:** UPV supply curve from reV for the open siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) - - **Units:** capacity numbers are in MW_DC and cost numbers are in $/MW_AC - ---- - - - [supplycurve_upv-reference.csv](/inputs/supply_curve/supplycurve_upv-reference.csv) - - **Description:** UPV supply curve from reV for the reference siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) - - **Units:** capacity numbers are in MW_DC and cost numbers are in $/MW_AC - ---- - - - [supplycurve_wind-ofs-limited.csv](/inputs/supply_curve/supplycurve_wind-ofs-limited.csv) - - **Description:** Offshore sind supply curve from reV for the limited siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) ---- - - - [supplycurve_wind-ofs-open.csv](/inputs/supply_curve/supplycurve_wind-ofs-open.csv) - - **Description:** Offshore wind supply curve from reV for the open siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) ---- - - - [supplycurve_wind-ofs-reference.csv](/inputs/supply_curve/supplycurve_wind-ofs-reference.csv) - - **Description:** Offshore wind supply curve from reV for the reference siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) ---- - - - [supplycurve_wind-ons-limited.csv](/inputs/supply_curve/supplycurve_wind-ons-limited.csv) - - **Description:** Land-based wind supply curve from reV for the limited siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) ---- - - - [supplycurve_wind-ons-open.csv](/inputs/supply_curve/supplycurve_wind-ons-open.csv) - - **Description:** Land-based wind supply curve from reV for the open siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) ---- - - - [supplycurve_wind-ons-reference.csv](/inputs/supply_curve/supplycurve_wind-ons-reference.csv) - - **Description:** Land-based wind supply curve from reV for the reference siting scenario - - **Dollar year:** specified in inputs/supply_curve/dollaryear.csv - - **Citation:** [https://docs.nrel.gov/docs/fy25osti/91900.pdf](https://docs.nrel.gov/docs/fy25osti/91900.pdf) ---- - - - [trans_intra_cost_adder.csv](/inputs/supply_curve/trans_intra_cost_adder.csv) ---- - - - -#### inputs/techs - - - [tech_resourceclass.csv](/inputs/techs/tech_resourceclass.csv) ---- - - - [techs_default.csv](/inputs/techs/techs_default.csv) - - **Description:** List of technologies to be used in the model ---- - - - [techs_subsetForTesting.csv](/inputs/techs/techs_subsetForTesting.csv) - - **Description:** Short list of technologies for testing ---- - - - -#### inputs/temporal - - - [month2quarter.csv](/inputs/temporal/month2quarter.csv) ---- - - - [period_szn_user.csv](/inputs/temporal/period_szn_user.csv) ---- - - - [reeds_region_tz_map.csv](/inputs/temporal/reeds_region_tz_map.csv) ---- - - - [stressperiods_user.csv](/inputs/temporal/stressperiods_user.csv) ---- - - - -#### inputs/transmission - - - [cost_hurdle_country.csv](/inputs/transmission/cost_hurdle_country.csv) - - **File Type:** GAMS set - - **Description:** Cost for transmission hurdle rate by country - - **Indices:** country - - **Dollar year:** 2004 ---- - - - [cost_hurdle_intra.csv](/inputs/transmission/cost_hurdle_intra.csv) ---- - - - [rev_transmission_basecost.csv](/inputs/transmission/rev_transmission_basecost.csv) - - **File Type:** inputs - - **Description:** Unweighted average base cost across the four regions for which we have transmission cost data. - - **Indices:** Transreg - - **Dollar year:** 2004 ---- - - - [transmission_capacity_future_ba_baseline.csv](/inputs/transmission/transmission_capacity_future_ba_baseline.csv) - - **File Type:** inputs - - **Description:** Future transmission capacity additions for the baseline case at BA resolution - - **Indices:** r,rr ---- - - - [transmission_capacity_future_ba_default.csv](/inputs/transmission/transmission_capacity_future_ba_default.csv) - - **File Type:** inputs - - **Description:** Future transmission capacity additions for the default case at BA resolution - - **Indices:** r,rr ---- - - - [transmission_capacity_future_ba_LCC_all.csv](/inputs/transmission/transmission_capacity_future_ba_LCC_all.csv) - - **File Type:** inputs - - **Description:** Future transmission capacity additions for the LCC_all case at BA resolution - - **Indices:** r,rr ---- - - - [transmission_capacity_future_ba_VSC_all.csv](/inputs/transmission/transmission_capacity_future_ba_VSC_all.csv) - - **File Type:** inputs - - **Description:** Future transmission capacity additions for the VSC_all_case at BA resolution - - **Indices:** r,rr ---- - - - [transmission_capacity_future_county_baseline.csv](/inputs/transmission/transmission_capacity_future_county_baseline.csv) - - **File Type:** inputs - - **Description:** Future transmission capacity additions for the baseline case at county resolution - - **Indices:** r,rr ---- - - - [transmission_capacity_future_county_default.csv](/inputs/transmission/transmission_capacity_future_county_default.csv) - - **File Type:** inputs - - **Description:** Future transmission capacity additions for the default case at county resolution - - **Indices:** r,rr ---- - - - [transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629.csv](/inputs/transmission/transmission_capacity_future_LCC_1000miles_demand1_wind1_subferc_20230629.csv) - - **File Type:** inputs - - **Description:** Future transmission capacity additions for the LCC_1000miles_demand1_wind1_subferc_20230629 case at BA resolution - - **Indices:** r,rr ---- - - - [transmission_capacity_init_AC_ba_NARIS2024.csv](/inputs/transmission/transmission_capacity_init_AC_ba_NARIS2024.csv) - - **Description:** Initial AC transmission capacity from the NARIS 2024 system at the BA resolution - 'NARIS2024' is a better starting point for future-oriented studies, but it becomes increasingly inaccurate for years earlier than 2024 ---- - - - [transmission_capacity_init_AC_ba_REFS2009.csv](/inputs/transmission/transmission_capacity_init_AC_ba_REFS2009.csv) - - **Description:** Initial AC transmission capacity from the 2009 transmission system for ReEDS at the BA resolution - 'REFS2009' does not include direction-dependent capacities or differentiated capacities for energy and PRM trading but it better represents historical additions between 2010-2024 ---- - - - [transmission_capacity_init_AC_county_NARIS2024.csv](/inputs/transmission/transmission_capacity_init_AC_county_NARIS2024.csv) - - **Description:** Initial AC transmission capacity modified from the NARIS 2024 file to eliminate most supply (with county transmission) demand mismatches for the 2024 solve year ---- - - - [transmission_capacity_init_AC_county_NARIS2024_base.csv](/inputs/transmission/transmission_capacity_init_AC_county_NARIS2024_base.csv) - - **Description:** Initial AC transmission capacity from the NARIS 2024 system at the county resolution ---- - - - [transmission_capacity_init_AC_transgrp_NARIS2024.csv](/inputs/transmission/transmission_capacity_init_AC_transgrp_NARIS2024.csv) - - **Description:** Initial AC transmission capacity from the NARIS 2024 system at the transgrp resolution ---- - - - [transmission_capacity_init_nonAC_ba.csv](/inputs/transmission/transmission_capacity_init_nonAC_ba.csv) - - **Description:** Initial non-AC transmission capacity at the BA resolution ---- - - - [transmission_capacity_init_nonAC_county.csv](/inputs/transmission/transmission_capacity_init_nonAC_county.csv) - - **Description:** Initial non-AC transmission capacity at the county resolution ---- - - - [transmission_cost_ac_500kv_ba.h5](/inputs/transmission/transmission_cost_ac_500kv_ba.h5) - - **Description:** Transmission costs for new 500 kV AC at BA resolution ---- - - - [transmission_cost_ac_500kv_county.h5](/inputs/transmission/transmission_cost_ac_500kv_county.h5) - - **Description:** Transmission costs for new 500 kV AC at county resolution ---- - - - [transmission_cost_dc_ba.csv](/inputs/transmission/transmission_cost_dc_ba.csv) - - **Description:** Transmission costs for new 500 kV DC at BA resolution ---- - - - [transmission_cost_dc_county.csv](/inputs/transmission/transmission_cost_dc_county.csv) - - **Description:** Transmission costs for new 500 kV DC at county resolution ---- - - - [transmission_distance_ba.h5](/inputs/transmission/transmission_distance_ba.h5) - - **Description:** Length of least-cost transmission paths between zones at BA resolution ---- - - - [transmission_distance_county.h5](/inputs/transmission/transmission_distance_county.h5) - - **Description:** Length of least-cost transmission paths between zones at county resolution ---- - - - -#### inputs/upgrades - - - [i_coolingtech_watersource_upgrades.csv](/inputs/upgrades/i_coolingtech_watersource_upgrades.csv) - - **File Type:** Inputs - - **Description:** List of cooling technologies for water sources that can be upgraded. - - **Indices:** i - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [i_coolingtech_watersource_upgrades_link.csv](/inputs/upgrades/i_coolingtech_watersource_upgrades_link.csv) - - **File Type:** Inputs - - **Description:** List of cooling technologies for water sources that can be upgraded + their to, from, ctt (cooling technology type) and wst (water source type) - - **Indices:** i, ctt, wst - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [upgrade_costs_ccs_coal.csv](/inputs/upgrades/upgrade_costs_ccs_coal.csv) ---- - - - [upgrade_costs_ccs_gas.csv](/inputs/upgrades/upgrade_costs_ccs_gas.csv) ---- - - - [upgrade_link.csv](/inputs/upgrades/upgrade_link.csv) - - **File Type:** Inputs - - **Description:** Techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta. - - **Indices:** i - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [upgrade_mult_atb23_ccs_adv.csv](/inputs/upgrades/upgrade_mult_atb23_ccs_adv.csv) - - **File Type:** Inputs - - **Description:** Cost adjustment (advanced) over various years for upgrade technologies - - **Indices:** i,t - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [upgrade_mult_atb23_ccs_con.csv](/inputs/upgrades/upgrade_mult_atb23_ccs_con.csv) - - **File Type:** Inputs - - **Description:** Cost adjustment (conservative) over various years for upgrade technologies - - **Indices:** i,t - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [upgrade_mult_atb23_ccs_mid.csv](/inputs/upgrades/upgrade_mult_atb23_ccs_mid.csv) - - **File Type:** Inputs - - **Description:** Cost adjustment (Mid) over various years for upgrade technologies - - **Indices:** i,t - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - [upgradelink_water.csv](/inputs/upgrades/upgradelink_water.csv) - - **File Type:** Inputs - - **Description:** Water techs that can be upgraded including the original technology, the technology it is upgrading to, and the delta - - **Indices:** i - - **Dollar year:** N/A - - **Citation:** N/A ---- - - - -#### inputs/userinput - - - [futurefiles.csv](/inputs/userinput/futurefiles.csv) ---- - - - [ivt_default.csv](/inputs/userinput/ivt_default.csv) ---- - - - [ivt_small.csv](/inputs/userinput/ivt_small.csv) ---- - - - [ivt_step.csv](/inputs/userinput/ivt_step.csv) - - **Description:** ivt steps for endyears beyond 2050 ---- - - - [modeled_regions.csv](/inputs/userinput/modeled_regions.csv) - - **Description:** Sets of BA regions that a user can model in a run. Each column is a different region option and can be specified in cases using GSw_Region. ---- - - - [windows_2100.csv](/inputs/userinput/windows_2100.csv) - - **Description:** Window size for using window solve method to 2100 ---- - - - [windows_default.csv](/inputs/userinput/windows_default.csv) - - **Description:** Window size for using window solve method ---- - - - [windows_step10.csv](/inputs/userinput/windows_step10.csv) - - **Description:** Window size for beyond2050step10 ---- - - - [windows_step5.csv](/inputs/userinput/windows_step5.csv) - - **Description:** Window size for beyond2050step5 ---- - - - -#### inputs/valuestreams - - - [var_map.csv](/inputs/valuestreams/var_map.csv) ---- - - - -#### inputs/waterclimate - - - [cost_cap_mult.csv](/inputs/waterclimate/cost_cap_mult.csv) ---- - - - [cost_vom_mult.csv](/inputs/waterclimate/cost_vom_mult.csv) ---- - - - [heat_rate_mult.csv](/inputs/waterclimate/heat_rate_mult.csv) ---- - - - [i_coolingtech_watersource.csv](/inputs/waterclimate/i_coolingtech_watersource.csv) ---- - - - [i_coolingtech_watersource_link.csv](/inputs/waterclimate/i_coolingtech_watersource_link.csv) ---- - - - [tg_rsc_cspagg_tmp.csv](/inputs/waterclimate/tg_rsc_cspagg_tmp.csv) ---- - - - [unapp_water_sea_distr.csv](/inputs/waterclimate/unapp_water_sea_distr.csv) ---- - - - [wat_access_cap_cost.csv](/inputs/waterclimate/wat_access_cap_cost.csv) ---- - - - [water_req_psh_10h_1_51.csv](/inputs/waterclimate/water_req_psh_10h_1_51.csv) ---- - - - [water_with_cons_rate.csv](/inputs/waterclimate/water_with_cons_rate.csv) ---- - - - -### postprocessing - - - [example.csv](/postprocessing/example.csv) ---- - - - -#### postprocessing/air_quality - - - [scenarios.csv](/postprocessing/air_quality/scenarios.csv) ---- - - - -##### postprocessing/air_quality/rcm_data - - - [counties_ACS_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/counties_ACS_high_stack_2017.csv) ---- - - - [counties_H6C_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/counties_H6C_high_stack_2017.csv) ---- - - - [states_ACS_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/states_ACS_high_stack_2017.csv) ---- - - - [states_H6C_high_stack_2017.csv](/postprocessing/air_quality/rcm_data/states_H6C_high_stack_2017.csv) ---- - - - -#### postprocessing/bokehpivot - - - [reeds_scenarios.csv](/postprocessing/bokehpivot/reeds_scenarios.csv) - - **Description:** Example data for ReEDS scenarios, each scenario with a custom style ---- - - - -##### postprocessing/bokehpivot/in - - - [example_custom_styles.csv](/postprocessing/bokehpivot/in/example_custom_styles.csv) - - **Description:** Examples of custom styles used for bokehpivot ---- - - - [example_data_US_electric_power_generation.csv](/postprocessing/bokehpivot/in/example_data_US_electric_power_generation.csv) - - **Description:** Example data for US electric power generation ---- - - - [gis_centroid_rb.csv](/postprocessing/bokehpivot/in/gis_centroid_rb.csv) ---- - - - [gis_nercr.csv](/postprocessing/bokehpivot/in/gis_nercr.csv) ---- - - - [gis_nercr_new.csv](/postprocessing/bokehpivot/in/gis_nercr_new.csv) ---- - - - [gis_rb.csv](/postprocessing/bokehpivot/in/gis_rb.csv) ---- - - - [gis_rs.csv](/postprocessing/bokehpivot/in/gis_rs.csv) ---- - - - [gis_rto.csv](/postprocessing/bokehpivot/in/gis_rto.csv) ---- - - - [gis_st.csv](/postprocessing/bokehpivot/in/gis_st.csv) ---- - - - [state_code.csv](/postprocessing/bokehpivot/in/state_code.csv) - - **Description:** Abbreviation and code for each state ---- - - - -###### postprocessing/bokehpivot/in/reeds2 - - - [class_map.csv](/postprocessing/bokehpivot/in/reeds2/class_map.csv) - - **Description:** Class mapping for bokehpivot postprocessing ---- - - - [class_style.csv](/postprocessing/bokehpivot/in/reeds2/class_style.csv) - - **Description:** Custom styles for classes in bokehpivot ---- - - - [con_adj_map.csv](/postprocessing/bokehpivot/in/reeds2/con_adj_map.csv) ---- - - - [con_adj_style.csv](/postprocessing/bokehpivot/in/reeds2/con_adj_style.csv) ---- - - - [cost_cat_map.csv](/postprocessing/bokehpivot/in/reeds2/cost_cat_map.csv) ---- - - - [cost_cat_style.csv](/postprocessing/bokehpivot/in/reeds2/cost_cat_style.csv) ---- - - - [ctt_map.csv](/postprocessing/bokehpivot/in/reeds2/ctt_map.csv) ---- - - - [ctt_style.csv](/postprocessing/bokehpivot/in/reeds2/ctt_style.csv) ---- - - - [hours.csv](/postprocessing/bokehpivot/in/reeds2/hours.csv) - - **Description:** Hours for each of the 17 timeslices ---- - - - [m_bar_width.csv](/postprocessing/bokehpivot/in/reeds2/m_bar_width.csv) ---- - - - [m_map.csv](/postprocessing/bokehpivot/in/reeds2/m_map.csv) ---- - - - [m_style.csv](/postprocessing/bokehpivot/in/reeds2/m_style.csv) ---- - - - [process_style.csv](/postprocessing/bokehpivot/in/reeds2/process_style.csv) ---- - - - [tech_ctt_wst.csv](/postprocessing/bokehpivot/in/reeds2/tech_ctt_wst.csv) ---- - - - [tech_map.csv](/postprocessing/bokehpivot/in/reeds2/tech_map.csv) ---- - - - [tech_style.csv](/postprocessing/bokehpivot/in/reeds2/tech_style.csv) - - **Description:** Custom colors for each technology used by bokehpivot ---- - - - [trtype_map.csv](/postprocessing/bokehpivot/in/reeds2/trtype_map.csv) ---- - - - [trtype_style.csv](/postprocessing/bokehpivot/in/reeds2/trtype_style.csv) ---- - - - [wst_map.csv](/postprocessing/bokehpivot/in/reeds2/wst_map.csv) ---- - - - [wst_style.csv](/postprocessing/bokehpivot/in/reeds2/wst_style.csv) ---- - - - -#### postprocessing/combine_runs - - - [combinefiles.csv](/postprocessing/combine_runs/combinefiles.csv) ---- - - - [runlist.csv](/postprocessing/combine_runs/runlist.csv) ---- - - - -#### postprocessing/land_use - - - -##### postprocessing/land_use/inputs - - - [federal_land_categories.csv](/postprocessing/land_use/inputs/federal_land_categories.csv) ---- - - - [field_definitions.csv](/postprocessing/land_use/inputs/field_definitions.csv) ---- - - - [nlcd_categories.csv](/postprocessing/land_use/inputs/nlcd_categories.csv) ---- - - - [nlcd_combined_categories.csv](/postprocessing/land_use/inputs/nlcd_combined_categories.csv) ---- - - - [usgs_categories.csv](/postprocessing/land_use/inputs/usgs_categories.csv) ---- - - - [usgs_combined_categories.csv](/postprocessing/land_use/inputs/usgs_combined_categories.csv) ---- - - - -#### postprocessing/plots - - - [scghg_annual.csv](/postprocessing/plots/scghg_annual.csv) ---- - - - [transmission-interface-coords.csv](/postprocessing/plots/transmission-interface-coords.csv) ---- - - - -#### postprocessing/retail_rate_module - - - [capital_financing_assumptions.csv](/postprocessing/retail_rate_module/capital_financing_assumptions.csv) ---- - - - [df_f861_contiguous.csv](/postprocessing/retail_rate_module/df_f861_contiguous.csv) ---- - - - [df_f861_state.csv](/postprocessing/retail_rate_module/df_f861_state.csv) ---- - - - [inputs.csv](/postprocessing/retail_rate_module/inputs.csv) ---- - - - [inputs_default.csv](/postprocessing/retail_rate_module/inputs_default.csv) ---- - - - [load_by_state_eia.csv](/postprocessing/retail_rate_module/load_by_state_eia.csv) - - **Description:** End use load by state since 1960 ---- - - - [map_i_to_tech.csv](/postprocessing/retail_rate_module/map_i_to_tech.csv) - - **Description:** Maps i to tech with custom coloring for each ---- - - - -##### postprocessing/retail_rate_module/calc_historical_capex - - - [existing_transmission_cost_bystate_USD2024.csv](/postprocessing/retail_rate_module/calc_historical_capex/existing_transmission_cost_bystate_USD2024.csv) ---- - - - -##### postprocessing/retail_rate_module/inputs - - - [Electric O & M Expenses-IOU-1993-2019.csv](/postprocessing/retail_rate_module/inputs/Electric%20O%20&%20M%20Expenses-IOU-1993-2019.csv) ---- - - - [Electric Operating Revenues-IOU-1993-2019.csv](/postprocessing/retail_rate_module/inputs/Electric%20Operating%20Revenues-IOU-1993-2019.csv) ---- - - - [Electric Plant in Service-IOU-1993-2019.csv](/postprocessing/retail_rate_module/inputs/Electric%20Plant%20in%20Service-IOU-1993-2019.csv) ---- - - - [f861_cust_counts.csv](/postprocessing/retail_rate_module/inputs/f861_cust_counts.csv) ---- - - - [overwrite-utility-energy_sales.csv](/postprocessing/retail_rate_module/inputs/overwrite-utility-energy_sales.csv) ---- - - - [state-meanbiaserror_rate-aggregation.csv](/postprocessing/retail_rate_module/inputs/state-meanbiaserror_rate-aggregation.csv) ---- - - - [Table_9.8_Average_Retail_Prices_of_Electricity.xlsx](/postprocessing/retail_rate_module/inputs/Table_9.8_Average_Retail_Prices_of_Electricity.xlsx) - - **Description:** Historical EIA861 rates (annual and monthly) ---- - - - -#### postprocessing/reValue - - - [scenarios.csv](/postprocessing/reValue/scenarios.csv) ---- - - - -#### postprocessing/tableau - - - [tables_to_aggregate.csv](/postprocessing/tableau/tables_to_aggregate.csv) ---- - - - -### preprocessing - - - -#### preprocessing/atb_updates_processing - - - -##### preprocessing/atb_updates_processing/input_files - - - [batt_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/batt_plant_char_format.csv) ---- - - - [conv_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/conv_plant_char_format.csv) ---- - - - [csp_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/csp_plant_char_format.csv) ---- - - - [geo_fom_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/geo_fom_plant_char_format.csv) ---- - - - [h2-combustion_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/h2-combustion_plant_char_format.csv) - - **Description:** Plant characteristics for which the H2-CC and CT ATB estimates are made using Gas-CC and CT data in preprocessing ---- - - - [ofs-wind_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/ofs-wind_plant_char_format.csv) ---- - - - [ofs-wind_rsc_mult_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/ofs-wind_rsc_mult_plant_char_format.csv) ---- - - - [ons-wind_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/ons-wind_plant_char_format.csv) ---- - - - [upv_plant_char_format.csv](/preprocessing/atb_updates_processing/input_files/upv_plant_char_format.csv) ---- - - - -### reeds2pras - - - -#### reeds2pras/test - - - -##### reeds2pras/test/reeds_cases - - - -###### reeds2pras/test/reeds_cases/test - - - [cases_reeds2pras.csv](/reeds2pras/test/reeds_cases/test/cases_reeds2pras.csv) ---- - - - [meta.csv](/reeds2pras/test/reeds_cases/test/meta.csv) ---- - - - -###### reeds2pras/test/reeds_cases/test/inputs_case - - - [hydcapadj.csv](/reeds2pras/test/reeds_cases/test/inputs_case/hydcapadj.csv) ---- - - - [hydcf.csv](/reeds2pras/test/reeds_cases/test/inputs_case/hydcf.csv) ---- - - - [mttr.csv](/reeds2pras/test/reeds_cases/test/inputs_case/mttr.csv) ---- - - - [outage_forced_hourly.h5](/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_hourly.h5) ---- - - - [outage_forced_static.csv](/reeds2pras/test/reeds_cases/test/inputs_case/outage_forced_static.csv) ---- - - - [outage_scheduled_hourly.h5](/reeds2pras/test/reeds_cases/test/inputs_case/outage_scheduled_hourly.h5) ---- - - - [resources.csv](/reeds2pras/test/reeds_cases/test/inputs_case/resources.csv) ---- - - - [tech-subset-table.csv](/reeds2pras/test/reeds_cases/test/inputs_case/tech-subset-table.csv) ---- - - - [unitdata.csv](/reeds2pras/test/reeds_cases/test/inputs_case/unitdata.csv) ---- - - - [unitsize.csv](/reeds2pras/test/reeds_cases/test/inputs_case/unitsize.csv) ---- - - - -###### reeds2pras/test/reeds_cases/test/ReEDS_Augur - - - -###### reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data - - - [cap_converter_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/cap_converter_2035.csv) ---- - - - [charge_eff_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/charge_eff_2035.csv) ---- - - - [discharge_eff_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/discharge_eff_2035.csv) ---- - - - [energy_cap_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/energy_cap_2035.csv) ---- - - - [max_cap_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_cap_2035.csv) ---- - - - [max_unitsize_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/max_unitsize_2035.csv) ---- - - - [pras_load_2035.h5](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_load_2035.h5) ---- - - - [pras_vre_gen_2035.h5](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/pras_vre_gen_2035.h5) ---- - - - [tran_cap_2035.csv](/reeds2pras/test/reeds_cases/test/ReEDS_Augur/augur_data/tran_cap_2035.csv) ---- - - - -### ReEDS_Augur - - - [augur_switches.csv](/ReEDS_Augur/augur_switches.csv) ---- - - - -### tests - - - -#### tests/data - - - -##### tests/data/county - - - [csp.h5](/tests/data/county/csp.h5) - - **Description:** Subset of county-level data for the github runner county test ---- - - - [distpv.h5](/tests/data/county/distpv.h5) - - **Description:** Subset of county-level data for the github runner county test ---- - - - [upv.h5](/tests/data/county/upv.h5) - - **Description:** Subset of county-level data for the github runner county test ---- - - - [wind-ofs.h5](/tests/data/county/wind-ofs.h5) - - **Description:** Subset of county-level data for the github runner county test ---- - - - [wind-ons.h5](/tests/data/county/wind-ons.h5) - - **Description:** Subset of county-level data for the github runner county test ---- - - -## Files - -- [cases.csv](/cases.csv) - - **File Type:** Switches file - - **Description:** Contains the configuration settings for the ReEDS run(s). - - **Dollar year:** 2004 - - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv](https://github.nrel.gov/ReEDS/ReEDS-2.0/blob/38e6610a8c6a92291804598c95c11b707bf187b9/cases.csv) ---- - -- [cases_examples.csv](/cases_examples.csv) ---- - -- [cases_small.csv](/cases_small.csv) - - **Description:** Contains settings to run ReEDS at a smaller scale to test operability of the ReEDS model. Turns off several technologies and reduces the model size to significantly improve solve times. ---- - -- [cases_standardscenarios.csv](/cases_standardscenarios.csv) - - **File Type:** StdScen Cases file - - **Description:** Contains the configuration settings for the Standard Scenarios ReEDS runs. ---- - -- [cases_test.csv](/cases_test.csv) - - **Description:** Contains the configuration settings for doing test runs including the default Pacific census division test case. ---- - -- [e_report_params.csv](/e_report_params.csv) - - **Description:** Contains a parameter list used in the model along with descriptions of what they are and units used. ---- - -- [runfiles.csv](/runfiles.csv) - - **Description:** Contains the locations of input data that is copied from the repository into the runs folder for each respective case. ---- - -- [sources.csv](/sources.csv) - - **Description:** CSV file containing a list of all input files (csv, h5, csv.gz) ---- diff --git a/srun_template.sh b/srun_template.sh deleted file mode 100644 index 4bebd071..00000000 --- a/srun_template.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -#SBATCH --account=[your HPC allocation] -#SBATCH --time=2-00:00:00 -#SBATCH --nodes=1 -#SBATCH --ntasks-per-node=1 -#SBATCH --mail-user=[your email address] -#SBATCH --mail-type=BEGIN,END,FAIL -#SBATCH --mem=246000 # RAM in MB; up to 246000 for normal or 2000000 for bigmem on kestrel -# add >>> #SBATCH --qos=high <<< above for quicker launch at double AU cost \ No newline at end of file diff --git a/tc_phaseout.py b/tc_phaseout.py deleted file mode 100644 index b89feeff..00000000 --- a/tc_phaseout.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Dec 2 16:36:48 2021 - -@author: pgagnon - -This script ingests historical CO2 direct combustion trends, determines if the -trigger for tax credit phasedown has been hit, and if so, outputs a tech-specific -adjustment to tax credit (both PTC and ITC) value. - -Based on the Inflation Reduction Act of 2022 - -""" -########### -#%% IMPORTS -import argparse -import pandas as pd -import numpy as np -import gdxpds -import os -import reeds - -########## -#%% INPUTS -use_historical = True - -############# -#%% FUNCTIONS -def calc_tc_phaseout_mult(year, case, use_historical=use_historical): - ''' - The TC phase down schedule starts the year after the trigger year. - GSw_TCPhaseout_start is the earliest allowed trigger year. - Example: If the conditions are met in 2033, - then the 0th value of the specified tc phaseout schedule applies in 2034 - - tc_phaseout_schedule: dataframe with ['n_yr_after_trigger', 'tc_phaseout_mult'] - Implicitly assumes that the tc value is zero after schedule is complete. - If tc phases to non-zero value, either enter that in the schedule or adjust code - ''' - # #%% Debugging - # year = 2035 - # case = os.path.expanduser('~/github2/ReEDS-2.0/runs/v20230305_reccM0_ref_seq') - - #%% Get switches - sw = reeds.io.get_switches(case) - GSw_TCPhaseout_trigger_f = float(sw.GSw_TCPhaseout_trigger_f) - GSw_TCPhaseout_ref_year = int(sw.GSw_TCPhaseout_ref_year) - GSw_TCPhaseout_start = int(sw.GSw_TCPhaseout_start) - GSw_TCPhaseout_forceyear = int(sw.GSw_TCPhaseout_forceyear) - startyear=int(sw.startyear) - - ### Set input/output path - tc_file_dir = os.path.join(case, 'outputs', 'tc_phaseout_data') - - # Import tech groups. Used to expand const_times - # (e.g., 'UPV' expands to all of the upv subclasses, like upv_1, upv_2, etc) - tech_groups = reeds.techs.import_tech_groups( - os.path.join(case, 'inputs_case', 'tech-subset-table.csv')) - - # The phasedown schedule is defined starting with the first year following the trigger year - # This schedule is for projects "commencing construction" - tc_phaseout = pd.read_csv(os.path.join(case, 'inputs_case', 'tc_phaseout_schedule.csv')) - - # The safe harbor window defines how long a project can be considered under construction. - # Note that even though we can specify incentive-level safe harbors in the inputs, we are - # calculating the single phaseout mult with the maximum safe harbor. This is an expedient for - # lack of time to create a phaseout for each incentive. - safe_harbors = pd.read_csv( - os.path.join(case, 'inputs_case', 'safe_harbor.csv') - ).rename(columns={'*i':'i', 't':'t_online'}) - - const_times = pd.read_csv( - os.path.join(case, 'inputs_case', 'construction_times.csv')) - - yearset = pd.read_csv( - os.path.join(case, 'inputs_case', 'modeledyears.csv') - ).columns.astype(int).values - - # Calc for all years that are covered by this modeled year, then avg the credit - if year==yearset.min(): - covered_years = [year] - else: - covered_years = np.arange(yearset[yearset GSw_TCPhaseout_start: - most_recent_year = max(yearset[yearset= int(sw['GSw_StartMarkets'])) - ].copy() - - # If at least one year fell below the trigger value, - # identify it and find each tech's tc_phaseout_mult - # OR if GSw_TCPhaseout_forceyear is nonzero, use it as the trigger year - if (len(df_qual) > 0) or GSw_TCPhaseout_forceyear: - if GSw_TCPhaseout_forceyear: - trigger_year = GSw_TCPhaseout_forceyear - else: - trigger_year = max([min(df_qual.index), GSw_TCPhaseout_start]) - - print(f'<><><> IRA tax credits start phasing out in {trigger_year} <><><>') - - const_times['n_yr_after_trigger'] = const_times['t_start_build'] - trigger_year - - const_times = const_times.merge( - tc_phaseout[['n_yr_after_trigger', 'tc_phaseout_mult']], - on='n_yr_after_trigger', how='left') - - const_times['tc_phaseout_mult'] = np.where( - const_times['n_yr_after_trigger']<=0, - 1.0, - const_times['tc_phaseout_mult']) - const_times['tc_phaseout_mult'] = np.where( - const_times['n_yr_after_trigger']>tc_phaseout['n_yr_after_trigger'].max(), - 0.0, - const_times['tc_phaseout_mult']) - - tc_phaseout_mult = ( - const_times[['i', 'tc_phaseout_mult']] - .groupby('i', as_index=False).mean() - ) - - # If no years fell below the trigger value, tc phaseout has not begun, - # so just set tc_phaseout_mult to 1.0 for all techs - else: - tc_phaseout_mult = const_times[['i']].copy() - tc_phaseout_mult['tc_phaseout_mult'] = 1.0 - - # If the first allowable trigger year has not yet been reached, tc phaseout has not begun, - # so just set tc_phaseout_mult to 1.0 for all techs - else: - tc_phaseout_mult = const_times[['i']].copy() - tc_phaseout_mult['tc_phaseout_mult'] = 1.0 - - # Round for GAMS - tc_phaseout_mult['tc_phaseout_mult'] = np.round(tc_phaseout_mult['tc_phaseout_mult'], 3) - tc_phaseout_mult['t'] = year - - data = {'tc_phaseout_mult_t':tc_phaseout_mult[['i', 't', 'tc_phaseout_mult']]} - gdxpds.to_gdx(data, os.path.join(tc_file_dir, 'tc_phaseout_mult_%s.gdx' % year)) - - -############# -#%% PROCEDURE -if __name__ == '__main__': - - parser = argparse.ArgumentParser(description="""Running tc_phaseout.py""") - parser.add_argument("year", help="ReEDS solve year", type=int) - parser.add_argument("case", help="filepath for ReEDS case") - args = parser.parse_args() - year = args.year - case = args.case - - ### Set up logger - log = reeds.log.makelog( - scriptname=__file__, - logpath=os.path.join(case,'gamslog.txt'), - ) - - print(f'starting tc_phaseout.py for {year}') - calc_tc_phaseout_mult(year, case, use_historical=use_historical) - print(f'finished tc_phaseout.py for {year}') diff --git a/valuestreams.py b/valuestreams.py deleted file mode 100644 index ef246976..00000000 --- a/valuestreams.py +++ /dev/null @@ -1,85 +0,0 @@ -import sys -import os -import pandas as pd -import raw_value_streams as rvs -from datetime import datetime -import logging - -sys.stdout = open('gamslog.txt', 'a') -sys.stderr = open('gamslog.txt', 'a') - -this_dir_path = os.path.dirname(os.path.realpath(__file__)) -vs_path = this_dir_path + '/inputs_case' -output_dir = this_dir_path + '/outputs' -solution_file = this_dir_path + '/ReEDSmodel_p.gdx' -problem_file = this_dir_path + '/ReEDSmodel_jacobian.gdx' -# problem_file = this_dir_path + '/ReEDSmodel.mps' - -logger = logging.getLogger('') -logger.setLevel(logging.DEBUG) -sh = logging.StreamHandler(sys.stdout) -sh.setLevel(logging.DEBUG) -formatter = logging.Formatter('%(asctime)s - %(message)s') -sh.setFormatter(formatter) -logger.addHandler(sh) - -df_var_map = pd.read_csv(vs_path+'/var_map.csv', dtype=object) -var_list = df_var_map['var_name'].values.tolist() - -#common function for outputting to csv -def add_concat_csv(df_in, csv_file): - df_in['t'] = pd.to_numeric(df_in['t']) - if not os.path.exists(csv_file): - df_in.to_csv(csv_file,index=False) - else: - df_csv = pd.read_csv(csv_file) - df_out = pd.concat([df_csv, df_in], ignore_index=True, sort=False) - df_out.to_csv(csv_file,index=False) - -def createValueStreams(): - very_start = datetime.now() - logger.info('Starting valuestreams.py') - df = rvs.get_value_streams(solution_file, problem_file, var_list) - logger.info('Raw value streams completed: ' + str(datetime.now() - very_start)) - - df = pd.merge(left=df, right=df_var_map, on='var_name', how='inner') - - #Chosen plants (with nonzero levels in solution) - start = datetime.now() - df_lev = df[df['var_level'] != 0].copy() - #convert to list of lists for speed - df_lev_ls = df_lev.values.tolist() - cols = df_lev.columns.values.tolist() - ci = {c:i for i,c in enumerate(cols)} - #Use iterrows or itertuples or somthing faster? iterrows is most convenient so if this isn't a bottleneck, use it. - replace_cols = ['i','v','r','t'] - for i, r in enumerate(df_lev_ls): - var_set_ls = r[ci['var_set']].split('.') - for c in replace_cols: - if str(r[ci[c]]).isdigit(): - df_lev_ls[i][ci[c]] = var_set_ls[int(r[ci[c]])] - #convert back to pandas dataframe - df_lev = pd.DataFrame(df_lev_ls) - df_lev.columns = cols - - #Fill missing values with 'none' - out_sets = ['i','v','r','t','var_name','con_name'] - df_lev[out_sets] = df_lev[out_sets].fillna(value='none') - - #Reduce df_lev to columns of interest and groupby sum - out_cols = out_sets + ['value'] - df_lev = df_lev[out_cols] - df_lev = df_lev.groupby(out_sets, sort=False, as_index =False).sum() - - add_concat_csv(df_lev.copy(), output_dir + '/valuestreams_chosen.csv') - logger.info('Levels output: ' + str(datetime.now() - start)) - logger.info('Done with years: ' + str(df_lev['t'].unique().tolist())) - logger.info('Finished valuestreams.py. Total time: ' + str(datetime.now() - very_start)) - -if __name__ == '__main__': - createValueStreams() - x_files = [problem_file.replace('.gdx',f'_{x}.csv') for x in ['i','j']] - files = x_files + [solution_file, problem_file] - for f in files: - if os.path.exists(f): - os.remove(f) \ No newline at end of file From 873eb2f02619e8351bf960388047ca249851a64c Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:41:21 -0600 Subject: [PATCH 04/34] rename ReEDS_Augur -> handoff, augur_data -> reeds_data, ReEDS_Augur_{year}.gdx -> ccdata_{year}.gdx, Augur (in general) -> resource adequacy --- cases.csv | 4 +- cases_small.csv | 2 +- docs/source/user_guide.md | 39 +---------- .../bokehpivot/in/reeds2/process_style.csv | 6 +- postprocessing/bokehpivot/reeds2.py | 10 +-- postprocessing/cleanup_files.py | 3 +- postprocessing/reValue/reValue.py | 32 ++++----- postprocessing/run_pcm.py | 2 +- postprocessing/run_reeds2pras.py | 13 ++-- reeds/core/setup/b_inputs.gms | 4 +- reeds/core/setup/e_solveprep.gms | 2 +- reeds/core/solve/3_solve_allyears.gms | 6 +- reeds/core/solve/3_solve_oneyear.gms | 5 +- reeds/core/solve/3_solve_window.gms | 4 +- reeds/core/solve/6_data_dump.gms | 10 +-- reeds/core/solve/solve.py | 28 ++++---- .../hourly_writetimeseries.py | 4 +- reeds/input_processing/recf.py | 2 +- reeds/io.py | 14 ++-- reeds/parse.py | 2 +- reeds/prasplots.py | 4 +- reeds/reedsplots.py | 12 ++-- reeds/resource_adequacy/__init__.py | 2 +- reeds/resource_adequacy/capacity_credit.py | 21 +++--- reeds/resource_adequacy/diagnostic_plots.py | 69 +++++++++---------- reeds/resource_adequacy/prep_data.py | 14 ++-- reeds/resource_adequacy/ra.py | 4 +- .../{Augur.py => ra_calcs.py} | 25 +++---- .../{augur_switches.csv => ra_switches.csv} | 2 +- reeds/resource_adequacy/reeds2pras/README.md | 18 +++-- .../src/utils/reeds_data_parsing.jl | 6 +- .../src/utils/reeds_input_parsing.jl | 36 +++++----- .../reeds2pras/src/utils/runchecks.jl | 20 +++--- reeds/resource_adequacy/run_pras.jl | 6 +- reeds/resource_adequacy/stress_periods.py | 22 +++--- run.py | 58 ++++++++-------- 36 files changed, 237 insertions(+), 274 deletions(-) rename reeds/resource_adequacy/{Augur.py => ra_calcs.py} (90%) rename reeds/resource_adequacy/{augur_switches.csv => ra_switches.csv} (89%) diff --git a/cases.csv b/cases.csv index 5a99444a..520c2020 100644 --- a/cases.csv +++ b/cases.csv @@ -295,7 +295,7 @@ GSw_SitingGeo,Specify the Geothermal siting scenario,reference,reference, GSw_SitingUPV,Specify the UPV siting scenario,open; reference; limited,reference, GSw_SitingWindOfs,Specify the offshore wind siting scenario,open; reference; limited,reference, GSw_SitingWindOns,Specify the onshore wind siting scenario,open; reference; limited,reference, -GSw_SkipAugurYear,Last year in which to skip running the Augur module,N/A,2020, +GSw_SkipRAyear,Last year in which to skip running the resource adequacy (RA) calculations,N/A,2020, GSw_SpurCostMult,Multiplier for spur-line costs,float,1, GSw_SpurScen,Spur-line scenario: 0 to include in resource supply curve; 1 to model endoegenously,0; 1,0, GSw_SpurShare,Indicate whether wind-ons and upv are allowed to share spur line capacity,0; 1,0, @@ -356,7 +356,7 @@ diagnose_year,Year in which to start report diagnose,N/A,2022, dump_alldata,switch to automatically dump data from final solve year into .gdx file,0; 1,0, file_replacements,"List of files to replace from run folder, e.g: inputs_case/national_gen_frac.csv << //nrelnas01/ReEDS/some proj/national_gen_frac.csv || c_supplymodel.gms << //nrelnas01/ReEDS/some proj/c_supplymodel.gms",N/A,none, input_processing_only,Only run input-processing scripts; stop before creating and solving model,0; 1,0, -keep_augur_files,Indicate whether to keep (1) or delete (0) Augur csv and h5 files after Augur finishes. If you plan to run PRAS via ReEDS2PRAS then set this switch to 1.,0; 1,0, +keep_resource_adequacy_files,Indicate whether to keep (1) or delete (0) resource adequacy csv and h5 files after RA calculations finish,0; 1,0, keep_g00_files,Keep (1) or delete (0) .g00 files for completed solve years,0; 1,0, keep_run_terminal,"0=close run terminal, 1=keep run terminal open",0; 1,0, land_use_analysis,switch to turn on/off land-use analysis. Requires the `reeds_to_rev` switch also be activated,0; 1,0, diff --git a/cases_small.csv b/cases_small.csv index 14ec1461..c4812e97 100644 --- a/cases_small.csv +++ b/cases_small.csv @@ -27,7 +27,7 @@ GSw_Refurb,0 GSw_PRM_CapCredit,0 GSw_Retire,0 GSw_RGGI,0 -GSw_SkipAugurYear,2030 +GSw_SkipRAyear,2030 GSw_StateCap,0 GSw_StateRPS,0 GSw_Storage,0 diff --git a/docs/source/user_guide.md b/docs/source/user_guide.md index dd5483ea..8860b1c5 100644 --- a/docs/source/user_guide.md +++ b/docs/source/user_guide.md @@ -71,38 +71,6 @@ Here is partial list of remotely hosted files used by ReEDS: -## Hourly Resolution - -The model can be run at hourly resolution using the following switch settings: - -- `GSw_Hourly = 1` - - Turn on hourly resolution -- `GSw_Canada = 2` - - Turn on hourly resolution for Canadian imports/exports -- `GSw_AugurCurtailment = 0` - - Turn off the Augur calculation of curtailment -- `GSw_StorageArbitrageMult = 0` - - Turn off the Augur calculation of storage arbitrage value -- `GSw_Storage_in_Min = 0` - - Turn off the Augur calculation of storage charging -- `capcredit_szn_hours = 3` - - The current default hourly representation is 18 representative 5-day weeks. Each representative period is treated as a 'season' and is thus active in the planning-reserve margin constraint. In h17 ReEDS we set `capcredit_szn_hours = 10`, giving 40 total hours considered for planning reserves (the top 10 hours in each of the 4 quarterly seasons). 18 'seasons' with 10 hours each would give 180 hours, so we switch to 3 hours per 'season' (for 54 hours total). - -To further reduce solve time, you can make the following changes: - -- `yearset = 2010_2015_2020_2025_2030_2035_2040_2045_2050` - - Solve in 5-year steps -- `GSw_OpRes = 0` - - Turn off operating reserves -- `GSw_MinLoading = 0` - - Turn off the sliding-window representation of minimum-generation limits -- `GSw_PVB = 0` - - Turn off PV-battery hybrids -- `GSw_calc_powfrac = 0` - - Turn off a post-processing calculation of power flows - - - ## Electricity Demand Profiles ### Switch options for GSw_LoadProfiles @@ -717,7 +685,6 @@ This section provides guidance on identifying and resolving common issues encoun - What to look for: - `1_inputs.lst`: errors will be preceded by `****` - `{batch_prefix}_{case}_{year}i0.lst`: there should be one file for each year of the model run - - `Augur_errors_{year}`: this file will appear in the event that there is an augur-related issue - GAMS Workfiles - Path: `/runs/{batch_prefix}_{case}/g00files/` @@ -733,10 +700,10 @@ This section provides guidance on identifying and resolving common issues encoun - these files should contain data, an error message "GDX file not found" indicates an issue with the reporting script at the end of the model - `reeds-report/` and `reeds-report-reduced/`: if these folders are not present, it can indicate a problem with the post-processing scripts -- Augur Data - - Path: `/runs/{batch_prefix}_{case}/ReEDS_Augur/augur_data/` +- Resource adequacy data + - Path: `/runs/{batch_prefix}_{case}/handoff/reeds_data/` - What to look for: - - `ReEDS_Augur_{year}.gdx`: there should be a file for each year of the model run = + - `ccdata_{year}.gdx`: there should be a file for each year of the model run = - `reeds_data_{year}.gdx`: there should be a file for each year of the model run - Case Inputs diff --git a/postprocessing/bokehpivot/in/reeds2/process_style.csv b/postprocessing/bokehpivot/in/reeds2/process_style.csv index 77320f5c..7ef7c82a 100644 --- a/postprocessing/bokehpivot/in/reeds2/process_style.csv +++ b/postprocessing/bokehpivot/in/reeds2/process_style.csv @@ -20,9 +20,9 @@ d_solveoneyear.gms,#843C39 solver/barrier,#AD494A solver/crossover,#D6616B solver/remainder,#E7969C -reeds_augur/prep_data.py,#3182BD -reeds_augur/stress_periods.py,#6BAED6 -reeds_augur/capacity_credit.py,#9ECAE1 +ra/prep_data.py,#3182BD +ra/stress_periods.py,#6BAED6 +ra/capacity_credit.py,#9ECAE1 reeds2pras,#E6550D pras,#FD8D3C report.gms,#74C476 diff --git a/postprocessing/bokehpivot/reeds2.py b/postprocessing/bokehpivot/reeds2.py index 7d8d0da4..28d3a237 100644 --- a/postprocessing/bokehpivot/reeds2.py +++ b/postprocessing/bokehpivot/reeds2.py @@ -1068,14 +1068,14 @@ def rgba2hex(rgba): def pre_runtime(dictin, **kw): """ ### Use the code below to redefine the colormap when new scripts are added - augur_start = df.index.tolist().index('ReEDS_Augur/prep_data.py') + ra_start = df.index.tolist().index('ra/prep_data.py') for i, row in enumerate(df.index): - if i < augur_start: + if i < ra_start: colors[row.lower()] = rgba2hex(plt.cm.tab20b(i)) - elif i < augur_start + 20: - colors[row.lower()] = rgba2hex(plt.cm.tab20c(i-augur_start)) + elif i < ra_start + 20: + colors[row.lower()] = rgba2hex(plt.cm.tab20c(i-ra_start)) else: - colors[row.lower()] = rgba2hex(plt.cm.tab20(i-augur_start-20)) + colors[row.lower()] = rgba2hex(plt.cm.tab20(i-ra_start-20)) """ df = dictin['runtime'].copy() diff --git a/postprocessing/cleanup_files.py b/postprocessing/cleanup_files.py index 1eb425cc..40480d9e 100644 --- a/postprocessing/cleanup_files.py +++ b/postprocessing/cleanup_files.py @@ -43,14 +43,13 @@ ], ## Large output files. Can be regenerated without rerunning the case. 3: [ - os.path.join('outputs', 'Augur_plots'), os.path.join('outputs', 'hourly'), os.path.join('outputs', 'figures'), ], ## Largest output files. Would need to rerun the case to regenerate. 4: [ 'g00files', - 'ReEDS_Augur', + 'handoff', ## The following regex matches the rep_{casename}.gdx file written by ## report.gms (which contains the same data as outputs.h5) os.path.join('outputs', '^rep_.*\.gdx$'), diff --git a/postprocessing/reValue/reValue.py b/postprocessing/reValue/reValue.py index b7062584..064376b2 100644 --- a/postprocessing/reValue/reValue.py +++ b/postprocessing/reValue/reValue.py @@ -75,39 +75,39 @@ def get_prices(): df_pq_rm_adj = df_pq_rm.rename(columns={'h':'season'}) df_seas_h_map = df_hmap[['season','h']].drop_duplicates() if res_marg_style == 'max_net_load_2012': - #Read in net_load_2012 of the appropriate ReEDS Augur file. + #Read in net_load_2012 of the appropriate capacity credit file. #The file we want is for the highest year that is lower than r['year'] #TODO: Perhaps we should use the file that has the same year as r['year'], #depending on if it represents the system of that year better. - aug_files = os.listdir(f'{reeds_run_path}/ReEDS_Augur/augur_data') - aug_file_yrs = [f for f in aug_files if 'ReEDS_Augur_' in f] - yrs = [int(f.replace('ReEDS_Augur_','').replace('.gdx','')) for f in aug_file_yrs] + aug_files = os.listdir(f'{reeds_run_path}/handoff/reeds_data') + aug_file_yrs = [f for f in aug_files if 'ccdata_' in f] + yrs = [int(f.replace('ccdata_','').replace('.gdx','')) for f in aug_file_yrs] yrs_less = [y for y in yrs if y < year] max_yr = max(yrs_less) - df_aug = gdxpds.to_dataframe(f'{reeds_run_path}/ReEDS_Augur/augur_data/ReEDS_Augur_{max_yr}.gdx', + df_ra = gdxpds.to_dataframe(f'{reeds_run_path}/handoff/reeds_data/ccdata_{max_yr}.gdx', 'net_load_2012', old_interface=False) - if int(df_aug['t'][0]) != year: - sys.exit(f'ERROR: Augur year ({int(df_aug["t"][0])}) does not match current scenario year ({year})') - df_aug = df_aug.sort_values('Value', ascending=False) - df_aug_top = df_aug.groupby(['ccreg','ccseason'], as_index=False).head(netload_num_hrs).copy() - df_aug_top = df_aug_top.rename(columns={'ccseason':'season'}) + if int(df_ra['t'][0]) != year: + raise ValueError(f'RA year ({int(df_ra["t"][0])}) does not match current scenario year ({year})') + df_ra = df_ra.sort_values('Value', ascending=False) + df_ra_top = df_ra.groupby(['ccreg','ccseason'], as_index=False).head(netload_num_hrs).copy() + df_ra_top = df_ra_top.rename(columns={'ccseason':'season'}) if netload_time_style == 'hour': - df_aug_top = df_aug_top[['ccreg','season','hour']].copy() - df_aug_top['hour'] = df_aug_top['hour'].astype(int) + df_ra_top = df_ra_top[['ccreg','season','hour']].copy() + df_ra_top['hour'] = df_ra_top['hour'].astype(int) #Convert the seasonal price to an hourly price over the set of hours assigned to that season (for that ba) df_pq_rm_adj['price'] = df_pq_rm_adj['price'] / netload_num_hrs #Restrict to prices only and add ccreg column df_p_rm = df_pq_rm_adj[['reeds_ba','season','price']].merge(df_ba_cc_map, on='reeds_ba', how='left') #Merge max net load hours into df_p_rm (which will duplicate rows if there are multiple timeslices in a ba/season) - df_p_rm = df_p_rm.merge(df_aug_top, on=['ccreg','season'], how='left') + df_p_rm = df_p_rm.merge(df_ra_top, on=['ccreg','season'], how='left') #Hour is one-indexed. Re-index to the 2012 set of 8760 hours (43801 to 52560) df_p_rm_h = df_p_rm.pivot_table(index=['hour'], columns='reeds_ba', values='price') df_p_rm_h = df_p_rm_h.reindex(range(43801,52561)).reset_index(drop=True) elif netload_time_style == 'timeslice': #We assign reserve margin prices to the entire timeslice(s) that contain the top net load hour(s) of each season - df_aug_top = df_aug_top[['ccreg','season','h']].drop_duplicates() + df_ra_top = df_ra_top[['ccreg','season','h']].drop_duplicates() #Find number of total hours that we're mapping prices to in each season - df_seas_hrs = df_aug_top.merge(df_h_num_hrs, on=['h'], how='left') + df_seas_hrs = df_ra_top.merge(df_h_num_hrs, on=['h'], how='left') df_seas_hrs = df_seas_hrs.groupby(['ccreg','season'], as_index=False)['num_hrs'].sum() #Add ccreg and num_hrs columns to df_pq_rm_adj df_pq_rm_adj = df_pq_rm_adj.merge(df_ba_cc_map, on='reeds_ba', how='left') @@ -116,7 +116,7 @@ def get_prices(): df_pq_rm_adj['price'] = df_pq_rm_adj['price'] / df_pq_rm_adj['num_hrs'] #Merge in the timeslices to which we'll be mapping prices. This might duplicate rows if there are #multiple timeslices in a ba/season (which is possible if netload_num_hrs is greater than 1) - df_pq_rm_adj = df_pq_rm_adj.merge(df_aug_top, on=['ccreg','season'], how='left') + df_pq_rm_adj = df_pq_rm_adj.merge(df_ra_top, on=['ccreg','season'], how='left') #Isolate prices with h as first column and reeds_ba as other columns df_p_rm = df_pq_rm_adj.pivot_table(index=['h'], columns='reeds_ba', values='price').reset_index() #Merge with df_hmap, duplicating prices across all hours of the chosen timeslices. diff --git a/postprocessing/run_pcm.py b/postprocessing/run_pcm.py index 1ad0f93f..b95ab40c 100644 --- a/postprocessing/run_pcm.py +++ b/postprocessing/run_pcm.py @@ -78,7 +78,7 @@ def solvestring_pcm( [ f" --{s}={sw[s]}" for s in [ - 'GSw_SkipAugurYear', + 'GSw_SkipRAyear', 'GSw_HourlyType', 'GSw_HourlyWrapLevel', 'GSw_ClimateWater', diff --git a/postprocessing/run_reeds2pras.py b/postprocessing/run_reeds2pras.py index 9d3006b8..95b7372d 100644 --- a/postprocessing/run_reeds2pras.py +++ b/postprocessing/run_reeds2pras.py @@ -99,7 +99,6 @@ def main( Run prep_data, ReEDS2PRAS, and PRAS as necessary. If running PRAS, append the number of samples to the filename. """ - ### Import Augur scripts if repo: site.addsitedir(reeds_path) else: @@ -132,7 +131,7 @@ def main( print(f'Running PRAS for {t}i{iteration}') ### Check if prep_data.py outputs exist; if not, run it - augur_data = os.path.join(case,'ReEDS_Augur','augur_data') + reeds_data = os.path.join(case,'handoff','reeds_data') files_expected = [ f'cap_converter_{t}.csv', f'energy_cap_{t}.csv', @@ -142,13 +141,13 @@ def main( f'pras_vre_gen_{t}.h5', ] if ( - any([not os.path.isfile(os.path.join(augur_data,f)) for f in files_expected]) + any([not os.path.isfile(os.path.join(reeds_data,f)) for f in files_expected]) or overwrite ): - augur_csv, augur_h5 = reeds.resource_adequacy.prep_data.main(t, case) + reeds_csv, reeds_h5 = reeds.resource_adequacy.prep_data.main(t, case) ### Run ReEDS2PRAS - reeds.resource_adequacy.Augur.run_pras( + reeds.resource_adequacy.ra_calcs.run_pras( case, t, iteration=iteration, @@ -187,7 +186,7 @@ def main( parser.add_argument('--samples', '-s', type=int, default=0, help='PRAS samples to run') parser.add_argument('--repo', '-r', action='store_true', - help=('Import Augur scripts from local repo ' + help=('Import RA scripts from local repo ' '(instead of from the case being rerun)')) parser.add_argument('--local', '-l', action='store_true', help='Run locally (not as SLURM job)') @@ -205,7 +204,7 @@ def main( help="Write hourly unit availability by sample from PRAS") parser.add_argument('--switch_mods', '-m', type=json.loads, default=json.dumps({}), help=('Dictionary-formated string of switch arguments for ' - 'Augur.run_pras(). Use single quotes outside the dictionary and ' + 'ra_calcs.run_pras(). Use single quotes outside the dictionary and ' 'double quotes for keys, as in:\n' '`-s \'{"pras_seed":0}\'`')) diff --git a/reeds/core/setup/b_inputs.gms b/reeds/core/setup/b_inputs.gms index 2aa9316e..4ad01569 100644 --- a/reeds/core/setup/b_inputs.gms +++ b/reeds/core/setup/b_inputs.gms @@ -1425,7 +1425,7 @@ ivt(i,newv,t)$[ord(newv) = ivt_num(i,t)] = yes ; ivt(i,v,t)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), ivt(ii,v,t) } ; -*Also expand ivt_num to water techs for use in Augur +*Also expand ivt_num to water techs for use in resource adequacy calculations ivt_num(i,t)$[i_water_cooling(i)$Sw_WaterMain] = sum{ii$ctt_i_ii(i,ii), ivt_num(ii,t) } ; @@ -6111,7 +6111,7 @@ scalar bio_transport_cost ; * biomass transport cost enter in $ per ton, convert to $ per MMBtu bio_transport_cost = Sw_BioTransportCost / bio_energy_content ; -* get price of cheapest supply curve bin that has resources (needed for Augur) +* get price of cheapest supply curve bin that has resources (needed for resource adequacy calculations) * price includes any transport costs for biomass parameter rep_bio_price_unused(r) "--2004$/MWh-- marginal price of lowest cost available supply curve bin for biofuel" ; rep_bio_price_unused(r)$[sum{usda_region, 1$r_usda(r,usda_region) }] = diff --git a/reeds/core/setup/e_solveprep.gms b/reeds/core/setup/e_solveprep.gms index dbaa6df9..1d30682b 100644 --- a/reeds/core/setup/e_solveprep.gms +++ b/reeds/core/setup/e_solveprep.gms @@ -135,7 +135,7 @@ $endif.seq $ifthen.intwin ((%timetype%=="int") or (%timetype%=="win")) set - loadset "set used for loading in merged gdx files" / ReEDS_Augur_%startyear%*ReEDS_Augur_%endyear% / + loadset "set used for loading in merged gdx files" / ccdata_%startyear%*ccdata_%endyear% / ; parameter diff --git a/reeds/core/solve/3_solve_allyears.gms b/reeds/core/solve/3_solve_allyears.gms index 204d1bc5..e06d360b 100644 --- a/reeds/core/solve/3_solve_allyears.gms +++ b/reeds/core/solve/3_solve_allyears.gms @@ -37,7 +37,7 @@ $endif.loadref *indicate we're loading data tload(t)$tmodel(t) = yes ; -$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_merged_%niter%.gdx +$gdxin handoff%ds%reeds_data%ds%ccdata_merged_%niter%.gdx $loaddcr cc_old_load2 = cc_old $loaddcr cc_mar_load2 = cc_mar $loaddcr cc_evmc_load2 = cc_evmc @@ -120,7 +120,7 @@ cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) < 0.001] = 0 ; cc_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) ; -execute_unload 'ReEDS_Augur%ds%augur_data%ds%curtout_%case%_%niter%.gdx' cc_int ; +execute_unload 'handoff%ds%reeds_data%ds%curtout_%case%_%niter%.gdx' cc_int ; *following line will load in the level values if the switch is enabled *note that this is still within the conditional that we are now past the first iteration @@ -162,4 +162,4 @@ gen_iter(i,v,r,t,"%niter%")$valcap(i,v,r,t) = sum{h, GEN.l(i,v,r,h,t) * hours(h) gen_iter(i,v,r,t,"%niter%")$[vre(i)$valcap(i,v,r,t)] = sum{h, m_cf(i,v,r,h,t) * CAP.l(i,v,r,t) * hours(h) } ; cap_firm_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) * CAP.l(i,v,r,t) ; cap_firm_iter(i,v,r,szn,t,"%niter%")$storage(i) = sum{sdbin, CAP_SDBIN.l(i,v,r,szn,sdbin,t) * cc_storage(i,sdbin) } ; -cap_energy_firm_iter(i,v,r,szn,t,"%niter%")$storage(i) = sum{sdbin, CAP_SDBIN_ENERGY.l(i,v,r,szn,sdbin,t) } ; \ No newline at end of file +cap_energy_firm_iter(i,v,r,szn,t,"%niter%")$storage(i) = sum{sdbin, CAP_SDBIN_ENERGY.l(i,v,r,szn,sdbin,t) } ; diff --git a/reeds/core/solve/3_solve_oneyear.gms b/reeds/core/solve/3_solve_oneyear.gms index 3a4ff1cc..797c3ab0 100644 --- a/reeds/core/solve/3_solve_oneyear.gms +++ b/reeds/core/solve/3_solve_oneyear.gms @@ -136,14 +136,13 @@ if(Sw_GrowthPenalties > 0, $endif.post_startyear * Load capacity credit results -$ifthene.tcheck %cur_year%>%GSw_SkipAugurYear% +$ifthene.tcheck %cur_year%>%GSw_SkipRAyear% *indicate we're loading data tload("%cur_year%") = yes ; -*file written by ReEDS_Augur.py * loaddcr = domain check (dc) + overwrite values storage previously (r) -$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_%prev_year%.gdx +$gdxin handoff%ds%reeds_data%ds%ccdata_%prev_year%.gdx $loaddcr cc_old_load = cc_old $loaddcr cc_mar_load = cc_mar $loaddcr cc_evmc_load = cc_evmc diff --git a/reeds/core/solve/3_solve_window.gms b/reeds/core/solve/3_solve_window.gms index 505675ac..99e7e699 100644 --- a/reeds/core/solve/3_solve_window.gms +++ b/reeds/core/solve/3_solve_window.gms @@ -44,7 +44,7 @@ $ifthene.notfirstiter %niter%>0 *indicate we're loading data tload(t)$tmodel(t) = yes ; -$gdxin ReEDS_Augur%ds%augur_data%ds%ReEDS_Augur_merged_%niter%.gdx +$gdxin handoff%ds%reeds_data%ds%ccdata_merged_%niter%.gdx $loaddcr loadset = merged_set_1 $loaddcr cc_old_load2 = cc_old $loaddcr cc_mar_load2 = cc_mar @@ -126,7 +126,7 @@ cc_int(i,v,r,szn,t)$[cc_int(i,v,r,szn,t) < 0.001] = 0 ; cc_iter(i,v,r,szn,t,"%niter%")$cc_int(i,v,r,szn,t) = cc_int(i,v,r,szn,t) ; -execute_unload 'ReEDS_Augur%ds%augur_data%ds%curtout_%case%_%niter%.gdx' cc_int ; +execute_unload 'handoff%ds%reeds_data%ds%curtout_%case%_%niter%.gdx' cc_int ; *following line will load in the level values if the switch is enabled *note that this is still within the conditional that we are now past the first iteration diff --git a/reeds/core/solve/6_data_dump.gms b/reeds/core/solve/6_data_dump.gms index 29c97036..5e804c1d 100644 --- a/reeds/core/solve/6_data_dump.gms +++ b/reeds/core/solve/6_data_dump.gms @@ -1,5 +1,5 @@ $ontext -This file creates a gdx file with all of the data necessary for the Augur module to solve. This includes: +This file creates a gdx file with all of the data necessary for the resource adequacy calculations: - Generator capacities - Exogenous retirments (sequential solves only) - Wind capacity by build year (because wind CFs change by build year) @@ -16,7 +16,7 @@ $if not set start_year $setglobal start_year %startyear% * Set and parameter definitions *=============================== -set rfeas(r) "list of feasible r regions - for use in Augur only" +set rfeas(r) "list of feasible r regions" trange(t) "range from first year to current year" tcur(t) "current year" tnext(t) "next year" @@ -280,7 +280,7 @@ cap_trans_prm(r,rr,trtype) = sum{t$tcur(t), CAPTRAN_PRM.l(r,rr,trtype,t) } ; cap_converter_filt(r) = sum{t$tcur(t), CAP_CONVERTER.l(r,t) } ; -* In Augur, trtype="AC" includes everything except for VSC +* In resource adequacy calculations, trtype="AC" includes everything except for VSC routes_filt(r,rr,trtype) = sum{t$tcur(t), routes(r,rr,trtype,t) } ; *============================ @@ -334,7 +334,7 @@ energy_price(r,h)$hours(h) = * Unload all relevant data to a gdx file *======================================= -execute_unload 'ReEDS_Augur%ds%augur_data%ds%reeds_data_%cur_year%.gdx' +execute_unload 'handoff%ds%reeds_data%ds%reeds_data_%cur_year%.gdx' avail_filt bcr bir_pvb_config @@ -421,4 +421,4 @@ execute_unload 'ReEDS_Augur%ds%augur_data%ds%reeds_data_%cur_year%.gdx' *** dump data for tax credit phaseout calculations execute_unload "outputs%ds%tc_phaseout_data%ds%emit_for_tc_phaseout_calc_%cur_year%.gdx" emit_nat_tc, emit_r_tc -; \ No newline at end of file +; diff --git a/reeds/core/solve/solve.py b/reeds/core/solve/solve.py index a9807fd1..e8bb829c 100644 --- a/reeds/core/solve/solve.py +++ b/reeds/core/solve/solve.py @@ -8,7 +8,6 @@ from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent.parent)) import reeds -from reeds.resource_adequacy import Augur #%% Main function @@ -31,7 +30,7 @@ def run_reeds(casepath, t, onlygams=False, iteration=0): tnext = {**dict(zip(years, years[1:])), **{years[-1]:years[-1]}} #%%### Run GAMS LP - if not onlyaugur: + if not onlyra: #%% Get the command to run GAMS for this solve year batch_case = os.path.basename(casepath) stress_year = f"{t}i{iteration}" @@ -77,9 +76,14 @@ def run_reeds(casepath, t, onlygams=False, iteration=0): raise Exception(f"Missing {savefile}.g00") - #%%### Run Augur - if (not onlygams) and (tnext[t] > int(sw.GSw_SkipAugurYear)): - Augur.main(t=t, tnext=tnext[t], casedir=casepath, iteration=iteration) + #%%### Run resource adequacy calculations + if (not onlygams) and (tnext[t] > int(sw.GSw_SkipRAyear)): + reeds.resource_adequacy.ra_calcs.main( + t=t, + tnext=tnext[t], + casedir=casepath, + iteration=iteration, + ) #%% Driver function @@ -103,15 +107,15 @@ def main(casepath, t, overwrite=False): and os.path.isfile( os.path.join( sw.casedir, 'inputs_case', f'stress{t}i{iteration+1}', 'cf_vre.csv')) - ## Check if Augur finished + ## Check if resource adequacy calculations finished and os.path.isfile( os.path.join( - sw.casedir, 'ReEDS_Augur', 'augur_data', f'ReEDS_Augur_{t}.gdx')) + sw.casedir, 'handoff', 'reeds_data', f'ccdata_{t}.gdx')) ): print(f'Already ran {t}i{iteration} so continuing to next iteration') continue - #%% Run ReEDS and Augur + #%% Run ReEDS and RA calculations run_reeds(casepath, t, iteration=iteration) #%% Stop here if there's no stress period data for the next iteration @@ -150,9 +154,9 @@ def main(casepath, t, overwrite=False): parser.add_argument('--iteration', '-i', type=int, default=0, help='iteration counter for this run') parser.add_argument('--onlygams', '-g', action='store_true', - help='Only run GAMS (skip Augur)') - parser.add_argument('--onlyaugur', '-a', action='store_true', - help='Only run Augur (skip GAMS)') + help='Only run GAMS (skip resource adequacy)') + parser.add_argument('--onlyra', '-a', action='store_true', + help='Only run resource adequacy (RA) (skip GAMS)') parser.add_argument('--overwrite', '-o', action='store_true', help='Overwrite iterations that have already finished') @@ -161,7 +165,7 @@ def main(casepath, t, overwrite=False): t = args.t iteration = args.iteration onlygams = args.onlygams - onlyaugur = args.onlyaugur + onlyra = args.onlyra overwrite = args.overwrite #%% Switch to run folder diff --git a/reeds/input_processing/hourly_writetimeseries.py b/reeds/input_processing/hourly_writetimeseries.py index 1882269a..5954516a 100644 --- a/reeds/input_processing/hourly_writetimeseries.py +++ b/reeds/input_processing/hourly_writetimeseries.py @@ -790,7 +790,7 @@ def main(sw, reeds_path, inputs_case, periodtype='rep', make_plots=1, logging=Tr np.ravel([[c]*GSw_HourlyChunkLength for c in outchunks_allyrs]) )) - # %%### h_dt_szn for Augur + # %%### h_dt_szn for resource adequacy calculations if not len(hmap_myr) % 8760: ## Important: When modeling a single weather year, rep periods in the ## h_dt_szn table are just the single-year periods concatenated n times. @@ -1331,7 +1331,7 @@ def main(sw, reeds_path, inputs_case, periodtype='rep', make_plots=1, logging=Tr False, False, ], - ## 8760 hour linkage set for Augur (h,szn,year,hour) + ## 8760 hour linkage set for resource adequacy (h,szn,year,hour) "h_dt_szn": [ h_dt_szn[["h", "season", "ccseason", "year", "hour"]].assign( h=h_dt_szn.h.map(chunkmap) diff --git a/reeds/input_processing/recf.py b/reeds/input_processing/recf.py index 3a160be5..f3d91cde 100644 --- a/reeds/input_processing/recf.py +++ b/reeds/input_processing/recf.py @@ -5,7 +5,7 @@ Resources: - Creates a resource-to-(i,r,ccreg) lookup table for use in hourly_writesupplycurves.py - and Augur + and resource adequacy calculations - Add the distributed PV resources RECF: - Add the distributed PV recf profiles diff --git a/reeds/io.py b/reeds/io.py index 2ff40de6..71539561 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -621,7 +621,7 @@ def standardize_case(case=None): def get_switches(case=None, **kwargs): """ - Get pd.Series of switch values from switches.csv, augur_switches.csv, + Get pd.Series of switch values from switches.csv, ra_switches.csv, and CPLEX opt file. Accepts either {case} or {case}/inputs_case as input. @@ -646,11 +646,11 @@ def get_switches(case=None, **kwargs): index_col=0, header=None, ).squeeze(1) - ### Augur-specific switches + ### Resource-adequacy-specific switches try: fpath_asw = os.path.join( (case if case is not None else reeds_path), - 'reeds', 'resource_adequacy', 'augur_switches.csv', + 'reeds', 'resource_adequacy', 'ra_switches.csv', ) asw = pd.read_csv(fpath_asw, index_col='key') for i, row in asw.iterrows(): @@ -670,7 +670,7 @@ def get_switches(case=None, **kwargs): row.value = float(row.value) sw = pd.concat([sw, asw.value]) except FileNotFoundError: - print(f"{fpath_asw} not found so leaving out Augur switches") + print(f"{fpath_asw} not found so leaving out resource adequacy switches") ### Add derivative switches sw['resource_adequacy_years_list'] = [int(y) for y in sw['resource_adequacy_years'].split('_')] sw['num_resource_adequacy_years'] = len(sw['resource_adequacy_years_list']) @@ -1230,7 +1230,7 @@ def get_last_iteration(case, year=2050, datum=None, samples=None): raise ValueError(f"datum must be in [None,'flow','energy'] but is {datum}") infile = sorted(glob( os.path.join( - case, 'ReEDS_Augur', 'PRAS', + case, 'handoff', 'PRAS', f"PRAS_{year}i*" + (f'-{samples}' if samples is not None else '') + (f'-{datum}' if datum is not None else '') @@ -1254,11 +1254,11 @@ def get_pras_system(case, year=None, iteration='last', verbose=0): get_last_iteration(case, t)[1] if iteration in [None, 'last'] else iteration ) - infile = os.path.join(case, 'ReEDS_Augur', 'PRAS', f"PRAS_{t}i{_iteration}.pras") + infile = os.path.join(case, 'handoff', 'PRAS', f"PRAS_{t}i{_iteration}.pras") if not os.path.exists(infile): raise FileNotFoundError( f'{infile} does not exist; run postprocessing/run_reeds2pras.py or rerun ' - 'the ReEDS case with keep_augur_files=1' + 'the ReEDS case with keep_resource_adequacy_files=1' ) pras = {} with h5py.File(infile,'r') as f: diff --git a/reeds/parse.py b/reeds/parse.py index 177c36e0..f51fc857 100644 --- a/reeds/parse.py +++ b/reeds/parse.py @@ -378,7 +378,7 @@ def solvestring_sequential( 'GSw_MGA_CostDelta', 'GSw_MGA_Direction', 'GSw_PVB_Dur', - 'GSw_SkipAugurYear', + 'GSw_SkipRAyear', 'GSw_StateCO2ImportLevel', 'GSw_StartMarkets', 'GSw_ValStr', diff --git a/reeds/prasplots.py b/reeds/prasplots.py index 2794c489..00440f1c 100644 --- a/reeds/prasplots.py +++ b/reeds/prasplots.py @@ -252,7 +252,7 @@ def plot_pras_eue_timeseries_full( else: _iteration = iteration infile = os.path.join( - case, 'ReEDS_Augur', 'PRAS', + case, 'handoff', 'PRAS', f"PRAS_{year}i{_iteration}" + (f'-{samples}' if samples is not None else '') + '.h5' ) dfpras = reeds.io.read_pras_results(infile) @@ -363,7 +363,7 @@ def plot_pras_samples( ### Get unit availability filebase = os.path.join( - case, 'ReEDS_Augur', 'PRAS', + case, 'handoff', 'PRAS', f"PRAS_{t}i{_iteration}" f"{f'-{samples}' if isinstance(samples, int) else ''}" ) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 7501c6ac..18428e1f 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -6206,20 +6206,20 @@ def map_stressors( dflevel = dfmap[level].copy() ### Get the data - augur_files = { - 'vre_gen': os.path.join(case, 'ReEDS_Augur', 'augur_data', f'pras_vre_gen_{t}.h5'), - 'load': os.path.join(case, 'ReEDS_Augur', 'augur_data', f'pras_load_{t}.h5'), + ra_files = { + 'vre_gen': os.path.join(case, 'handoff', 'reeds_data', f'pras_vre_gen_{t}.h5'), + 'load': os.path.join(case, 'handoff', 'reeds_data', f'pras_load_{t}.h5'), } - if any([not os.path.exists(fpath) for fpath in augur_files.values()]): + if any([not os.path.exists(fpath) for fpath in ra_files.values()]): reeds.resource_adequacy.prep_data.main(t, case, iteration) - vre_gen = reeds.io.read_file(augur_files['vre_gen'], parse_timestamps=True) + vre_gen = reeds.io.read_file(ra_files['vre_gen'], parse_timestamps=True) vre_gen.columns = pd.MultiIndex.from_tuples( vre_gen.columns.map(lambda x: tuple(x.split('|'))), names=['i','r'], ) - load = reeds.io.read_file(augur_files['load'], parse_timestamps=True) + load = reeds.io.read_file(ra_files['load'], parse_timestamps=True) recf = reeds.io.read_file( os.path.join(case, 'inputs_case', 'recf.h5'), diff --git a/reeds/resource_adequacy/__init__.py b/reeds/resource_adequacy/__init__.py index 92f9aebb..a1507d1e 100644 --- a/reeds/resource_adequacy/__init__.py +++ b/reeds/resource_adequacy/__init__.py @@ -1,5 +1,5 @@ -from . import Augur as Augur from . import capacity_credit as capacity_credit from . import prep_data as prep_data from . import ra as ra +from . import ra_calcs as ra_calcs from . import stress_periods as stress_periods diff --git a/reeds/resource_adequacy/capacity_credit.py b/reeds/resource_adequacy/capacity_credit.py index d778d6da..c320deff 100644 --- a/reeds/resource_adequacy/capacity_credit.py +++ b/reeds/resource_adequacy/capacity_credit.py @@ -34,8 +34,8 @@ def set_marg_vre_step_size(t, sw, gdx, hierarchy): Inputs * marg_vre_steps [int]: Number of previous solve years to consider when evaluating the marginal VRE step size (default: 2). Must be at least 1; - a value of 2 can help reduce oscillations. Augur will automatically drop - from consideration solves that are more than 5 years from the previous solve. + a value of 2 can help reduce oscillations. Solves that are more than + 5 years from the previous solve are automatically dropped. ''' # load yearset for getting various previous steps yearset = gdx['tmodel_new'].allt.astype(int).tolist() @@ -45,8 +45,7 @@ def set_marg_vre_step_size(t, sw, gdx, hierarchy): step_sizes = [] for step in range(int(sw['marg_vre_steps'])): - # try-except to handle cases where there aren't multiple - # steps to go back to (e.g. running Augur after 1st solve) + # try-except to handle cases where there aren't multiple steps to go back to try: target_last_step = yearset[yearset.index(t)-step] @@ -56,7 +55,7 @@ def set_marg_vre_step_size(t, sw, gdx, hierarchy): step_sizes.append(get_relative_step_sizes(t, yearset, target_last_step)) prev_year_list.append(target_last_step) except Exception: - print('First Augur year so no previous steps') + print('First resource adequacy year so no previous steps') relative_step_sizes = pd.DataFrame(list(zip(prev_year_list, step_sizes)), columns=['t', 'step']) @@ -121,10 +120,10 @@ def reeds_cc(t, tnext, casedir): hierarchy = reeds.io.get_hierarchy(casedir).reset_index() resources = pd.read_csv(os.path.join(inputs_case, 'resources.csv')) - augur_data = os.path.join(casedir,'ReEDS_Augur','augur_data') - cap = pd.read_csv(os.path.join(augur_data, f'max_cap_{t}.csv')) + reeds_data = os.path.join(casedir, 'handoff', 'reeds_data') + cap = pd.read_csv(os.path.join(reeds_data, f'max_cap_{t}.csv')) - gdx = gdxpds.to_dataframes(os.path.join(augur_data,f'reeds_data_{t}.gdx')) + gdx = gdxpds.to_dataframes(os.path.join(reeds_data, f'reeds_data_{t}.gdx')) techs = gdx['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') techs.columns = techs.columns.str.lower() r = gdx['rfeas'] @@ -170,9 +169,9 @@ def reeds_cc(t, tnext, casedir): ### Prepare the seasonal profiles ## vre_gen needs to have tech_class_r columns ## last version has (ccseason,year,h,hour) index - vre_gen = pd.read_hdf(os.path.join(augur_data,f'vre_gen_exist_{t}.h5')) + vre_gen = pd.read_hdf(os.path.join(reeds_data,f'vre_gen_exist_{t}.h5')) ## vre_cf_marg has same columns and index as vre_gen - vre_cf_marg = pd.read_hdf(os.path.join(augur_data,f'vre_cf_marg_{t}.h5')) + vre_cf_marg = pd.read_hdf(os.path.join(reeds_data,f'vre_cf_marg_{t}.h5')) if int(sw['GSw_PRM_CapCreditMulti']) == 0: # Restrict capacity credit evaluation to use 2012 only (rather than multi-year) @@ -191,7 +190,7 @@ def reeds_cc(t, tnext, casedir): load_profiles = ( # HOURLY_PROFILES['load'].profiles - pd.read_hdf(os.path.join(augur_data,f'load_{t}.h5')) + pd.read_hdf(os.path.join(reeds_data,f'load_{t}.h5')) ### Map BA regions to ccreg's and sum over them .rename(columns=hierarchy.set_index('r').ccreg) .groupby(axis=1, level=0).sum() diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index 61523955..bc22f1a3 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -31,10 +31,10 @@ def delete_temporary_files(sw): Delete temporary csv, pkl, and h5 files """ dropfiles = ( - glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.pkl")) - + glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.h5")) - + glob(os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f"*_{sw['t']}.csv")) - + glob(os.path.join(sw['casedir'],'ReEDS_Augur','PRAS',f"PRAS_{sw['t']}*.pras")) + glob(os.path.join(sw['casedir'],'handoff','reeds_data',f"*_{sw['t']}.pkl")) + + glob(os.path.join(sw['casedir'],'handoff','reeds_data',f"*_{sw['t']}.h5")) + + glob(os.path.join(sw['casedir'],'handoff','reeds_data',f"*_{sw['t']}.csv")) + + glob(os.path.join(sw['casedir'],'handoff','PRAS',f"PRAS_{sw['t']}*.pras")) ) for keyword in sw['keepfiles']: @@ -46,7 +46,7 @@ def delete_temporary_files(sw): #%% Input-loading function def get_inputs(sw): ### Make savepath - sw['savepath'] = os.path.join(sw['casedir'], 'outputs', 'Augur_plots') + sw['savepath'] = os.path.join(sw['casedir'], 'outputs', 'figures', 'resource_adequacy') os.makedirs(sw['savepath'], exist_ok=True) ##### Load shared parameters @@ -61,7 +61,7 @@ def get_inputs(sw): h_dt_szn['d'] = h_dt_szn.datetime.dt.strftime('sy%Yd%j') gdxreeds = gdxpds.to_dataframes( - os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'reeds_data_{sw["t"]}.gdx')) + os.path.join(sw['casedir'],'handoff','reeds_data',f'reeds_data_{sw["t"]}.gdx')) techs = gdxreeds['i_subsets'].pivot(columns='i_subtech',index='i',values='Value') h2dac = techs['CONSUME'].dropna().index @@ -89,7 +89,7 @@ def get_inputs(sw): ### Load and aggregate the VRE generation profiles by tech group try: vre_gen = reeds.io.read_file( - os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'pras_vre_gen_{sw.t}.h5'), + os.path.join(sw['casedir'],'handoff','reeds_data',f'pras_vre_gen_{sw.t}.h5'), parse_timestamps=True, ) except FileNotFoundError: @@ -118,7 +118,7 @@ def get_inputs(sw): try: load_r = pd.read_hdf( os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data',f'load_{sw.t}.h5') + sw['casedir'],'handoff','reeds_data',f'load_{sw.t}.h5') ) load_r.index = fulltimeindex except FileNotFoundError: @@ -127,7 +127,7 @@ def get_inputs(sw): ### Load PRAS load try: pras_load = reeds.io.read_file( - os.path.join(sw['casedir'],'ReEDS_Augur','augur_data',f'pras_load_{sw.t}.h5'), + os.path.join(sw['casedir'],'handoff','reeds_data',f'pras_load_{sw.t}.h5'), parse_timestamps=True, ) except FileNotFoundError: @@ -136,7 +136,7 @@ def get_inputs(sw): try: pras_h2dac_load = reeds.io.read_file( os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data', + sw['casedir'],'handoff','reeds_data', f"pras_h2dac_load_{sw['t']}.h5"), parse_timestamps=True, ) @@ -148,7 +148,7 @@ def get_inputs(sw): try: max_cap = pd.read_csv( os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data',f"max_cap_{sw['t']}.csv")) + sw['casedir'],'handoff','reeds_data',f"max_cap_{sw['t']}.csv")) max_cap.i = reeds.reedsplots.simplify_techs(max_cap.i, display_level = 'diagnostics') except FileNotFoundError: max_cap = pd.DataFrame(columns=['i','v','r','MW']) @@ -156,7 +156,7 @@ def get_inputs(sw): ### Load LOLE/EUE/NEUE from PRAS try: pras = reeds.io.read_pras_results( - os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', + os.path.join(sw['casedir'], 'handoff', 'PRAS', f"PRAS_{sw.t}i{sw.iteration}.h5") ) pras.index = fulltimeindex @@ -499,14 +499,14 @@ def plot_pras_ICAP(sw, dfs): plt.close() -def plot_augur_pras_capacity(sw, dfs): +def plot_reeds_pras_capacity(sw, dfs): """ - Plot the nameplate capacity from Augur and PRAS to check consistency + Plot the nameplate capacity from ReEDS and PRAS to check consistency """ if not len(dfs['pras_system']): print('PRAS system was not loaded') return - savename = f"PRAS-Augur-capacity-{sw['t']}.png" + savename = f"PRAS-ReEDS-capacity-{sw['t']}.png" ### Get the colors tech_style = dfs['tech_style']['color'].squeeze() ### Collect the PRAS system capacities @@ -523,12 +523,12 @@ def plot_augur_pras_capacity(sw, dfs): .groupby(axis=1, level=[1,0]).sum().max().rename('MW') ) - ### Collect the Augur capacities - cap['augur'] = dfs['max_cap'].groupby(['i','r'], as_index=False).MW.sum() + ### Collect the ReEDS capacities + cap['reeds'] = dfs['max_cap'].groupby(['i','r'], as_index=False).MW.sum() ## Convert from s to p regions - cap['augur'].r = cap['augur'].r + cap['reeds'].r = cap['reeds'].r ## Aggregate by type - cap['augur'] = (cap['augur'] + cap['reeds'] = (cap['reeds'] .replace({'i':{'Hydropower Existing':'Hydropower', 'Hydropower New':'Hydropower'}}) .groupby(['r','i']).MW.sum() / 1e3 ) @@ -556,7 +556,7 @@ def plot_augur_pras_capacity(sw, dfs): ) alltechs = set() for r in zones: - df = pd.concat({'A':cap['augur'].get(r,pd.Series()), 'P':cap['pras'].get(r,pd.Series())}, axis=1).T + df = pd.concat({'A':cap['reeds'].get(r,pd.Series()), 'P':cap['pras'].get(r,pd.Series())}, axis=1).T order = [c for c in tech_style.index if c in df] missing = [c for c in df if c not in order] if len(missing): @@ -906,17 +906,17 @@ def plot_pras_load_units(sw, dfs): plt.close() -def plot_pras_augur_load(sw, dfs): - """PRAS load against Augur load""" +def plot_pras_reeds_load(sw, dfs): + """PRAS load against ReEDS load""" dfpras = dfs['pras_system']['load'].sum(axis=1).rename('PRAS') - dfaugur = dfs['load_r'].set_axis(dfpras.index).sum(axis=1).rename('Augur') + dfreeds = dfs['load_r'].set_axis(dfpras.index).sum(axis=1).rename('ReEDS') years = dfpras.index.year.unique() - linecolors = {'Augur':'C0', 'PRAS':'C3'} + linecolors = {'ReEDS':'C0', 'PRAS':'C3'} for year in years: - savename = f"demand_USA-Augur-PRAS-w{year}-{sw['t']}.png" + savename = f"demand_USA-ReEDS-PRAS-w{year}-{sw['t']}.png" plt.close() f,ax = plots.plotyearbymonth( - dfaugur.loc[str(year)], style='line', colors=linecolors['Augur']) + dfreeds.loc[str(year)], style='line', colors=linecolors['ReEDS']) plots.plotyearbymonth( dfpras.loc[str(year)], style='line', colors=linecolors['PRAS'], f=f, ax=ax) ## Legend @@ -1033,8 +1033,7 @@ def plot_cc_mar(sw, dfs): if not int(sw['GSw_PRM_CapCredit']): raise KeyError('No capacity credit values to plot') cc_results = gdxpds.to_dataframes(os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data', - 'ReEDS_Augur_{}.gdx'.format(sw['t']) + sw['casedir'], 'handoff', 'reeds_data', f"ccdata_{sw['t']}.gdx" )) dfplot = cc_results[param].drop('t',axis=1).copy() @@ -1177,7 +1176,7 @@ def plot_netloadhours_histogram(sw, dfs): def plot_stressors(sw, dfs): """ - Map demand/CF/FOR (organized differently to allow use outside of Augur) + Map demand/CF/FOR (organized differently to allow for independent use) """ for iteration in range(sw['iteration']): plot_generator = reeds.reedsplots.map_stressors( @@ -1231,9 +1230,9 @@ def main(sw, debug=False): print('plot_pras_unitnumber() failed:', traceback.format_exc()) try: - plot_augur_pras_capacity(sw, dfs) + plot_reeds_pras_capacity(sw, dfs) except Exception: - print('plot_augur_pras_capacity() failed:', traceback.format_exc()) + print('plot_reeds_pras_capacity() failed:', traceback.format_exc()) try: plot_pras_load(sw, dfs) @@ -1279,9 +1278,9 @@ def main(sw, debug=False): if debug: try: - plot_pras_augur_load(sw, dfs) + plot_pras_reeds_load(sw, dfs) except Exception: - print('plot_pras_augur_load() failed:', traceback.format_exc()) + print('plot_pras_reeds_load() failed:', traceback.format_exc()) try: plot_pras_ICAP(sw, dfs) @@ -1347,7 +1346,7 @@ def main(sw, debug=False): sw['iteration'] = iteration ### Make the plots - print('plotting intermediate Augur results...') + print('plotting intermediate resource adequacy results...') try: main(sw, debug) except Exception as _err: @@ -1355,5 +1354,5 @@ def main(sw, debug=False): print(traceback.format_exc()) ### Remove intermediate csv files to save drive space - if (not int(sw['keep_augur_files'])) and (not int(sw['debug'])): + if (not int(sw['keep_resource_adequacy_files'])) and (not int(sw['debug'])): delete_temporary_files(sw) diff --git a/reeds/resource_adequacy/prep_data.py b/reeds/resource_adequacy/prep_data.py index 8f4febfc..872cdbd0 100644 --- a/reeds/resource_adequacy/prep_data.py +++ b/reeds/resource_adequacy/prep_data.py @@ -5,7 +5,7 @@ * run_pras.jl -> ReEDS2PRAS.jl -> PRAS.jl (probabilistic resource adequacy) The files used by PRAS are: -* In {case}/ReEDS_Augur/augur_data: +* In {case}/handoff/reeds_data: * cap_converter_{year}.csv * energy_cap_{year}.csv * max_cap_{year}.csv @@ -105,11 +105,11 @@ def errorcheck_reeds2pras(casedir, csvout, h5out): def main(t, casedir, iteration=0): #%%### DEBUGGING: Inputs # t = 2020 - # reeds_path = os.path.expanduser('~/github2/ReEDS-2.0') - # casedir = os.path.join(reeds_path,'runs','v20230214_PRMaugurM0_Pacific_d7fIrh4_CC_y2012') + # reeds_path = os.path.expanduser('~/github/ReEDS') + # casedir = os.path.join(reeds_path,'runs','v20260417_reorgM0_Pacific') #%%### Get inputs from ReEDS - gdx_file = os.path.join(casedir,'ReEDS_Augur','augur_data',f'reeds_data_{t}.gdx') + gdx_file = os.path.join(casedir,'handoff','reeds_data',f'reeds_data_{t}.gdx') gdxreeds = gdxpds.to_dataframes(gdx_file) ### Use indices as multiindex for key in gdxreeds: @@ -567,7 +567,7 @@ def intify(v): #%% .csv files for key in csvout: csvout[key].round(int(sw['decimals'])).to_csv( - os.path.join(casedir,'ReEDS_Augur','augur_data',f'{key}_{t}.csv'), + os.path.join(casedir,'handoff','reeds_data',f'{key}_{t}.csv'), ) #%% .h5 files @@ -576,11 +576,11 @@ def intify(v): reeds.io.write_profile_to_h5( df=h5out[key].astype(np.float32), filename=f'{key}_{t}.h5', - outfolder=os.path.join(casedir,'ReEDS_Augur','augur_data'), + outfolder=os.path.join(casedir,'handoff','reeds_data'), ) else: h5out[key].astype(np.float32).to_hdf( - os.path.join(casedir,'ReEDS_Augur','augur_data',f'{key}_{t}.h5'), + os.path.join(casedir,'handoff','reeds_data',f'{key}_{t}.h5'), key='data', complevel=4, mode='w', ) diff --git a/reeds/resource_adequacy/ra.py b/reeds/resource_adequacy/ra.py index 22d8ee35..c1da83bd 100644 --- a/reeds/resource_adequacy/ra.py +++ b/reeds/resource_adequacy/ra.py @@ -13,7 +13,7 @@ def get_pras_eue(case, t, iteration=0): """ ### Get PRAS outputs dfpras = reeds.io.read_pras_results( - os.path.join(case, 'ReEDS_Augur', 'PRAS', f"PRAS_{t}i{iteration}.h5") + os.path.join(case, 'handoff', 'PRAS', f"PRAS_{t}i{iteration}.h5") ) ### Create the time index sw = reeds.io.get_switches(case) @@ -82,7 +82,7 @@ def get_eue_periods( ### Get load at hierarchy_level dfload = reeds.io.read_h5py_file( os.path.join( - case,'ReEDS_Augur','augur_data',f'pras_load_{t}.h5') + case,'handoff','reeds_data',f'pras_load_{t}.h5') ).rename(columns=rmap).groupby(level=0, axis=1).sum() dfload.index = dfeue.index diff --git a/reeds/resource_adequacy/Augur.py b/reeds/resource_adequacy/ra_calcs.py similarity index 90% rename from reeds/resource_adequacy/Augur.py rename to reeds/resource_adequacy/ra_calcs.py index 17ad0026..d3df43aa 100644 --- a/reeds/resource_adequacy/Augur.py +++ b/reeds/resource_adequacy/ra_calcs.py @@ -6,6 +6,8 @@ import datetime import pandas as pd import gdxpds +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent)) import reeds @@ -55,7 +57,7 @@ def run_pras( '--threads=1' if (sys.platform == 'darwin') or int(sw.get('pras_singlethread', 0)) else f"--threads={sw['threads'] if sw['threads'] > 0 else 'auto'}" ), - f"{os.path.join(scriptpath, 'ReEDS_Augur','run_pras.jl')}", + f"{os.path.join(scriptpath, 'reeds', 'resource_adequacy', 'run_pras.jl')}", f"--reeds_path={sw['reeds_path']}", f"--reedscase={casedir}", f"--solve_year={t}", @@ -97,20 +99,20 @@ def run_pras( def main(t, tnext, casedir, iteration=0): # #%% To debug, uncomment these lines and update the run path - # t = 2026 - # tnext = 2029 + # t = 2020 + # tnext = 2023 # reeds_path = reeds.io.reeds_path # casedir = os.path.join( - # reeds_path,'runs','v20250521_prasM0_Pacific') + # reeds_path,'runs','v20260417_reorgM1_Pacific') # iteration = 0 # assert tnext >= t # os.chdir(casedir) # ## Copy reeds2pras from repo to run folder # import shutil - # shutil.rmtree(os.path.join(casedir, 'reeds2pras')) + # shutil.rmtree(os.path.join(casedir, 'reeds', 'resource_adequacy', 'reeds2pras')) # shutil.copytree( - # os.path.join(reeds_path, 'reeds2pras'), - # os.path.join(casedir, 'reeds2pras'), + # os.path.join(reeds_path, 'reeds', 'resource_adequacy', 'reeds2pras'), + # os.path.join(casedir, 'reeds', 'resource_adequacy', 'reeds2pras'), # ignore=shutil.ignore_patterns('test'), # ) @@ -121,7 +123,7 @@ def main(t, tnext, casedir, iteration=0): #%% Prep data for resource adequacy print('Preparing data for resource adequacy calculations') tic = datetime.datetime.now() - augur_csv, augur_h5 = reeds.resource_adequacy.prep_data.main(t, casedir, iteration) + reeds_csv, reeds_h5 = reeds.resource_adequacy.prep_data.main(t, casedir, iteration) reeds.log.toc(tic=tic, year=t, process='ra/prep_data.py') #%% Calculate capacity credit if necessary; otherwise bypass @@ -184,14 +186,13 @@ def main(t, tnext, casedir, iteration=0): ) gdx[-1].dataframe = cc_results[key] gdx.write( - os.path.join('ReEDS_Augur', 'augur_data', f'ReEDS_Augur_{t}.gdx') + os.path.join('handoff', 'reeds_data', f'ccdata_{t}.gdx') ) # #%% Uncomment to run diagnostic_plots # ### (typically run from call_{}.sh script for parallelization) # try: - # import ReEDS_Augur.diagnostic_plots as diagnostic_plots - # diagnostic_plots.main(sw) + # reeds.resource_adequacy.diagnostic_plots.main(sw) # except Exception as err: # print('diagnostic_plots.py failed with the following exception:') # print(err) @@ -200,7 +201,7 @@ def main(t, tnext, casedir, iteration=0): #%% Procedure if __name__ == '__main__': - parser = argparse.ArgumentParser(description="""Running ReEDS Augur""") + parser = argparse.ArgumentParser(description="Resource adequacy calculations") parser.add_argument("tnext", help="Next ReEDS solve year", type=int) parser.add_argument("t", help="Previous ReEDS solve year", type=int) diff --git a/reeds/resource_adequacy/augur_switches.csv b/reeds/resource_adequacy/ra_switches.csv similarity index 89% rename from reeds/resource_adequacy/augur_switches.csv rename to reeds/resource_adequacy/ra_switches.csv index bff65dbe..092ca9cd 100644 --- a/reeds/resource_adequacy/augur_switches.csv +++ b/reeds/resource_adequacy/ra_switches.csv @@ -12,5 +12,5 @@ cc_stor_stepsize,100,int,step size (in MW) used when determining the peaking cap decimals,3,int,number of decimals to round results to for ReEDS flex_consume_techs,"dac,electrolyzer",list,list of consume techs that are flexible keepfiles,"dropped_load,cf",list,list of temporary files to keep -marg_vre_steps,2,int,Number of previous solve years to consider when evaluating the marginal VRE step size (default: 2). Must be at least 1; a value of 2 can help reduce oscillations. Augur will automatically drop from consideration solves that are more than 5 years from the previous solve. +marg_vre_steps,2,int,Number of previous solve years to consider when evaluating the marginal VRE step size (default: 2). Must be at least 1; a value of 2 can help reduce oscillations. Solves that are more than 5 years from the previous solve are automatically dropped from consideration. storcap_cutoff,1,float,[MW and MWh] Minimum storage capacity to send to ReEDS2PRAS (applies to both power and energy capacity) diff --git a/reeds/resource_adequacy/reeds2pras/README.md b/reeds/resource_adequacy/reeds2pras/README.md index c0ed00a9..2f5d09df 100644 --- a/reeds/resource_adequacy/reeds2pras/README.md +++ b/reeds/resource_adequacy/reeds2pras/README.md @@ -32,15 +32,14 @@ If you have a completed ReEDS run and a REPL with ReEDS2PRAS (`using ReEDS2PRAS` ```julia using ReEDS2PRAS - -reedscase = "/projects/ntps/llavin/ReEDS-2.0/runs/ntpsrerun_Xlim_DemHi_90by2035EarlyPhaseout__core" # path to completed ReEDS run -solve_year = 2035 #need ReEDS Augur data for the input solve year -weather_year = 2012 # must be 2007-2013 or 2016-2023 +# path to completed ReEDS run +reedscase = "/projects/ntps/llavin/ReEDS-2.0/runs/ntpsrerun_Xlim_DemHi_90by2035EarlyPhaseout__core" +solve_year = 2035 +weather_year = 2012 timesteps = 8760 -user_descriptors = "your_user_descriptors_json_location_here" # Optional - if not passed uses default values # returns a parameterized PRAS system -pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, user_descriptors = user_descriptors) +pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year) ``` This will save out a pras system to the variable `pras_system` from the ReEDS2PRAS run. The user can also save a PRAS system to a specific location using `PRAS.savemodel(pras_system, joinpath("MYPATH"*".pras")`. The saved PRAS system may then be read in by other tools like PRAS Analytics (`https://github.nrel.gov/PRAS/PRAS-Analytics`) for further analysis, post-processing, and plotting. @@ -54,13 +53,12 @@ using ReEDS2PRAS # path to completed ReEDS run reedscase = "/projects/ntps/llavin/ReEDS-2.0/runs/ntpsrerun_Xlim_DemHi_90by2035EarlyPhaseout__core" -solve_year = 2035 #need ReEDS Augur data for the input solve year -weather_year = 2007 # must be 2007-2013 or 2016-2023 +solve_year = 2035 +weather_year = 2007 timesteps = 61320 -user_descriptors = "your_user_descriptors_json_location_here" # Optional - if not passed uses default values # returns a parameterized PRAS system -pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year, user_descriptors = user_descriptors) +pras_system = ReEDS2PRAS.reeds_to_pras(reedscase, solve_year, timesteps, weather_year) ``` Importantly, the timesteps count from the first hour of the first `weather_year`, so the user must input `2007` as the `weather_year` to run all 61320 hourly timesteps. diff --git a/reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl index ec1acc2d..2e434a5e 100644 --- a/reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl +++ b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_data_parsing.jl @@ -173,7 +173,7 @@ function split_generator_types(ReEDS_data::ReEDSdatapaths) ## Read {case}/inputs_case/tech-subset-table.csv tech_subset_table = get_technology_types(ReEDS_data) @debug "tech_subset_table is $(tech_subset_table)" - ## Read {case}/ReEDS_Augur/augur_data/max_cap_{year}.csv + ## Read {case}/handoff/reeds_data/max_cap_{year}.csv capacity_data = get_ICAP_data(ReEDS_data) ## Read {case}/inputs_case/resources.csv resources = get_valid_resources(ReEDS_data) @@ -738,8 +738,8 @@ function process_storages( efficiency_in = Dict( polarity => DataFrames.DataFrame(CSV.File(joinpath( ReEDS_data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", + "handoff", + "reeds_data", "$(polarity)_eff_$(ReEDS_data.year).csv" ))) for polarity in ["charge", "discharge"] diff --git a/reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl index 985936bc..39d2424c 100644 --- a/reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl +++ b/reeds/resource_adequacy/reeds2pras/src/utils/reeds_input_parsing.jl @@ -75,15 +75,15 @@ end Returns ------- HDF5.h5read(filepath, "data") - A readout of the Augur load h5 file associated with the given ReEDS + A readout of the load h5 file associated with the given ReEDS filepath and year. """ function get_load_file(data::ReEDSdatapaths) filepath = joinpath( data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", + "handoff", + "reeds_data", "pras_load_$(string(data.year)).h5", ) columns = HDF5.h5read(filepath, "columns") @@ -93,7 +93,7 @@ function get_load_file(data::ReEDSdatapaths) end """ - This function reads a hdf5 file from the ReEDS Augur directory, based on + This function reads a hdf5 file from the ReEDS directory, based on the year provided in the ReEDSdatapaths struct. Parameters @@ -109,8 +109,8 @@ end function get_vg_cf_data(data::ReEDSdatapaths) filepath = joinpath( data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", + "handoff", + "reeds_data", "pras_vre_gen_$(string(data.year)).h5", ) columns = HDF5.h5read(filepath, "columns") @@ -145,7 +145,7 @@ end """ function get_max_unitsize(data::ReEDSdatapaths) filepath = joinpath( - data.ReEDSfilepath, "ReEDS_Augur", "augur_data", + data.ReEDSfilepath, "handoff", "reeds_data", "max_unitsize_$(string(data.year)).csv" ) df = DataFrames.DataFrame(CSV.File(filepath)) @@ -154,7 +154,7 @@ end """ - Get the forced outage data from the augur files. + Get the forced outage data. Parameters ---------- @@ -227,8 +227,8 @@ function get_line_capacity_data(data::ReEDSdatapaths) #assumes this file has been formatted by ReEDS to be PRM line capacity data filepath = joinpath( data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", + "handoff", + "reeds_data", "tran_cap_$(string(data.year)).csv", ) return DataFrames.DataFrame(CSV.File(filepath)) @@ -251,8 +251,8 @@ end function get_converter_capacity_data(data::ReEDSdatapaths) filepath = joinpath( data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", + "handoff", + "reeds_data", "cap_converter_$(string(data.year)).csv", ) return DataFrames.DataFrame(CSV.File(filepath)) @@ -285,7 +285,7 @@ end """ Returns a DataFrame containing the installed capacity of generators for a - given year, read from {case}/ReEDS_Augur/augur_data/max_cap_{year}.csv. + given year, read from {case}/handoff/reeds_data/max_cap_{year}.csv. Parameters ---------- @@ -305,8 +305,8 @@ end function get_ICAP_data(data::ReEDSdatapaths) filepath = joinpath( data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", + "handoff", + "reeds_data", "max_cap_$(string(data.year)).csv", ) return DataFrames.DataFrame(CSV.File(filepath)) @@ -380,8 +380,8 @@ end function get_storage_energy_capacity_data(data::ReEDSdatapaths) filepath = joinpath( data.ReEDSfilepath, - "ReEDS_Augur", - "augur_data", + "handoff", + "reeds_data", "energy_cap_$(string(data.year)).csv", ) return DataFrames.DataFrame(CSV.File(filepath)) @@ -420,7 +420,7 @@ function get_hourly_scheduled_outage_data(data::ReEDSdatapaths) end """ - Get the hourly forced outage data from the augur files. + Get the hourly forced outage data. Parameters ---------- diff --git a/reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl b/reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl index 9a5a88a6..31f4d72c 100644 --- a/reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl +++ b/reeds/resource_adequacy/reeds2pras/src/utils/runchecks.jl @@ -14,17 +14,17 @@ function check_file(loc::String) end function run_checks(data::ReEDSdatapaths) - augur_data_path = joinpath(data.ReEDSfilepath, "ReEDS_Augur", "augur_data") + reeds_data_path = joinpath(data.ReEDSfilepath, "handoff", "reeds_data") filepaths = [ - joinpath(augur_data_path, "cap_converter_$(string(data.year)).csv"), - joinpath(augur_data_path, "charge_eff_$(string(data.year)).csv"), - joinpath(augur_data_path, "discharge_eff_$(string(data.year)).csv"), - joinpath(augur_data_path, "energy_cap_$(string(data.year)).csv"), - joinpath(augur_data_path, "max_cap_$(string(data.year)).csv"), - joinpath(augur_data_path, "max_unitsize_$(string(data.year)).csv"), - joinpath(augur_data_path, "pras_load_$(string(data.year)).h5"), - joinpath(augur_data_path, "pras_vre_gen_$(string(data.year)).h5"), - joinpath(augur_data_path, "tran_cap_$(string(data.year)).csv"), + joinpath(reeds_data_path, "cap_converter_$(string(data.year)).csv"), + joinpath(reeds_data_path, "charge_eff_$(string(data.year)).csv"), + joinpath(reeds_data_path, "discharge_eff_$(string(data.year)).csv"), + joinpath(reeds_data_path, "energy_cap_$(string(data.year)).csv"), + joinpath(reeds_data_path, "max_cap_$(string(data.year)).csv"), + joinpath(reeds_data_path, "max_unitsize_$(string(data.year)).csv"), + joinpath(reeds_data_path, "pras_load_$(string(data.year)).h5"), + joinpath(reeds_data_path, "pras_vre_gen_$(string(data.year)).h5"), + joinpath(reeds_data_path, "tran_cap_$(string(data.year)).csv"), joinpath(data.ReEDSfilepath, "inputs_case", "hydcapadj.csv"), joinpath(data.ReEDSfilepath, "inputs_case", "hydcf.csv"), joinpath(data.ReEDSfilepath, "inputs_case", "mttr.csv"), diff --git a/reeds/resource_adequacy/run_pras.jl b/reeds/resource_adequacy/run_pras.jl index a44e86f6..8a79d01e 100644 --- a/reeds/resource_adequacy/run_pras.jl +++ b/reeds/resource_adequacy/run_pras.jl @@ -391,7 +391,7 @@ end function main(args::Dict) #%% Define some intermediate filenames pras_system_path = joinpath( - args["reedscase"],"ReEDS_Augur","PRAS", + args["reedscase"], "handoff", "PRAS", "PRAS_$(args["solve_year"])i$(args["iteration"]).pras" ) @@ -477,7 +477,9 @@ if abspath(PROGRAM_FILE) == @__FILE__ args = parse_commandline() #%% Include ReEDS2PRAS - include(joinpath(args["reedscase"], "reeds2pras", "src", "ReEDS2PRAS.jl")) + include(joinpath( + args["reedscase"], "reeds", "resource_adequacy", "reeds2pras", "src", "ReEDS2PRAS.jl" + )) #%% Run it main(args) diff --git a/reeds/resource_adequacy/stress_periods.py b/reeds/resource_adequacy/stress_periods.py index 8774f6fa..55801f9a 100644 --- a/reeds/resource_adequacy/stress_periods.py +++ b/reeds/resource_adequacy/stress_periods.py @@ -45,7 +45,7 @@ def plot_eue_diagnostics(sw, t, iteration, high_eue_periods): vmax=vmax[outage_type], ) plt.savefig( - os.path.join(sw.casedir, 'outputs', 'Augur_plots', savename) + os.path.join(sw.casedir, 'outputs', 'figures', 'resource_adequacy', savename) ) plt.close() except Exception as err: @@ -64,7 +64,7 @@ def get_and_write_neue(sw, write=True): """ infiles = [ i for i in sorted(glob( - os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', 'PRAS_*.h5'))) + os.path.join(sw['casedir'], 'handoff', 'PRAS', 'PRAS_*.h5'))) if re.match(r"PRAS_[0-9]+i[0-9]+.h5", os.path.basename(i)) ] eue = {} @@ -94,12 +94,12 @@ def get_annual_neue(case, t, iteration=0): """ """ ### Get EUE from PRAS - dfeue = reeds.resource_adequacy.get_pras_eue(case=case, t=t, iteration=iteration) + dfeue = reeds.resource_adequacy.ra.get_pras_eue(case=case, t=t, iteration=iteration) ### Get load (for calculating NEUE) dfload = reeds.io.read_h5py_file( os.path.join( - case,'ReEDS_Augur','augur_data',f'pras_load_{t}.h5') + case,'handoff','reeds_data',f'pras_load_{t}.h5') ) dfload.index = dfeue.index @@ -204,7 +204,7 @@ def get_shoulder_periods(sw, criterion, dfenergy_r, high_eue_periods): def get_eue_sorted_periods(sw, t, iteration): ### Get storage state of charge (SOC) to use in selection of "shoulder" stress periods dfenergy = reeds.io.read_pras_results( - os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', f"PRAS_{t}i{iteration}-energy.h5") + os.path.join(sw['casedir'], 'handoff', 'PRAS', f"PRAS_{t}i{iteration}-energy.h5") ) timeindex = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) dfenergy.index = timeindex @@ -364,7 +364,7 @@ def prm_increment_pras(sw, t, iteration, combined_periods_write, failed_regions) ## shortfall data # read the net shortfall (positive) and net surplus (negative) results # by sample from PRAS run (MWh) - filepath = os.path.join(sw['casedir'], 'ReEDS_Augur', 'PRAS', + filepath = os.path.join(sw['casedir'], 'handoff', 'PRAS', f'PRAS_{sw["t"]}i{iteration}-shortfall_samples.h5') net_short = reeds.io.read_pras_results(filepath) # get number of samples @@ -387,7 +387,7 @@ def prm_increment_pras(sw, t, iteration, combined_periods_write, failed_regions) ## get load data dfload = reeds.io.read_file( os.path.join( - sw['casedir'],'ReEDS_Augur','augur_data',f'pras_load_{t}.h5'), + sw['casedir'],'handoff','reeds_data',f'pras_load_{t}.h5'), parse_timestamps=True ) @@ -536,11 +536,6 @@ def update_prm(sw, t, iteration, failed, combined_periods_write): def main(sw, t, iteration=0, logging=True): """ """ - #%% More imports and settings - site.addsitedir(os.path.join(sw['casedir'],'reeds','inputs')) - import hourly_writetimeseries - newstresspath = f'stress{t}i{iteration+1}' - #%% Write consolidated NEUE so far try: _neue_simple = get_and_write_neue(sw, write=True) @@ -573,7 +568,8 @@ def main(sw, t, iteration=0, logging=True): return #%% Write timeseries data for stress periods for the next iteration of ReEDS - hourly_writetimeseries.main( + newstresspath = f'stress{t}i{iteration+1}' + reeds.input_processing.hourly_writetimeseries.main( sw=sw, reeds_path=sw['reeds_path'], inputs_case=os.path.join(sw['casedir'], 'inputs_case'), periodtype=newstresspath, diff --git a/run.py b/run.py index fd08b810..7c88b7b2 100644 --- a/run.py +++ b/run.py @@ -537,17 +537,17 @@ def setup_sequential_year( ## check to see if the restart file exists OPATH.writelines(writeerrorcheck(os.path.join("g00files", savefile + ".g*"))) - ## Run Augur if it not the final solve year and if not skipping Augur + ## Run resource adequacy (RA) calculations if it not the final solve year and if not skipping RA if (( (cur_year < max(solveyears)) - and (next_year > int(caseSwitches['GSw_SkipAugurYear'])) + and (next_year > int(caseSwitches['GSw_SkipRAyear'])) ) or (cur_year == max(solveyears))): OPATH.writelines( - f"\npython Augur.py {next_year} {cur_year} {casedir}\n") - ## Check to make sure Augur ran successfully; quit otherwise + f"\npython {Path('reeds', 'resource_adequacy', 'ra_calcs.py')} {next_year} {cur_year} {casedir}\n") + ## Check to make sure RA ran successfully; quit otherwise OPATH.writelines( writeerrorcheck(os.path.join( - "ReEDS_Augur", "augur_data", f"ReEDS_Augur_{cur_year}.gdx"))) + "handoff", "reeds_data", f"ccdata_{cur_year}.gdx"))) ## delete the previous restart file unless we're keeping them if (cur_year > min(solveyears)) and (not int(caseSwitches['keep_g00_files'])): @@ -582,7 +582,7 @@ def setup_sequential( ### Write the tax credit phaseout call OPATH.writelines(f"python {Path('reeds','core','solve','1_tc_phaseout.py')} {cur_year} {casedir}\n\n") - ### Write the GAMS LP and Augur calls + ### Write the GAMS LP and resource adequacy calls if int(caseSwitches['GSw_PRM_StressIterateMax']): OPATH.writelines( f"python {Path('reeds','core','solve','solve.py')} {casedir} {cur_year}\n" @@ -600,12 +600,12 @@ def setup_sequential( ### multipliers aren't created until the first solve year is run) if cur_year == min(solveyears): OPATH.writelines( - f"\npython {os.path.join(casedir, 'reeds', 'inputs', 'check_inputs.py')} " + f"\npython {os.path.join(casedir, 'reeds', 'input_processing', 'check_inputs.py')} " f"{casedir}\n" ) OPATH.writelines(writescripterrorcheck('check_inputs.py')+'\n') - ### Run Augur plots in background + ### Run resource adequacy plots in background OPATH.writelines( f"python {Path('reeds','resource_adequacy','diagnostic_plots.py')} " f"--reeds_path={reeds_path} --casedir={casedir} --t={cur_year} &\n") @@ -613,9 +613,9 @@ def setup_sequential( def setup_intertemporal( caseSwitches, startiter, niter, ccworkers, - solveyears, endyear, batch_case, toLogGamsString, yearset_augur, OPATH, + solveyears, endyear, batch_case, toLogGamsString, modeledyears, OPATH, ): - ### beginning year is passed to augurbatch + ### beginning year is passed to rabatch begyear = min(solveyears) ### first save file from d_solveprep is just the case name savefile = batch_case @@ -658,10 +658,10 @@ def setup_intertemporal( ## start threads for cc/curt ## no need to run cc curt scripts for final iteration if i < niter-1: - ## batch out calls to augurbatch + ## batch out calls to rabatch OPATH.writelines( - "python augurbatch.py " + batch_case + " " + str(ccworkers) + " " - + yearset_augur + " " + savefile + " " + str(begyear) + " " + "python rabatch.py " + batch_case + " " + str(ccworkers) + " " + + modeledyears + " " + savefile + " " + str(begyear) + " " + str(endyear) + " " + caseSwitches['distpvscen'] + " " + str(caseSwitches['calc_csp_cc']) + " " + str(caseSwitches['timetype']) + " " @@ -674,10 +674,10 @@ def setup_intertemporal( ## the output file will be for the next iteration nextiter = i+1 gdxmergedfile = os.path.join( - "ReEDS_Augur","augur_data","ReEDS_Augur_merged_" + str(nextiter)) + 'handoff', 'reeds_data', f'ccdata_merged_{nextiter}') OPATH.writelines( - "gdxmerge "+os.path.join("ReEDS_Augur","augur_data","ReEDS_Augur*") - + " output=" + gdxmergedfile + ' \n') + 'gdxmerge ' + os.path.join('handoff', 'reeds_data', 'ccdata*') + + f' output={gdxmergedfile} \n') ## check to make sure previous calls were successful OPATH.writelines(writeerrorcheck(gdxmergedfile+".gdx")) @@ -690,7 +690,7 @@ def setup_intertemporal( def setup_window( caseSwitches, startiter, niter, ccworkers, reeds_path, - batch_case, toLogGamsString, yearset_augur, OPATH, + batch_case, toLogGamsString, modeledyears, OPATH, ): ### load the windows win_in = list(csv.reader(open( @@ -727,8 +727,8 @@ def setup_window( ## start threads for cc/curt OPATH.writelines(writeerrorcheck(os.path.join("g00files",savefile + ".g*"))) OPATH.writelines( - "python augurbatch.py " + batch_case + " " + str(ccworkers) + " " - + yearset_augur + " " + savefile + " " + str(begyear) + " " + "python rabatch.py " + batch_case + " " + str(ccworkers) + " " + + modeledyears + " " + savefile + " " + str(begyear) + " " + str(endyear) + " " + caseSwitches['distpvscen'] + " " + str(caseSwitches['calc_csp_cc']) + " " + str(caseSwitches['timetype']) + " " @@ -742,10 +742,10 @@ def setup_window( nextiter = i+1 ## create names for then merge the curt and cc gdx files gdxmergedfile = os.path.join( - "ReEDS_Augur","augur_data","ReEDS_Augur_merged_" + str(nextiter)) + 'handoff', 'reeds_data', f'ccdata_merged_{nextiter}') OPATH.writelines( - "gdxmerge " + os.path.join("ReEDS_Augur","augur_data","ReEDS_Augur*") - + " output=" + gdxmergedfile + ' \n') + 'gdxmerge ' + os.path.join('handoff', 'reeds_data', 'ccdata*') + + f' output={gdxmergedfile} \n') ## check to make sure previous calls were successful OPATH.writelines(writeerrorcheck(gdxmergedfile+".gdx")) restartfile = savefile @@ -1176,7 +1176,7 @@ def write_batch_script( shutil.copy2(os.path.join(reeds_path, cases_filename), casedir) ### Switches with values derived from other switches - ## Get hpc setting (used in Augur) + ## Determine whether we're running on the HPC caseSwitches['hpc'] = int(hpc) ## Get numclass from the max value in ivt caseSwitches['numclass'] = get_ivt_numclass( @@ -1203,7 +1203,7 @@ def write_batch_script( solveyears = [y for y in solveyears if (y <= endyear and y >= startyear)] - yearset_augur = os.path.join('inputs_case','modeledyears.csv') + modeledyears = os.path.join('inputs_case','modeledyears.csv') toLogGamsString = ' logOption=4 logFile=gamslog.txt appendLog=1 ' ## Copy code folders @@ -1214,9 +1214,9 @@ def write_batch_script( ignore=shutil.ignore_patterns('test'), ) - #make the augur_data folder - os.makedirs(os.path.join(casedir,'ReEDS_Augur','augur_data'), exist_ok=True) - os.makedirs(os.path.join(casedir,'ReEDS_Augur','PRAS'), exist_ok=True) + #make the reeds_data folder + os.makedirs(os.path.join(casedir,'handoff','reeds_data'), exist_ok=True) + os.makedirs(os.path.join(casedir,'handoff','PRAS'), exist_ok=True) ###### Replace files according to 'file_replacements' in cases. Ignore quotes in input text. # << is used to separate the file that is to be replaced from the file that is used @@ -1322,12 +1322,12 @@ def write_batch_script( elif caseSwitches['timetype'] == 'int': setup_intertemporal( caseSwitches, startiter, niter, ccworkers, - solveyears, endyear, batch_case, toLogGamsString, yearset_augur, OPATH, + solveyears, endyear, batch_case, toLogGamsString, modeledyears, OPATH, ) elif caseSwitches['timetype'] == 'win': setup_window( caseSwitches, startiter, niter, ccworkers, reeds_path, - batch_case, toLogGamsString, yearset_augur, OPATH, + batch_case, toLogGamsString, modeledyears, OPATH, ) ################################# From 10db2bd13e3ff1c02b19e7c32100a26e1ab81ad2 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:52:29 -0600 Subject: [PATCH 05/34] move b_sets.gms to reeds/core/setup --- reeds/core/setup/b_inputs.gms | 2 +- reeds/input_processing/copy_files.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reeds/core/setup/b_inputs.gms b/reeds/core/setup/b_inputs.gms index 4ad01569..c80bdc77 100644 --- a/reeds/core/setup/b_inputs.gms +++ b/reeds/core/setup/b_inputs.gms @@ -233,7 +233,7 @@ $include inputs_case%ds%val_hurdlereg.csv ; * Written by copy_files.py -$include b_sets.gms +$include reeds%ds%core%ds%setup%ds%b_sets.gms sets *The following two sets: diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py index 08d80dc7..b74e508a 100644 --- a/reeds/input_processing/copy_files.py +++ b/reeds/input_processing/copy_files.py @@ -930,7 +930,7 @@ def write_GAMS_sets(runfiles, reeds_path, inputs_case): for i, row in sets.iterrows() ]) + '\n$onlisting\n' # Write to file - with open(os.path.join(casedir,'b_sets.gms'), 'w') as f: + with open(os.path.join(casedir, 'reeds', 'core', 'setup', 'b_sets.gms'), 'w') as f: f.write(settext) From b90cef29f0b7b5abce849ad72d13426f064338be Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:46:05 -0600 Subject: [PATCH 06/34] move reeds.prase back to reeds.inputs; move reeds.resource_adequacy.ra functions back to reeds.resource_adequacy.stress_periods --- docs/source/model_documentation.md | 2 +- hourlize/README.md | 2 +- hourlize/resource.py | 2 +- inputs/transmission/README.md | 2 +- inputs/zones/README.md | 2 +- postprocessing/input_plots.py | 2 +- reeds/__init__.py | 10 +- reeds/core/solve/solve.py | 2 +- reeds/input_processing/copy_files.py | 3 +- reeds/input_processing/transmission.py | 6 +- reeds/input_processing/writesupplycurves.py | 2 +- reeds/{parse.py => inputs.py} | 2 +- reeds/prasplots.py | 2 +- reeds/resource_adequacy/__init__.py | 1 - reeds/resource_adequacy/ra.py | 125 ------------------- reeds/resource_adequacy/stress_periods.py | 128 ++++++++++++++++++-- run.py | 10 +- 17 files changed, 143 insertions(+), 160 deletions(-) rename reeds/{parse.py => inputs.py} (99%) delete mode 100644 reeds/resource_adequacy/ra.py diff --git a/docs/source/model_documentation.md b/docs/source/model_documentation.md index b56bfa5c..83503b8c 100644 --- a/docs/source/model_documentation.md +++ b/docs/source/model_documentation.md @@ -1922,7 +1922,7 @@ import reeds # GSw_ZoneSet can be any of the supported values listed in the "Choices" column # for the `GSw_ZoneSet` switch in `cases.csv` GSw_ZoneSet = 'z132' -reeds.parse.get_itls(GSw_ZoneSet=GSw_ZoneSet) +reeds.inputs.get_itls(GSw_ZoneSet=GSw_ZoneSet) ``` ``` diff --git a/hourlize/README.md b/hourlize/README.md index 80d2565c..faf38431 100644 --- a/hourlize/README.md +++ b/hourlize/README.md @@ -185,7 +185,7 @@ The `resource.py` script follows the following logic (in order of execution): * Existing and planned sites from a generator database (`existing_sites`) are assigned to supply curve points for exogenous and prescribed capacity outputs respectively. * If we have minimum capacity thresholds for the supply curve points, these are applied to further filter the supply curve. 1. `add_classes()` - * A 'class' column is added to the supply curve and filled with the associated class. Classes can be based on statically defined conditions for columns in the supply curve (`class_path`). Otherwise (or layered on top of static class definitions), dynamic classes can be assigned (`class_bin`=true) using a binning method (`class_bin_method`, e.g. "kmeans"), a number of bins (`class_bin_num`), and the supply curve column to bin (`class_bin_col`). The binning logic itself is in `reeds.parse.get_bin()`. The current default classes for onshore wind and utility-scale PV are based on national k-means clustering of average annual capacity factor (where higher class number corresponds with higher annual CF). Offshore wind, by contrast, uses statically defined classes from `hourlize/inputs/resource/wind-ofs_resource_classes.csv`. + * A 'class' column is added to the supply curve and filled with the associated class. Classes can be based on statically defined conditions for columns in the supply curve (`class_path`). Otherwise (or layered on top of static class definitions), dynamic classes can be assigned (`class_bin`=true) using a binning method (`class_bin_method`, e.g. "kmeans"), a number of bins (`class_bin_num`), and the supply curve column to bin (`class_bin_col`). The binning logic itself is in `reeds.inputs.get_bin()`. The current default classes for onshore wind and utility-scale PV are based on national k-means clustering of average annual capacity factor (where higher class number corresponds with higher annual CF). Offshore wind, by contrast, uses statically defined classes from `hourlize/inputs/resource/wind-ofs_resource_classes.csv`. 1. `add_cost()` * A column of overall supply curve costs is added to the supply curve (`supply_curve_cost_per_mw`), as well as certain components of that cost (e.g. `trans_adder_per_mw` and `capital_adder_per_mw`). Logic for these costs depends on `tech`, and the value of `cost_out` in config (e.g. `combined_eos_trans` for onshore wind). * A column of overall supply curve costs is added to the supply curve (`supply_curve_cost_per_mw`), as well as certain components of that cost (e.g. `trans_adder_per_mw` and `capital_adder_per_mw`). Logic for these costs depends on `tech`, and the value of `cost_out` in config (e.g. `combined_eos_trans` for onshore wind). diff --git a/hourlize/resource.py b/hourlize/resource.py index 62064b96..1566e5f5 100644 --- a/hourlize/resource.py +++ b/hourlize/resource.py @@ -541,7 +541,7 @@ def add_classes(df_sc, class_path, class_bin, class_bin_col, class_bin_method, c df_sc .groupby(['class_orig'], sort=False) .apply( - reeds.parse.get_bin, + reeds.inputs.get_bin, bin_col=class_bin_col, bin_out_col='class_bin', weight_col='capacity', diff --git a/inputs/transmission/README.md b/inputs/transmission/README.md index 8f3454af..e349a7d7 100644 --- a/inputs/transmission/README.md +++ b/inputs/transmission/README.md @@ -33,7 +33,7 @@ Calculated using the [TSC](https://github.nrel.gov/pbrown/TSC) model as describe import reeds ## GSw_ZoneSet can be any of the supported zone resolutions listed in the `GSw_ZoneSet` row of `cases.csv` GSw_ZoneSet = 'z134' - reeds.parse.get_itls(GSw_ZoneSet=GSw_ZoneSet) + reeds.inputs.get_itls(GSw_ZoneSet=GSw_ZoneSet) ``` - `rev_transmission_basecost.csv`: Base transmission costs (before terrain multipliers) used in reV. diff --git a/inputs/zones/README.md b/inputs/zones/README.md index 904afa7d..54159636 100644 --- a/inputs/zones/README.md +++ b/inputs/zones/README.md @@ -56,7 +56,7 @@ Once you're happy with your zone and hierarchy level definitions, run the follow - Creates the `zonehash.csv` file - Rewrites the `itl_NARIS.csv` file (existing data in the file are preserved, so you should only see new rows added to the bottom of the file) 1. Add the new zone definition to the choices for the `GSw_ZoneSet` switch in `cases.csv` -1. To make sure it worked (or just to read the ITLs in general), you can run `import reeds` and then `reeds.parse.get_itls(GSw_ZoneSet='your new zoneset name')` in Python with the `reeds2` conda environment activated. +1. To make sure it worked (or just to read the ITLs in general), you can run `import reeds` and then `reeds.inputs.get_itls(GSw_ZoneSet='your new zoneset name')` in Python with the `reeds2` conda environment activated. 1. Try a ReEDS run. - The following checks will be performed; if any of them fail, the run will stop. - `b2b.csv`, `county2zone.csv`, `hierarchy.csv`, `zonehash.csv`, `interfaces_r.csv`, and `interfaces_transgrp.csv` should all be preset in the `inputs/zones/{GSw_ZoneSet}` folder diff --git a/postprocessing/input_plots.py b/postprocessing/input_plots.py index 9b3c51a4..5507c141 100644 --- a/postprocessing/input_plots.py +++ b/postprocessing/input_plots.py @@ -117,7 +117,7 @@ def plot_profile( ## Parse inputs sw = reeds.io.get_switches(case) t = reeds.io.get_years(case)[-1] if year in [0, None, 'last'] else year - rs = reeds.parse.parse_regions((region if region else case), case) + rs = reeds.inputs.parse_regions((region if region else case), case) if weatheryears is None: weatheryears = sw.resource_adequacy_years_list elif isinstance(weatheryears, int): diff --git a/reeds/__init__.py b/reeds/__init__.py index 5f165aba..fc27b8ec 100644 --- a/reeds/__init__.py +++ b/reeds/__init__.py @@ -1,19 +1,19 @@ from . import checks as checks +from . import core as core from . import financials as financials +from . import hpc as hpc +from . import input_processing as input_processing +from . import inputs as inputs from . import io as io from . import log as log from . import output_calc as output_calc -from . import parse as parse from . import plots as plots from . import prasplots as prasplots from . import reedsplots as reedsplots from . import remote as remote from . import report_utils as report_utils +from . import resource_adequacy as resource_adequacy from . import spatial as spatial from . import techs as techs from . import timeseries as timeseries from . import units as units -from . import core -from . import hpc -from . import input_processing -from . import resource_adequacy diff --git a/reeds/core/solve/solve.py b/reeds/core/solve/solve.py index e8bb829c..4042d288 100644 --- a/reeds/core/solve/solve.py +++ b/reeds/core/solve/solve.py @@ -42,7 +42,7 @@ def run_reeds(casepath, t, onlygams=False, iteration=0): glob(os.path.join(casepath,'g00files',f"{batch_case}_{tprev[t]}i*")) )[-1] - cmd_gams = reeds.parse.solvestring_sequential( + cmd_gams = reeds.inputs.solvestring_sequential( batch_case=batch_case, caseSwitches=sw, cur_year=t, diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py index b74e508a..0f5b5c9a 100644 --- a/reeds/input_processing/copy_files.py +++ b/reeds/input_processing/copy_files.py @@ -13,7 +13,6 @@ import h5py from pathlib import Path # Local Imports -from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent)) import reeds @@ -1406,7 +1405,7 @@ def write_miscellaneous_files( os.path.join(inputs_case,'co2_tax.csv') ) - solveyears = reeds.parse.parse_yearset(sw['yearset']) + solveyears = reeds.inputs.parse_yearset(sw['yearset']) if int(sw['startyear']) not in solveyears: solveyears.append(int(sw['startyear'])) solveyears = sorted(solveyears) diff --git a/reeds/input_processing/transmission.py b/reeds/input_processing/transmission.py index 6d137faf..0c9e674c 100644 --- a/reeds/input_processing/transmission.py +++ b/reeds/input_processing/transmission.py @@ -66,7 +66,7 @@ def get_trancap_init(case, networksource='NARIS2024', level='r'): """ sw = reeds.io.get_switches(case) trancap_init_ac = ( - reeds.parse.get_itls(case, level=level, GSw_ZoneSet=sw.GSw_ZoneSet) + reeds.inputs.get_itls(case, level=level, GSw_ZoneSet=sw.GSw_ZoneSet) [['r', 'rr', 'MW_forward', 'MW_reverse']] .assign(trtype='AC') ) @@ -91,8 +91,8 @@ def get_trancap_init(case, networksource='NARIS2024', level='r'): ### DC if level == 'r': ## transgrp capacity is only defined for AC - hvdc = reeds.parse.map_hvdc_lines_to_interfaces(case).assign(trtype='LCC') - b2b = reeds.parse.get_b2b(case).assign(trtype='B2B') + hvdc = reeds.inputs.map_hvdc_lines_to_interfaces(case).assign(trtype='LCC') + b2b = reeds.inputs.get_b2b(case).assign(trtype='B2B') ## DC capacity is only defined in one direction, ## so duplicate it for the opposite direction trancap_init_nonac_undup = pd.concat([hvdc, b2b])[['r', 'rr', 'trtype', 'MW']] diff --git a/reeds/input_processing/writesupplycurves.py b/reeds/input_processing/writesupplycurves.py index 11af4f6d..a0df3bbe 100644 --- a/reeds/input_processing/writesupplycurves.py +++ b/reeds/input_processing/writesupplycurves.py @@ -115,7 +115,7 @@ def agg_supplycurve( dfin = ( dfin .groupby(['region','class'], sort=False, group_keys=True) - .apply(reeds.parse.get_bin, numbins_tech, bin_method, bin_col) + .apply(reeds.inputs.get_bin, numbins_tech, bin_method, bin_col) .reset_index(drop=True) .sort_values('sc_point_gid') ) diff --git a/reeds/parse.py b/reeds/inputs.py similarity index 99% rename from reeds/parse.py rename to reeds/inputs.py index f51fc857..416bebbd 100644 --- a/reeds/parse.py +++ b/reeds/inputs.py @@ -87,7 +87,7 @@ def parse_yearset(yearset:str) -> list: "For formatting notes and examples, run the following commands:\n" "$ python\n" ">>> import reeds\n" - ">>> help(reeds.parse.parse_yearset)" + ">>> help(reeds.inputs.parse_yearset)" ) if not re.match(pattern, yearset): err = f"Invalid yearset ({yearset}); must match {pattern}. {helper}" diff --git a/reeds/prasplots.py b/reeds/prasplots.py index 00440f1c..ba09429d 100644 --- a/reeds/prasplots.py +++ b/reeds/prasplots.py @@ -339,7 +339,7 @@ def plot_pras_samples( reeds.io.get_last_iteration(case, t) if iteration in [None, 'last'] else iteration ) - rs = reeds.parse.parse_regions(region, case) + rs = reeds.inputs.parse_regions(region, case) bokehcolors, plotorder = reeds.reedsplots.get_tech_colors_order(order='fuel_storage_vre') diff --git a/reeds/resource_adequacy/__init__.py b/reeds/resource_adequacy/__init__.py index a1507d1e..63c5372a 100644 --- a/reeds/resource_adequacy/__init__.py +++ b/reeds/resource_adequacy/__init__.py @@ -1,5 +1,4 @@ from . import capacity_credit as capacity_credit from . import prep_data as prep_data -from . import ra as ra from . import ra_calcs as ra_calcs from . import stress_periods as stress_periods diff --git a/reeds/resource_adequacy/ra.py b/reeds/resource_adequacy/ra.py deleted file mode 100644 index c1da83bd..00000000 --- a/reeds/resource_adequacy/ra.py +++ /dev/null @@ -1,125 +0,0 @@ -### Imports -import os -import sys -import numpy as np -import pandas as pd -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import reeds - - -### Functions -def get_pras_eue(case, t, iteration=0): - """ - """ - ### Get PRAS outputs - dfpras = reeds.io.read_pras_results( - os.path.join(case, 'handoff', 'PRAS', f"PRAS_{t}i{iteration}.h5") - ) - ### Create the time index - sw = reeds.io.get_switches(case) - dfpras.index = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) - - ### Keep the EUE columns by zone - eue_tail = '_EUE' - dfeue = dfpras[[ - c for c in dfpras - if (c.endswith(eue_tail) and not c.startswith('USA')) - ]].copy() - ## Drop the tailing _EUE - dfeue = dfeue.rename( - columns=dict(zip(dfeue.columns, [c[:-len(eue_tail)] for c in dfeue]))) - - return dfeue - - -def get_eue_periods( - case, t, iteration=0, - hierarchy_level='transgrp', - stress_metric='EUE', - period_agg_method='sum', - ): - """_summary_ - - Args: - sw (pd.series): ReEDS switches for this run. - t (int): Model solve year. - iteration (int, optional): Iteration number of this solve year. Defaults to 0. - hierarchy_level (str, optional): column of hierarchy.csv specifying the spatial - level over which to calculate stress_metric. Defaults to 'country'. - stress_metric (str, optional): 'EUE' or 'NEUE'. Defaults to 'EUE'. - period_agg_method (str, optional): 'sum' or 'max', indicating how to aggregate - over the hours in each period. Defaults to 'sum'. - - Raises: - NotImplementedError: if invalid value for stress_metric or GSw_PRM_StressModel - - Returns: - pd.DataFrame: Table of periods sorted in descending order by stress metric. - """ - sw = reeds.io.get_switches(case) - ### Get the region aggregator - rmap = reeds.io.get_rmap(case=case, hierarchy_level=hierarchy_level) - - ### Get EUE from PRAS - dfeue = get_pras_eue(case=case, t=t, iteration=iteration) - ## Aggregate to hierarchy_level - dfeue = ( - dfeue - .rename_axis('r', axis=1).rename_axis('h', axis=0) - .rename(columns=rmap).groupby(axis=1, level=0).sum() - ) - - ###### Calculate the stress metric by period - if stress_metric.upper() == 'EUE': - ### Aggregate according to period_agg_method - dfmetric_period = ( - dfeue - .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) - elif stress_metric.upper() == 'NEUE': - ### Get load at hierarchy_level - dfload = reeds.io.read_h5py_file( - os.path.join( - case,'handoff','reeds_data',f'pras_load_{t}.h5') - ).rename(columns=rmap).groupby(level=0, axis=1).sum() - dfload.index = dfeue.index - - ### Recalculate NEUE [ppm] and aggregate appropriately - if period_agg_method == 'sum': - dfmetric_period = ( - dfeue - .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) / ( - dfload - .groupby([dfload.index.year, dfload.index.month, dfload.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) * 1e6 - elif period_agg_method == 'max': - dfmetric_period = ( - (dfeue / dfload) - .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) - .agg(period_agg_method) - .rename_axis(['y','m','d']) - ) * 1e6 - - ### Sort and drop zeros and duplicates - dfmetric_top = ( - dfmetric_period.stack('r') - .sort_values(ascending=False) - .replace(0,np.nan).dropna() - .reset_index().drop_duplicates(['y','m','d'], keep='first') - .set_index(['y','m','d','r']).squeeze(1).rename(stress_metric) - .reset_index('r') - ) - ## Convert to timestamp, then to ReEDS period - dfmetric_top['actual_period'] = [ - reeds.timeseries.timestamp2h(pd.Timestamp(*d), sw['GSw_HourlyType']).split('h')[0] - for d in dfmetric_top.index.values - ] - - return dfmetric_top diff --git a/reeds/resource_adequacy/stress_periods.py b/reeds/resource_adequacy/stress_periods.py index 55801f9a..424fd6d4 100644 --- a/reeds/resource_adequacy/stress_periods.py +++ b/reeds/resource_adequacy/stress_periods.py @@ -1,18 +1,11 @@ #%%### General imports import os -import site import traceback import pandas as pd import numpy as np from glob import glob import re import matplotlib.pyplot as plt -### Local imports - -## use this to import reeds when running locally for debugging -# import site -# this_dir_path = os.path.dirname(os.path.realpath(__file__)) -# site.addsitedir(os.path.join(this_dir_path, "..")) import reeds @@ -24,6 +17,123 @@ #%%### Functions +def get_pras_eue(case, t, iteration=0): + """ + """ + ### Get PRAS outputs + dfpras = reeds.io.read_pras_results( + os.path.join(case, 'handoff', 'PRAS', f"PRAS_{t}i{iteration}.h5") + ) + ### Create the time index + sw = reeds.io.get_switches(case) + dfpras.index = reeds.timeseries.get_timeindex(sw['resource_adequacy_years']) + + ### Keep the EUE columns by zone + eue_tail = '_EUE' + dfeue = dfpras[[ + c for c in dfpras + if (c.endswith(eue_tail) and not c.startswith('USA')) + ]].copy() + ## Drop the tailing _EUE + dfeue = dfeue.rename( + columns=dict(zip(dfeue.columns, [c[:-len(eue_tail)] for c in dfeue]))) + + return dfeue + + +def get_eue_periods( + case, t, iteration=0, + hierarchy_level='transgrp', + stress_metric='EUE', + period_agg_method='sum', + ): + """_summary_ + + Args: + sw (pd.series): ReEDS switches for this run. + t (int): Model solve year. + iteration (int, optional): Iteration number of this solve year. Defaults to 0. + hierarchy_level (str, optional): column of hierarchy.csv specifying the spatial + level over which to calculate stress_metric. Defaults to 'country'. + stress_metric (str, optional): 'EUE' or 'NEUE'. Defaults to 'EUE'. + period_agg_method (str, optional): 'sum' or 'max', indicating how to aggregate + over the hours in each period. Defaults to 'sum'. + + Raises: + NotImplementedError: if invalid value for stress_metric or GSw_PRM_StressModel + + Returns: + pd.DataFrame: Table of periods sorted in descending order by stress metric. + """ + sw = reeds.io.get_switches(case) + ### Get the region aggregator + rmap = reeds.io.get_rmap(case=case, hierarchy_level=hierarchy_level) + + ### Get EUE from PRAS + dfeue = get_pras_eue(case=case, t=t, iteration=iteration) + ## Aggregate to hierarchy_level + dfeue = ( + dfeue + .rename_axis('r', axis=1).rename_axis('h', axis=0) + .rename(columns=rmap).groupby(axis=1, level=0).sum() + ) + + ###### Calculate the stress metric by period + if stress_metric.upper() == 'EUE': + ### Aggregate according to period_agg_method + dfmetric_period = ( + dfeue + .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) + elif stress_metric.upper() == 'NEUE': + ### Get load at hierarchy_level + dfload = reeds.io.read_h5py_file( + os.path.join( + case,'handoff','reeds_data',f'pras_load_{t}.h5') + ).rename(columns=rmap).groupby(level=0, axis=1).sum() + dfload.index = dfeue.index + + ### Recalculate NEUE [ppm] and aggregate appropriately + if period_agg_method == 'sum': + dfmetric_period = ( + dfeue + .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) / ( + dfload + .groupby([dfload.index.year, dfload.index.month, dfload.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) * 1e6 + elif period_agg_method == 'max': + dfmetric_period = ( + (dfeue / dfload) + .groupby([dfeue.index.year, dfeue.index.month, dfeue.index.day]) + .agg(period_agg_method) + .rename_axis(['y','m','d']) + ) * 1e6 + + ### Sort and drop zeros and duplicates + dfmetric_top = ( + dfmetric_period.stack('r') + .sort_values(ascending=False) + .replace(0,np.nan).dropna() + .reset_index().drop_duplicates(['y','m','d'], keep='first') + .set_index(['y','m','d','r']).squeeze(1).rename(stress_metric) + .reset_index('r') + ) + ## Convert to timestamp, then to ReEDS period + dfmetric_top['actual_period'] = [ + reeds.timeseries.timestamp2h(pd.Timestamp(*d), sw['GSw_HourlyType']).split('h')[0] + for d in dfmetric_top.index.values + ] + + return dfmetric_top + + def plot_eue_diagnostics(sw, t, iteration, high_eue_periods): try: dates = ( @@ -94,7 +204,7 @@ def get_annual_neue(case, t, iteration=0): """ """ ### Get EUE from PRAS - dfeue = reeds.resource_adequacy.ra.get_pras_eue(case=case, t=t, iteration=iteration) + dfeue = get_pras_eue(case=case, t=t, iteration=iteration) ### Get load (for calculating NEUE) dfload = reeds.io.read_h5py_file( @@ -236,7 +346,7 @@ def get_eue_sorted_periods(sw, t, iteration): ## Example: criterion = 'transgrp_10_EUE_sum' (hierarchy_level, ppm, stress_metric, period_agg_method) = criterion.split('_') - eue_periods = reeds.resource_adequacy.get_eue_periods( + eue_periods = get_eue_periods( case=sw.casedir, t=t, iteration=iteration, hierarchy_level=hierarchy_level, stress_metric=stress_metric, diff --git a/run.py b/run.py index 7c88b7b2..c0516f98 100644 --- a/run.py +++ b/run.py @@ -283,7 +283,7 @@ def check_compatibility(sw): .format('\n'.join(err_switch_configs)) ) - reeds.parse.validate_zoneset(sw['GSw_ZoneSet']) + reeds.inputs.validate_zoneset(sw['GSw_ZoneSet']) ### Aggregation if (sw['GSw_RegionResolution'] != 'aggreg') and (int(sw['GSw_NumCSPclasses']) != 12): @@ -422,7 +422,7 @@ def check_compatibility(sw): f"resource_adequacy_years={sw['resource_adequacy_years']}" ) - solveyears = reeds.parse.parse_yearset(sw['yearset']) + solveyears = reeds.inputs.parse_yearset(sw['yearset']) if int(sw['endyear']) not in solveyears: err = f"`endyear` = {sw['endyear']} but must be in `yearset`: {sw['yearset']}" raise ValueError(err) @@ -523,7 +523,7 @@ def setup_sequential_year( if (cur_year >= min(solveyears)): ## solve one year OPATH.writelines( - reeds.parse.solvestring_sequential( + reeds.inputs.solvestring_sequential( batch_case, caseSwitches, cur_year, next_year, prev_year, restartfile, toLogGamsString, hpc, @@ -853,7 +853,7 @@ def setupEnvironment( else: cases_filename = f'cases_{cases_suffix}.csv' - df_cases = reeds.parse.parse_cases( + df_cases = reeds.inputs.parse_cases( cases_filename=cases_filename, single=single, skip_checks=skip_checks, @@ -1192,7 +1192,7 @@ def write_batch_script( pd.Series(caseSwitches).to_csv( os.path.join(inputs_case,'switches.csv'), header=False) - solveyears = reeds.parse.parse_yearset(caseSwitches['yearset']) + solveyears = reeds.inputs.parse_yearset(caseSwitches['yearset']) # If start year is not in solveyears, start year is added into solveyears set startyear = int(caseSwitches['startyear']) From 1ce6daafd1c153c0ed4a28324f5c98c733e33748 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Sun, 19 Apr 2026 08:41:20 -0600 Subject: [PATCH 07/34] move auto-generated code to {casename}/autocode; fix report paths --- cases.csv | 2 +- docs/source/developer_best_practices.md | 2 +- postprocessing/run_pcm.py | 2 +- postprocessing/uncertainty_plots.py | 2 +- reeds/core/setup/b_inputs.gms | 4 ++-- reeds/core/setup/c_model.gms | 2 +- reeds/core/terminus/report.gms | 12 ++++++------ reeds/core/terminus/report_dump.py | 7 +++---- reeds/input_processing/copy_files.py | 24 ++++++++++++++---------- reeds/io.py | 8 ++++---- run.py | 1 + tests/test_outputs.py | 16 ++++++++-------- 12 files changed, 43 insertions(+), 39 deletions(-) diff --git a/cases.csv b/cases.csv index 520c2020..9b69adf4 100644 --- a/cases.csv +++ b/cases.csv @@ -85,7 +85,7 @@ GSw_BinOM,Turn on/off binned FOM and VOM for each historical binned vintage,0; 1 GSw_Biopower,Turn on/off biopower,0; 1,1, GSw_BioSupply,Multiplier to adjust total biomass supply,N/A,1, GSw_BioTransportCost,Biomass collection and transport costs ($2004 per dry ton),N/A,22, -GSw_calc_powfrac,Switch to compute powfrac in e_report and e_powfrac_calc - dramatically reduces calculation times with hourly resolution,0; 1,0, +GSw_calc_powfrac,Compute powfrac in report.gms and powfrac_calc.gms - dramatically increases calculation times with hourly resolution,0; 1,0, GSw_Canada,"Turn canada off [0], or use flexible (dispatchable-hydro-like) representation [1]",0; 1,1, GSw_CarbTax,Turn on/off CO2 tax,0; 1,0, GSw_CarbTaxOption,Choose the co2_tax input csv file (see inputs\emission_constraints),,default, diff --git a/docs/source/developer_best_practices.md b/docs/source/developer_best_practices.md index 403f6602..544692ab 100644 --- a/docs/source/developer_best_practices.md +++ b/docs/source/developer_best_practices.md @@ -107,7 +107,7 @@ _Some exceptions to this might exist due to number scaling (e.g., emission rates * GAMS functions such as sum, max, smax, etc. should use {}; Example: avg_outage(i) = sum{h,hours(h)*outage(i,h)} / 8760 ; * When including the semicolon on the end of a line there should be a space between the semicolon and the last character of the line (see previous example) * When using `/ /` for a parameter declaration, place the closing semicolon on the same line as the final slash: `/ ;` - * Sums outside of equations (e.g., in e_reports) need not be split over multiple lines if they do not exceed the line limit + * Sums outside of equations (e.g., in `report.gms`) need not be split over multiple lines if they do not exceed the line limit * Do not use hard-coded numbers in equations or calculations. Values should be assigned to an appropriate parameter name that is subsequently used in the code. * Large input data tables should be loaded from individual data files for each table, preferably in *.csv format. Large data tables should not be manually written into the code but can be written dynamically by scripts or inserted with a $include statement. * Compile-time conditionals should always use a tag (period + tag name) to clearly define the relationships between compile-time conditional statements. Failure to do so hurts readability sometimes leads to compilation errors. Example: diff --git a/postprocessing/run_pcm.py b/postprocessing/run_pcm.py index b95ab40c..8e43a703 100644 --- a/postprocessing/run_pcm.py +++ b/postprocessing/run_pcm.py @@ -266,7 +266,7 @@ def main(casepath, t, switch_mods=switch_mods_default, label='', overwrite=False # %% Dump gdx to h5 ## Get new file names if applicable dfparams = pd.read_csv( - os.path.join(casepath, "e_report_params.csv"), + os.path.join(casepath, 'autocode', 'report_params.csv'), comment="#", index_col="param", ) diff --git a/postprocessing/uncertainty_plots.py b/postprocessing/uncertainty_plots.py index d8e391d7..1cc51802 100644 --- a/postprocessing/uncertainty_plots.py +++ b/postprocessing/uncertainty_plots.py @@ -486,7 +486,7 @@ def __init__(self, cases_dict: dict): # --- Fetch methods for output data --- self.OUT_FETCH_METHODS = { - # Naming as in e_report_params.csv + # Naming as in report_params.csv "cap_out": self._fetch_cap_out, "gen_ann": self._fetch_gen_ann, "tran_mi_out_detail": self._fetch_tran_mi_out_detail, diff --git a/reeds/core/setup/b_inputs.gms b/reeds/core/setup/b_inputs.gms index c80bdc77..ebf9f825 100644 --- a/reeds/core/setup/b_inputs.gms +++ b/reeds/core/setup/b_inputs.gms @@ -233,7 +233,7 @@ $include inputs_case%ds%val_hurdlereg.csv ; * Written by copy_files.py -$include reeds%ds%core%ds%setup%ds%b_sets.gms +$include autocode%ds%b_sets.gms sets *The following two sets: @@ -6503,7 +6503,7 @@ allow_cap_up(i,v,r,rscbin,t)$[valcap(i,v,r,t)$cap_cap_up(i,v,r,rscbin,t)$(t.val> allow_ener_up(i,v,r,rscbin,t)$[valcap(i,v,r,t)$cap_ener_up(i,v,r,rscbin,t)$(t.val>=Sw_UpgradeYear)] = yes ; -* Track the initial amount of m_rsc_dat capacity to compare in e_report +* Track the initial amount of m_rsc_dat capacity to compare in report.gms * We adjust upwards by small amounts given potential for infeasibilities * in very tiny amounts and thus track the extent of the adjustments parameter m_rsc_dat_init(r,i,rscbin) "--MW-- Initial amount of resource supply curve capacity to compare with final amounts after adjustments" ; diff --git a/reeds/core/setup/c_model.gms b/reeds/core/setup/c_model.gms index 029d53f1..6c463aa1 100644 --- a/reeds/core/setup/c_model.gms +++ b/reeds/core/setup/c_model.gms @@ -2381,7 +2381,7 @@ eq_offshore_no_backflow(r,rr,trtype,h,t) * --------------------------------------------------------------------------- * Because EMIT is only evaluated for emit_modeled, the full emissions need to be -* calculated in the e_report rather than relying on the EMIT variable +* calculated in report.gms rather than relying on the EMIT variable eq_emit_accounting(etype,e,r,t)$[emit_modeled(e,r,t)$tmodel(t)].. EMIT(etype,e,r,t) diff --git a/reeds/core/terminus/report.gms b/reeds/core/terminus/report.gms index ebd14d10..d8e1f0c6 100644 --- a/reeds/core/terminus/report.gms +++ b/reeds/core/terminus/report.gms @@ -113,10 +113,10 @@ h2_demand_type / "electricity", "cross-sector"/ ; -* Parameter definitions in the following file are read from e_report_params.csv +* Parameter definitions in the following file are read from report_params.csv * and parsed in copy_files.py. -* All output parameters should be defined in e_report_params.csv. -$include e_report_params.gms +* All output parameters should be defined in report_params.csv. +$include autocode%ds%report_params.gms * Restrict operational outputs to representative timeslices and seasons h(h)$[not h_rep(h)] = no ; @@ -2077,15 +2077,15 @@ h2_usage(r,h,t)$tmodel_new(t) = *======================================== $ifthene.powerfrac %GSw_calc_powfrac% == 1 -$include e_powfrac_calc.gms +$include reeds%ds%core%ds%terminus%ds%e_powfrac_calc.gms $endif.powerfrac *======================================== * Dump results *======================================== -* The parameter list in the following file is read from e_report_params.csv +* The parameter list in the following file is read from report_params.csv * and parsed in copy_files.py execute_unload "outputs%ds%rep_%fname%.gdx" -$include e_report_paramlist.txt +$include autocode%ds%report_paramlist.txt ; diff --git a/reeds/core/terminus/report_dump.py b/reeds/core/terminus/report_dump.py index 03287d96..089482f3 100644 --- a/reeds/core/terminus/report_dump.py +++ b/reeds/core/terminus/report_dump.py @@ -7,11 +7,10 @@ import os import traceback import sys - -# Third-party packages import pandas as pd import gdxpds - +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent.parent.parent)) import reeds @@ -259,7 +258,7 @@ def postprocess_outputs(case, outputs_path=None, verbose=0): ### Get new file names if applicable dfparams = pd.read_csv( - os.path.join(case, "e_report_params.csv"), + os.path.join(case, 'autocode', 'report_params.csv'), comment="#", index_col="param", ) diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py index 0f5b5c9a..678bd101 100644 --- a/reeds/input_processing/copy_files.py +++ b/reeds/input_processing/copy_files.py @@ -769,14 +769,14 @@ def scalar_csv_to_txt(path_to_scalar_csv): return dfscalar -def param_csv_to_txt(path_to_param_csv, writelist=True): +def param_csv_to_txt(infilepath, outdirpath, writelist=True): """ Write a parameter csv to GAMS-readable text Format of csv should be: parameter(indices),units,comment """ # Load the csv dfparams = pd.read_csv( - path_to_param_csv, + infilepath, index_col='param', comment='#', ) # Create the GAMS-readable param definition string (comments must be ≤255 characters) @@ -786,15 +786,17 @@ def param_csv_to_txt(path_to_param_csv, writelist=True): for i, row in dfparams.loc[dfparams.input != 1].iterrows() ]) # Write it to a file, replacing .csv with .gms in the filename - param_gms_path = path_to_param_csv.replace('.csv','.gms') + param_gms_path = Path(outdirpath, Path(infilepath).stem + '.gms') with open(param_gms_path, 'w') as w: w.write(paramtext) # Write the list of parameters if desired if writelist: # Create the GAMS-readable list of parameters (without indices) paramlist = '\n'.join(dfparams.index.map(lambda x: x.split('(')[0]).tolist()) - param_list_path = ( - path_to_param_csv.replace('params','paramlist').replace('.csv','.txt')) + param_list_path = Path( + outdirpath, + Path(infilepath).stem.replace('params','paramlist') + '.txt' + ) with open(param_list_path, 'w') as w: w.write(paramlist) @@ -929,7 +931,7 @@ def write_GAMS_sets(runfiles, reeds_path, inputs_case): for i, row in sets.iterrows() ]) + '\n$onlisting\n' # Write to file - with open(os.path.join(casedir, 'reeds', 'core', 'setup', 'b_sets.gms'), 'w') as f: + with open(os.path.join(casedir, 'autocode', 'b_sets.gms'), 'w') as f: f.write(settext) @@ -987,10 +989,6 @@ def write_non_region_file(filename, filepath, src_file, dir_dst, sw, regions_and else: shutil.copy(src_file, os.path.join(dir_dst,filename)) - if filename == 'e_report_params.csv': - # Rewrite e_report_params as GAMS-readable definition - param_csv_to_txt(os.path.join(dir_dst,'e_report_params.csv')) - if filename == 'scalars.csv': # Rewrite scalars.csv as GAMS-readable definition scalars = reeds.io.get_scalars(full=True) @@ -1554,6 +1552,12 @@ def write_miscellaneous_files( ).rename_axis('tech').dropna().astype(int) unitsize.to_csv(fpath_out) + # Rewrite report_params as GAMS-readable definitions + param_csv_to_txt( + infilepath=Path(reeds_path, 'reeds', 'core', 'terminus', 'report_params.csv'), + outdirpath=Path(inputs_case, '..', 'autocode'), + ) + def generate_maps_gpkg(inputs_case): """ diff --git a/reeds/io.py b/reeds/io.py index 71539561..3e2cc7c2 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -490,12 +490,12 @@ def read_output( df[col] = df[col].str.decode('utf-8') except KeyError: ## Empty dataframes aren't written to h5 file, so make one ourselves - e_report_params = pd.read_csv( - os.path.join(case, 'e_report_params.csv'), + report_params = pd.read_csv( + os.path.join(case, 'autocode', 'report_params.csv'), comment='#', ) - _index = e_report_params.loc[ - e_report_params.param.map(lambda x: x.split('(')[0]) == key, 'param' + _index = report_params.loc[ + report_params.param.map(lambda x: x.split('(')[0]) == key, 'param' ].squeeze() if not len(_index): raise KeyError(f"{filename} is not in {h5path}") diff --git a/run.py b/run.py index c0516f98..08b12584 100644 --- a/run.py +++ b/run.py @@ -1091,6 +1091,7 @@ def write_batch_script( #%% Set up case-specific directory structure os.makedirs(inputs_case, exist_ok=True) + os.makedirs(os.path.join(casedir, 'autocode'), exist_ok=True) os.makedirs(os.path.join(casedir, 'g00files'), exist_ok=True) os.makedirs(os.path.join(casedir, 'lstfiles'), exist_ok=True) os.makedirs(os.path.join(casedir, 'outputs', 'figures'), exist_ok=True) diff --git a/tests/test_outputs.py b/tests/test_outputs.py index cdd56086..c84a7f27 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -6,17 +6,17 @@ def test_output_files(casepath): outputs_folder = os.path.join(casepath, 'outputs') - e_report_params_path = os.path.join(casepath, 'e_report_params.csv') + report_params_path = os.path.join(casepath, 'autocode', 'report_params.csv') lastyear = reeds.io.get_years(casepath)[-1] - # each parameter in e_report_params.csv should be associated with an output csv file - e_report_params = pd.read_csv(e_report_params_path, comment='#') - e_report_params['fpath'] = e_report_params.param.map(lambda x: x.split('(')[0]) + # each parameter in report_params.csv should be associated with an output csv file + report_params = pd.read_csv(report_params_path, comment='#') + report_params['fpath'] = report_params.param.map(lambda x: x.split('(')[0]) # rename outputs as specified by output_rename column - rename = e_report_params.loc[ - ~e_report_params.output_rename.isnull() + rename = report_params.loc[ + ~report_params.output_rename.isnull() ].set_index('fpath').output_rename.to_dict() - e_report_params['fpath'] = e_report_params['fpath'].replace(rename) + '.csv' + report_params['fpath'] = report_params['fpath'].replace(rename) + '.csv' # Include additional files in outputs folder that should be generated for each run expected_plots = [ @@ -29,7 +29,7 @@ def test_output_files(casepath): ] expected_files = ( - e_report_params.fpath.tolist() + report_params.fpath.tolist() + [ # Standard bokeh outputs (postprocessing/bokehpivot) os.path.join('reeds-report', 'report.html'), From ce951812f1ed7efc0f2ef77fcf61d448803a81cc Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:38:09 -0600 Subject: [PATCH 08/34] fix report_params.csv path; copy solver file --- postprocessing/run_pcm.py | 2 +- reeds/core/terminus/report_dump.py | 2 +- reeds/input_processing/copy_files.py | 7 ++++++- reeds/inputs.py | 19 +++++++++++++++++++ reeds/io.py | 2 +- tests/test_outputs.py | 2 +- 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/postprocessing/run_pcm.py b/postprocessing/run_pcm.py index 8e43a703..89098d7b 100644 --- a/postprocessing/run_pcm.py +++ b/postprocessing/run_pcm.py @@ -266,7 +266,7 @@ def main(casepath, t, switch_mods=switch_mods_default, label='', overwrite=False # %% Dump gdx to h5 ## Get new file names if applicable dfparams = pd.read_csv( - os.path.join(casepath, 'autocode', 'report_params.csv'), + os.path.join(casepath, 'reeds', 'core', 'terminus', 'report_params.csv'), comment="#", index_col="param", ) diff --git a/reeds/core/terminus/report_dump.py b/reeds/core/terminus/report_dump.py index 089482f3..bbb63b93 100644 --- a/reeds/core/terminus/report_dump.py +++ b/reeds/core/terminus/report_dump.py @@ -258,7 +258,7 @@ def postprocess_outputs(case, outputs_path=None, verbose=0): ### Get new file names if applicable dfparams = pd.read_csv( - os.path.join(case, 'autocode', 'report_params.csv'), + os.path.join(case, 'reeds', 'core', 'terminus', 'report_params.csv'), comment="#", index_col="param", ) diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py index 678bd101..af72ab0c 100644 --- a/reeds/input_processing/copy_files.py +++ b/reeds/input_processing/copy_files.py @@ -1300,7 +1300,12 @@ def write_miscellaneous_files( Many of these files are not in the non_region_files and region_files set (runfiles.csv - from function read_runfiles). """ - # ---- Miscellaneous files not in non_region_files or region_files ---- + ### Solver file + case = Path(inputs_case).parent + optfile = reeds.inputs.get_optfile(case) + shutil.copy(Path(reeds_path, 'reeds', 'solver', optfile), case) + + ### Parsed switches pd.DataFrame( {'*pvb_type': [f'pvb{i}' for i in sw['GSw_PVB_Types'].split('_')], 'ilr': [np.around(float(c) / 100, 2) for c in sw['GSw_PVB_ILR'].split('_') diff --git a/reeds/inputs.py b/reeds/inputs.py index 416bebbd..b536ff3f 100644 --- a/reeds/inputs.py +++ b/reeds/inputs.py @@ -338,6 +338,25 @@ def parse_cases( return dfcases_out +def get_optfile(case=None): + """ + Get the name of the optfile used by GAMS, formatted as described by + https://gams.com/49/docs/UG_GamsCall.html#GAMSAOoptfile + """ + sw = reeds.io.get_switches(case) + GSw_gopt = int(sw.GSw_gopt) + if GSw_gopt == 1: + suffix = 'opt' + elif len(str(GSw_gopt)) == 1: + suffix = f'op{GSw_gopt}' + elif len(str(GSw_gopt)) == 2: + suffix = f'o{GSw_gopt}' + else: + suffix = str(GSw_gopt) + optfile = f'{sw.solver}.{suffix}'.lower() + return optfile + + def solvestring_sequential( batch_case, caseSwitches, cur_year, next_year, prev_year, restartfile, diff --git a/reeds/io.py b/reeds/io.py index 3e2cc7c2..c2d60547 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -491,7 +491,7 @@ def read_output( except KeyError: ## Empty dataframes aren't written to h5 file, so make one ourselves report_params = pd.read_csv( - os.path.join(case, 'autocode', 'report_params.csv'), + os.path.join(case, 'reeds', 'core', 'terminus', 'report_params.csv'), comment='#', ) _index = report_params.loc[ diff --git a/tests/test_outputs.py b/tests/test_outputs.py index c84a7f27..c6a73abc 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -6,7 +6,7 @@ def test_output_files(casepath): outputs_folder = os.path.join(casepath, 'outputs') - report_params_path = os.path.join(casepath, 'autocode', 'report_params.csv') + report_params_path = os.path.join(casepath, 'reeds', 'core', 'terminus', 'report_params.csv') lastyear = reeds.io.get_years(casepath)[-1] # each parameter in report_params.csv should be associated with an output csv file From 694b005b314f956ac6402842a63476af08d7e8af Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:18:18 -0600 Subject: [PATCH 09/34] rename run.py -> runreeds.py; fix restart_runs.py (add --copy_reeds) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/python-app.yaml | 2 +- README.md | 6 +++--- docs/source/developer_best_practices.md | 2 +- docs/source/reeds_training_homework.md | 4 ++-- docs/source/setup.md | 2 +- helpers/restart_runs.py | 12 ++++++++++-- hourlize/qaqc/summarize_supply_curves.py | 4 ++-- .../air_quality/health_damage_calculations.py | 2 +- preprocessing/casemaker.py | 4 ++-- reeds/core/setup/b_inputs.gms | 4 ++-- reeds/hpc/aws_setup.sh | 3 +-- reeds/input_processing/copy_files.py | 2 +- reeds/input_processing/mcs_sampler.py | 2 +- reeds/input_processing/runfiles.csv | 4 ++-- reeds/inputs.py | 4 ++-- run.py => runreeds.py | 0 17 files changed, 33 insertions(+), 26 deletions(-) rename run.py => runreeds.py (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 57b5973f..187beea4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -76,7 +76,7 @@ Include additional illustrative plots describing input data, methods, testing, a - [ ] No large data file(s) added/modified - [ ] No substantive impact on runtime for full-US reference case - [ ] No substantive impact on folder size for full-US reference case -- [ ] No change to process flow (run.py, d_solve_iterate.py) +- [ ] No change to process flow (runreeds.py, solve.py) - [ ] No change to code organization - [ ] No change to package requirements (environment.yml or Project.toml) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index b5396f5d..4f009fe1 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -230,7 +230,7 @@ jobs: env: SCENARIO: ${{ matrix.scenario }} run: | - python run.py -b "$batch" -c test -s "$SCENARIO" + python runreeds.py -b "$batch" -c test -s "$SCENARIO" echo "RUN_FOLDER=$GITHUB_WORKSPACE/runs/${batch}_${SCENARIO}" >> "$GITHUB_ENV" - name: Print GAMS log diff --git a/README.md b/README.md index 383e55ba..a38809a2 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,10 @@ A step-by-step guide for getting started with ReEDS is available [here](https:// These files are downloaded automatically as needed during a ReEDS run, but to finish all the internet-requiring steps up front, you can download them all by running `python reeds/remote.py`. Additional details on remote files and other topics can be found in the [user guide](https://pages.github.nrel.gov/ReEDS/ReEDS-2.0/user_guide.html#large-input-files). 5. Run ReEDS on a test case from the root of the cloned repository: - 1. For interactive setup: `python run.py` - 2. For one-line operation: `python run.py -b v20250314_main -c test`. + 1. For interactive setup: `python runreeds.py` + 2. For one-line operation: `python runreeds.py -b v20250314_main -c test`. In this example, "v20250314_main" is the prefix for this batch of cases, and "test" is the suffix of the cases file, in this case `cases_test.csv`, located in the root of the repository. - Run `python run.py -h` for information on other optional command-line arguments for ReEDS. + Run `python runreeds.py -h` for information on other optional command-line arguments for ReEDS. diff --git a/docs/source/developer_best_practices.md b/docs/source/developer_best_practices.md index 544692ab..582e1241 100644 --- a/docs/source/developer_best_practices.md +++ b/docs/source/developer_best_practices.md @@ -629,7 +629,7 @@ Additionally, if you want to re-run a given scenario without having to run all o * To avoid the prompts when kicking off a run, you can use the command line arguments: * The following example runs the scenarios in cases_test.csv with the batch name '20240717_test'. The '-r -1' means that all cases will run simultaneously. ``` - python run.py -c test -b 20240717_test -r -1 + python runreeds.py -c test -b 20240717_test -r -1 ``` * All options for command line arguments that can be used: | Flag | | diff --git a/docs/source/reeds_training_homework.md b/docs/source/reeds_training_homework.md index 93a66cbb..070649b7 100644 --- a/docs/source/reeds_training_homework.md +++ b/docs/source/reeds_training_homework.md @@ -25,7 +25,7 @@ orphan: true - Open a new terminal and activate the reeds2 environment 7. Start a new run - - `python run.py` + - `python runreeds.py` - when prompted for case file name, enter `examples` - when prompted for how many simultaneous runs you would like to execute, enter 2 - The 'ERCOT_county' and 'ERCOT_BA' runs should start @@ -74,7 +74,7 @@ Create an informal slide deck with the following results: - Open a new terminal and activate the reeds2 environment 7. Start a new run - - `python run.py` + - `python runreeds.py` - when prompted for case file name, enter `examples` - The 'Western_BA_Decarb' run should start diff --git a/docs/source/setup.md b/docs/source/setup.md index 10b61d8d..bf5b2ce6 100644 --- a/docs/source/setup.md +++ b/docs/source/setup.md @@ -231,7 +231,7 @@ If that doesn't resolve the issue, the following may help: **Quick Start:** 1. Navigate to the ReEDS directory from the command line 2. Activate environment: `conda activate reeds2` -3. Run the model: `python run.py` +3. Run the model: `python runreeds.py` 4. Follow the prompts for batch configuration 5. Check for a successful run: 1. Look for CSV files in `runs/[batchname_scenario]/outputs` (a successful run should have 100+ csv files in the outputs folder) diff --git a/helpers/restart_runs.py b/helpers/restart_runs.py index 37174055..bc093e30 100644 --- a/helpers/restart_runs.py +++ b/helpers/restart_runs.py @@ -1,11 +1,13 @@ #%% Imports import os +import sys import shutil import subprocess import argparse -import pandas as pd from glob import glob -from runbatch import submit_slurm_parallel_jobs +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent)) +from runreeds import submit_slurm_parallel_jobs from runstatus import get_run_status #%% Argument inputs @@ -19,6 +21,8 @@ help='Proceed without double-checking') parser.add_argument('--more_copyfiles', '-m', type=str, default='', help=',-delimited list of additional files to copy from reeds_path') +parser.add_argument('--copy_reeds', '-r', action='store_true', + help='Copy the reeds/ model folder from the repo to the run') parser.add_argument('--include_finished', '-i', action='store_true', help='Also restart finished runs (e.g. to redo postprocessing)') @@ -28,6 +32,7 @@ copy_srun_template = args.copy_srun_template force = args.force more_copyfiles = [i for i in args.more_copyfiles.split(',') if len(i)] +copy_reeds = args.copy_reeds include_finished = args.include_finished # #%% Inputs for debugging @@ -36,6 +41,7 @@ # copy_srun_template = True # force = True # more_copyfiles = ['report.gms'] +# copy_reeds = False # include_finished = False ###### Procedure @@ -93,6 +99,8 @@ #%% Copy additional files if desired for f in more_copyfiles: shutil.copy(os.path.join(reeds_path,f), os.path.join(case,f)) + if copy_reeds: + shutil.copytree(Path(reeds_path, 'reeds'), Path(case, 'reeds'), dirs_exist_ok=True) #%% Make a backup copy of the original bash and sbatch scripts callfile = os.path.join(case,f'call_{casename}.sh') diff --git a/hourlize/qaqc/summarize_supply_curves.py b/hourlize/qaqc/summarize_supply_curves.py index 26a7aa87..4522abcf 100644 --- a/hourlize/qaqc/summarize_supply_curves.py +++ b/hourlize/qaqc/summarize_supply_curves.py @@ -93,7 +93,7 @@ def load_raw_supply_curves(rev_paths): today = datetime.datetime.now().strftime("%Y-%m-%d") site.addsitedir(os.path.join(reeds_path)) - import runbatch as rb + import runreeds ## read list of rev_path files rev_paths = [] @@ -105,7 +105,7 @@ def load_raw_supply_curves(rev_paths): sys.exit(1) else: rev_path = pd.read_csv(rp) - rev_path = rb.get_rev_paths(rev_path, resolution) + rev_path = runreeds.get_rev_paths(rev_path, resolution) # subset to base name for sc_path rev_path['sc_folder'] = rev_path['sc_path'].apply(lambda row: os.path.basename(row)) # subset to relevant columns and techs diff --git a/postprocessing/air_quality/health_damage_calculations.py b/postprocessing/air_quality/health_damage_calculations.py index 9e96e811..01145251 100644 --- a/postprocessing/air_quality/health_damage_calculations.py +++ b/postprocessing/air_quality/health_damage_calculations.py @@ -1,6 +1,6 @@ ''' This script can be run in one of two ways: -1. called automatically from run.py as part of a ReEDS run, +1. called automatically from runreeds.py as part of a ReEDS run, in which case a single case folder is passed to the script 2. call directly to post-process a set of completed runs, with a csv file diff --git a/preprocessing/casemaker.py b/preprocessing/casemaker.py index 4f3a8db7..a793a152 100644 --- a/preprocessing/casemaker.py +++ b/preprocessing/casemaker.py @@ -191,9 +191,9 @@ def main(casematrix_path=None, batchname=None): ### Write it dfcases.to_csv(os.path.join(reeds_path,f'cases_{_batchname}.csv')) - ### Make sure the switch names and values pass the checks in run.py + ### Make sure the switch names and values pass the checks in runreeds.py sep = ';' if os.name == 'posix' else '&&' - cmd = f"cd {reeds_path} {sep} python run.py -b test -c {_batchname} -r 4 -p 1 --dryrun" + cmd = f"cd {reeds_path} {sep} python runreeds.py -b test -c {_batchname} -r 4 -p 1 --dryrun" result = subprocess.run(cmd, shell=True, capture_output=True) err = result.stderr.decode() if len(err): diff --git a/reeds/core/setup/b_inputs.gms b/reeds/core/setup/b_inputs.gms index ebf9f825..f1a48a4f 100644 --- a/reeds/core/setup/b_inputs.gms +++ b/reeds/core/setup/b_inputs.gms @@ -63,7 +63,7 @@ alias(dummy,adummy) ; * Following are scalars used to turn on or off various components of the model. * For binary switches, [0] is off and [1] is on. -* These switches are generated from the cases file in run.py. +* These switches are generated from the cases file in runreeds.py. $include inputs_case%ds%gswitches.txt * Extra switches that are defined based on other switches @@ -91,7 +91,7 @@ scalar retireyear "first year to allow capacity to start retiring" /%GSw_Retire upgradeyear "first year to allow capacity to upgrade" /%GSw_Upgradeyear%/ climateyear "first year to apply climate impacts" /%GSw_ClimateStartYear%/ ; -*** Scalars: copied from inputs/scalars.csv to inputs_case/scalars.txt in run.py +*** Scalars: copied from inputs/scalars.csv to inputs_case/scalars.txt in runreeds.py $include inputs_case%ds%scalars.txt *========================== diff --git a/reeds/hpc/aws_setup.sh b/reeds/hpc/aws_setup.sh index fcc250f4..b6fdd6c3 100644 --- a/reeds/hpc/aws_setup.sh +++ b/reeds/hpc/aws_setup.sh @@ -97,7 +97,7 @@ git clone git@github.nrel.gov:ReEDS/ReEDS-2.0 # Run ReEDS! # (using nohup to keep the process from dying when you end your ssh session) -#nohup python runbatch_aws.py -c weekendcentroid -r 4 -b centwknd > myout.txt & +#nohup python runreeds_aws.py -c weekendcentroid -r 4 -b centwknd > myout.txt & #======================================== @@ -132,4 +132,3 @@ git clone git@github.nrel.gov:ReEDS/ReEDS-2.0 #move to your git directory #!!! could be different for different users #cd ~/r2/r2_aws - diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py index af72ab0c..0a37f335 100644 --- a/reeds/input_processing/copy_files.py +++ b/reeds/input_processing/copy_files.py @@ -1618,7 +1618,7 @@ def main(reeds_path, inputs_case): write_GAMS_sets(runfiles, reeds_path, inputs_case) # Rewrite the switches tables as GAMS-readable definition - # (gswitches.csv is first written at run.py) + # (gswitches.csv is first written at runreeds.py) scalar_csv_to_txt(os.path.join(inputs_case,'gswitches.csv')) source_deflator_map = get_source_deflator_map(reeds_path) diff --git a/reeds/input_processing/mcs_sampler.py b/reeds/input_processing/mcs_sampler.py index 80679f06..419ae45e 100644 --- a/reeds/input_processing/mcs_sampler.py +++ b/reeds/input_processing/mcs_sampler.py @@ -426,7 +426,7 @@ def get_dist_instructions(reeds_path: str, inputs_case: str, run_ReEDS: bool = T mcs_dist_groups = sw['MCS_dist_groups'].split('.') if not run_ReEDS: - # Since you did not run using run.py - check inputs here + # Since you did not run using runreeds.py - check inputs here general_mcs_dist_validation(reeds_path, mcs_dist_path, sw) # Ignore all cases not in mcs_dist_groups diff --git a/reeds/input_processing/runfiles.csv b/reeds/input_processing/runfiles.csv index 890b202b..366b1025 100644 --- a/reeds/input_processing/runfiles.csv +++ b/reeds/input_processing/runfiles.csv @@ -231,7 +231,7 @@ interconnection_queues.csv,inputs/capacity_exogenous/interconnection_queues.csv, itc_energy_comm_bonus.csv,,1,mean,ignore,r,*i,,0,0,,1,,,, itc_frac_monetized.csv,,1,ignore,ignore,,,,,0,,,,,, itc_fractions.csv,,1,ignore,ignore,,"i,country,t",,0,0,,,,,, -ivt.csv,,1,ignore,ignore,,,,,0,,,,,,created in run.py +ivt.csv,,1,ignore,ignore,,,,,0,,,,,,created in runreeds.py ivt_step.csv,,1,ignore,ignore,,,,,0,,,,,, lcclike.csv,inputs/sets/lcclike.csv,1,ignore,ignore,,,,,0,,,,lcclike,, load_2010.csv,,1,sum,ignore,r,wide,,1,0,,1,,,,disaggfunc set to ignore because load will already be in correct spatial resolution @@ -490,4 +490,4 @@ scalars.txt,,1,ignore,ignore,,,,,0,,,,,, Project.toml,Project.toml,1,ignore,ignore,,,,,,,,,,, gamslice.txt,gamslice.txt,0,ignore,ignore,,,,,,,,,,, max_hintage_number.txt,,1,ignore,ignore,,,,,0,,,,,, -run.py,run.py,1,ignore,ignore,,,,,,,,,,, +runreeds.py,runreeds.py,1,ignore,ignore,,,,,,,,,,, diff --git a/reeds/inputs.py b/reeds/inputs.py index b536ff3f..5c1639f5 100644 --- a/reeds/inputs.py +++ b/reeds/inputs.py @@ -338,12 +338,12 @@ def parse_cases( return dfcases_out -def get_optfile(case=None): +def get_optfile(case=None, **kwargs): """ Get the name of the optfile used by GAMS, formatted as described by https://gams.com/49/docs/UG_GamsCall.html#GAMSAOoptfile """ - sw = reeds.io.get_switches(case) + sw = reeds.io.get_switches(case, **kwargs) GSw_gopt = int(sw.GSw_gopt) if GSw_gopt == 1: suffix = 'opt' diff --git a/run.py b/runreeds.py similarity index 100% rename from run.py rename to runreeds.py From 03aa0815ae5eee0926d928b3d4eb6d1f676f6b7b Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:54:39 -0600 Subject: [PATCH 10/34] revert unintended changes; reeds/inputs -> reeds/input_processing in comments --- .github/workflows/python-app.yaml | 8 ++--- README.md | 6 ++-- cases.csv | 2 +- docs/source/developer_best_practices.md | 8 ++--- inputs/national_generation/README.md | 2 +- reeds/core/setup/b_inputs.gms | 44 ++++++++++++------------- reeds/core/solve/2_temporal_params.gms | 6 ++-- tests/objective_function_params.yaml | 2 +- 8 files changed, 38 insertions(+), 40 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 80bf85d0..09d89f9e 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -29,7 +29,7 @@ env: jobs: julia-setup: name: "Julia environment" - runs-on: reeds-runners + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: @@ -50,7 +50,7 @@ jobs: python-setup: name: "Python environment" - runs-on: reeds-runners + runs-on: ubuntu-latest permissions: contents: read actions: write @@ -69,7 +69,7 @@ jobs: name: "Get Zenodo files" needs: - python-setup - runs-on: reeds-runners + runs-on: ubuntu-latest permissions: contents: read actions: write @@ -116,7 +116,7 @@ jobs: run: python .github/scripts/download_test_zenodo_files.py run-ReEDS: - runs-on: reeds-runners + runs-on: ubuntu-latest needs: - julia-setup - python-setup diff --git a/README.md b/README.md index a38809a2..b6df4002 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,9 @@ This GitHub repository contains the source code for NLR's ReEDS model. The ReEDS model source code is available at no cost from the National Laboratory of the Rockies. -The ReEDS model can be downloaded or cloned from [https://github.com/NatLabRockies/ReEDS-2.0](https://github.com/NatLabRockies/ReEDS-2.0). +The ReEDS model can be downloaded or cloned from [https://github.com/ReEDS-Model/ReEDS](https://github.com/ReEDS-Model/ReEDS). -**For more information about the model, see the [ReEDS-2.0 Documentation](https://pages.github.nrel.gov/ReEDS/ReEDS-2.0).** - - +**For more information about the model, see the [ReEDS Documentation](https://reeds-model.github.io/ReEDS).** ReEDS training videos are available on the [NLR Learning YouTube channel](https://youtube.com/playlist?list=PLmIn8Hncs7bG558qNlmz2QbKhsv7QCKiC&si=NgGBaL_MxNcYiIEX). diff --git a/cases.csv b/cases.csv index 9b69adf4..39719e0b 100644 --- a/cases.csv +++ b/cases.csv @@ -355,7 +355,7 @@ diagnose,Write A and B matrix of the model [0 = no diagnose ; 1 = diagnose ],0; diagnose_year,Year in which to start report diagnose,N/A,2022, dump_alldata,switch to automatically dump data from final solve year into .gdx file,0; 1,0, file_replacements,"List of files to replace from run folder, e.g: inputs_case/national_gen_frac.csv << //nrelnas01/ReEDS/some proj/national_gen_frac.csv || c_supplymodel.gms << //nrelnas01/ReEDS/some proj/c_supplymodel.gms",N/A,none, -input_processing_only,Only run input-processing scripts; stop before creating and solving model,0; 1,0, +input_processing_only,Only run input_processing scripts; stop before creating and solving model,0; 1,0, keep_resource_adequacy_files,Indicate whether to keep (1) or delete (0) resource adequacy csv and h5 files after RA calculations finish,0; 1,0, keep_g00_files,Keep (1) or delete (0) .g00 files for completed solve years,0; 1,0, keep_run_terminal,"0=close run terminal, 1=keep run terminal open",0; 1,0, diff --git a/docs/source/developer_best_practices.md b/docs/source/developer_best_practices.md index 582e1241..7d60eebb 100644 --- a/docs/source/developer_best_practices.md +++ b/docs/source/developer_best_practices.md @@ -162,7 +162,7 @@ _Some exceptions to this might exist due to number scaling (e.g., emission rates * If preprocessing is needed to create an input file that is placed in the ReEDS repository, the preprocessing scripts or workbooks should be included in the [ReEDS_Input_Processing repository](https://github.com/ReEDS-Model/ReEDS_Input_Processing). Data from external sources should be downloaded programmatically when possible. -* Any scripts that preprocess data after a ReEDS run is started should be placed in the `reeds/inputs` folder. +* Any scripts that preprocess data after a ReEDS run is started should be placed in the `reeds/input_processing` folder. * Input processing scripts should start with a block of descriptive comments describing the purpose and methodology, and internal functions should use docstrings and liberal comments on functionality and assumptions. * Any costs read into b_inputs should already be in 2004$. Cost adjustments in preprocessing scripts should rely on the deflator.csv file rather than have hard-coded conversions. @@ -174,9 +174,9 @@ _Some exceptions to this might exist due to number scaling (e.g., emission rates * Data column headers should use the ReEDS set names when practical. * Example: data that include regions should use "r" for the column name rather than "ba", "reeds_ba", or "region". -* Preprocessing scripts in `reeds/inputs` should not change the working directory or use relative filepaths; absolute filepaths should be used wherever possible. +* Preprocessing scripts in input_processing should not change the working directory or use relative filepaths; absolute filepaths should be used wherever possible. -* When feasible, inputs used in the objective function (c_supplyobjective.gms) should be included in tests/objective_function_params.yaml. Inputs included in this .yaml file will be checked for missing values using `reeds/inputs/check_inputs.py`. +* When feasible, inputs used in the objective function (c_supplyobjective.gms) should be included in tests/objective_function_params.yaml. Inputs included in this .yaml file will be checked for missing values using input_processing/check_inputs.py. #### Input Data @@ -555,7 +555,7 @@ The following are best practices that should be considered when reviewing pull r - Check out the branch locally (optional) - You should check the branch out locally and run the test scenario (cases_test.csv) to ensure there are no issues - - If there are a large amount of changes to one of the scripts or code files (ex. input processing scripts or GAMS files), it could be helpful to run just that script and walk through it line by line with a debugging tool (ex. [pdb](https://docs.python.org/3/library/pdb.html)) to more deeply understand how the revised script functions and any issues we might face with the way that script is now written. + - If there are a large amount of changes to one of the scripts or code files (ex. input_processing scripts or GAMS files), it could be helpful to run just that script and walk through it line by line with a debugging tool (ex. [pdb](https://docs.python.org/3/library/pdb.html)) to more deeply understand how the revised script functions and any issues we might face with the way that script is now written. **A few notes on reviewing pull requests:** - When reviewing PRs, be sure to provide constructive feedback and highlight positive aspects as well. Reviewing PRs is an opportunity to learn from one another and support each other's development as developers! diff --git a/inputs/national_generation/README.md b/inputs/national_generation/README.md index 5a058f10..5e7539e5 100644 --- a/inputs/national_generation/README.md +++ b/inputs/national_generation/README.md @@ -57,7 +57,7 @@ For existing coal plants, this is the code implementation: This is the emissions rate (metric tons CO2 per MWh) equivalent to average emissions from a new coal-CCS plant, assuming 90% capture rate. The emissions rate from a new coal-CCS plant in ReEDS is 0.051956 metric tons CO2 per MWh (see `emit_rate` parameter) which assumes 95% capture. For 90% capture, the emissions rate is double that or 0.1039 metric tons CO2 per MWh, which we use as the standard. -2. `reeds/inputs/WriteHintage.py` +2. `reeds/input_processing/WriteHintage.py` - Coal plants are binned at the unit level if `GSw_Clean_Air_Act=1` so that each coal unit can independently choose to retire or upgrade. - Coal plants maintain their exogenous retirement assumption, except after 2032, when the Clean Air Act regulations begin and coal can retire endogenously. For example, if the NEMS data states that a plant will retire in 2029, we maintain that assumption and that plant will retire in 2029. diff --git a/reeds/core/setup/b_inputs.gms b/reeds/core/setup/b_inputs.gms index f1a48a4f..1778ef20 100644 --- a/reeds/core/setup/b_inputs.gms +++ b/reeds/core/setup/b_inputs.gms @@ -1512,7 +1512,7 @@ $onlisting /, h2_ptc(i,v,r,allt) "--2004$/kg h2 produced -- incentive on hydrogen production by electrolyzers which purchase Energy Attribute Credits" - h2_ptc_in(i,v,allt) "--2004$/kg h2 produced -- incentive on hydrogen production by electrolyzers which purchase Energy Attribute Credits, this parameter is used to build h2_ptc and is produced in reeds/inputs/calc_financial_inputs.py" + h2_ptc_in(i,v,allt) "--2004$/kg h2 produced -- incentive on hydrogen production by electrolyzers which purchase Energy Attribute Credits, this parameter is used to build h2_ptc" / $offlisting $ondelim @@ -1611,8 +1611,8 @@ $offdelim $onlisting / ; -*created by /reeds/inputs/writecapdat.py -table capnonrsc(i,r,*) "--MW-- raw power capacity data for non-RSC tech created by .\reeds/inputs\writecapdat.py" +*created by reeds/input_processing/writecapdat.py +table capnonrsc(i,r,*) "--MW-- raw power capacity data for non-RSC tech" $offlisting $ondelim $include inputs_case%ds%capnonrsc.csv @@ -1620,9 +1620,9 @@ $offdelim $onlisting ; -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py $onempty -table capnonrsc_energy(i,r,*) "--MWh-- raw energy capacity data for battery tech created by .\reeds/inputs\writecapdat.py" +table capnonrsc_energy(i,r,*) "--MWh-- raw energy capacity data for battery tech" $offlisting $ondelim $include inputs_case%ds%capnonrsc_energy.csv @@ -1631,9 +1631,9 @@ $onlisting ; $offempty -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py $onempty -table caprsc(pcat,r,*) "--MW-- raw RSC capacity data, created by .\writecapdat.py" +table caprsc(pcat,r,*) "--MW-- raw RSC capacity data" $offlisting $ondelim $include inputs_case%ds%caprsc.csv @@ -1642,10 +1642,10 @@ $onlisting ; $offempty -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py * declared over allt to allow for external data files that extend beyond end_year $onempty -table prescribednonrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for non-RSC tech created by writecapdat.py" +table prescribednonrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for non-RSC tech" $offlisting $ondelim $include inputs_case%ds%prescribed_nonRSC.csv @@ -1655,7 +1655,7 @@ $onlisting $offempty $onempty -table prescribednonrsc_energy(allt,pcat,r,*) "--MWh-- raw prescribed energy capacity data for non-RSC tech created by writecapdat.py" +table prescribednonrsc_energy(allt,pcat,r,*) "--MWh-- raw prescribed energy capacity data for non-RSC tech" $offlisting $ondelim $include inputs_case%ds%prescribed_nonRSC_energy.csv @@ -1664,9 +1664,9 @@ $onlisting ; $offempty -*Created using reeds/inputs\writecapdat.py +*Created using reeds/input_processing/writecapdat.py $onempty -table prescribedrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for RSC tech created by .\reeds/inputs\writecapdat.py" +table prescribedrsc(allt,pcat,r,*) "--MW-- raw prescribed capacity data for RSC tech" $offlisting $ondelim $include inputs_case%ds%prescribed_rsc.csv @@ -1700,11 +1700,11 @@ $offempty prescribedrsc(allt,"wind-ofs",r,"value") = prescribed_wind_ofs(r,allt,"capacity") ; -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py *following does not include wind *Retirements for techs binned by heatrates are handled in hintage_data.csv $onempty -table prescribedretirements(allt,r,i,*) "--MW-- raw prescribed power capacity retirement data for non-RSC, non-heatrate binned tech created by /reeds/inputs/writecapdat.py" +table prescribedretirements(allt,r,i,*) "--MW-- raw prescribed power capacity retirement data for non-RSC, non-heatrate binned tech" $offlisting $ondelim $include inputs_case%ds%retirements.csv @@ -1713,10 +1713,10 @@ $onlisting ; $offempty -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py *Retirements for techs binned by heatrates are handled in hintage_data.csv $onempty -table prescribedretirements_energy(allt,r,i,*) "--MWh-- raw prescribed energy capacity retirement data for battery tech created by /reeds/inputs/writecapdat.py" +table prescribedretirements_energy(allt,r,i,*) "--MWh-- raw prescribed energy capacity retirement data for battery tech" $offlisting $ondelim $include inputs_case%ds%retirements_energy.csv @@ -1750,8 +1750,8 @@ $include inputs_case%ds%hintage_char.csv $onlisting / ; -*created by /reeds/inputs/writehintage.py -table hintage_data(i,v,r,allt,hintage_char) "table of existing unit characteristics written by writehintage.py" +*created by reeds/input_processing/writehintage.py +table hintage_data(i,v,r,allt,hintage_char) "table of existing unit characteristics" $offlisting $ondelim $include inputs_case%ds%hintage_data.csv @@ -1791,7 +1791,7 @@ if(Sw_Upgrades = 1, ) ; ) ; -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py parameter binned_capacity(i,v,r,allt) "existing capacity (that is not rsc, but including distpv) binned by heat rates" ; binned_capacity(i,v,r,allt) = hintage_data(i,v,r,allt,"cap") ; @@ -4605,7 +4605,7 @@ heat_rate(i,v,r,t)$[heat_rate_adj(i,'post2010')$newv(v)] = heat_rate_adj(i,'post parameter fuel_price(i,r,t) "$/MMBtu - fuel prices by technology" ; -* Written by reeds/inputs\fuelcostprep.py +* Written by reeds/input_processing/fuelcostprep.py * declared over allt to allow for external data files that extend beyond end_year table fprice(allt,r,f) "--2004$/MMBtu-- fuel prices by fuel type" $offlisting @@ -4673,7 +4673,7 @@ $endif.climatewater * -- Capacity Factor Adjustments over Time -- *============================================= -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py parameter cap_hyd_ccseason_adj(i,ccseason,r) "--fraction-- ccseason max capacity adjustment for dispatchable hydro" / $offlisting @@ -5638,7 +5638,7 @@ nat_beta(t)$(not tfirst(t)) = nat_beta_nonenergy ; $endif.gassector -* Written by reeds/inputs\fuelcostprep.py +* Written by reeds/input_processing/fuelcostprep.py * declared over allt to allow for external data files that extend beyond end_year table cd_alpha(allt,cendiv) "--$/MMBtu-- alpha value for natural gas supply curves" $offlisting diff --git a/reeds/core/solve/2_temporal_params.gms b/reeds/core/solve/2_temporal_params.gms index ecb54ebe..68ca1901 100644 --- a/reeds/core/solve/2_temporal_params.gms +++ b/reeds/core/solve/2_temporal_params.gms @@ -223,7 +223,7 @@ $offdelim $onlisting / ; -* Written by reeds/inputs/hourly_writetimeseries.py +* Written by reeds/input_processing/hourly_writetimeseries.py parameter frac_h_quarter_weights(allh,quarter) "--unitless-- fraction of timeslice associated with each quarter" / $offlisting @@ -456,7 +456,7 @@ cf_in(i,r,h)$[i_water_cooling(i)$Sw_WaterMain] = cf_rsc(i,v,r,allh,t) = 0 ; cf_rsc(i,v,r,h,t)$[cf_in(i,r,h)$cf_tech(i)$valcap(i,v,r,t)] = cf_in(i,r,h) ; -* Written by reeds/inputs/hourly_writetimeseries.py +* Written by reeds/input_processing/hourly_writetimeseries.py $onempty parameter cf_hyd(i,allszn,r,allt) "--fraction-- hydro capacity factors by season and year" / @@ -492,7 +492,7 @@ cf_hyd(i,szn,r,t)$[hydro_d(i)$(yeart(t)>=Sw_ClimateStartYear)] = $endif.climatehydro -*created by /reeds/inputs/writecapdat.py +*created by reeds/input_processing/writecapdat.py parameter cap_hyd_szn_adj(i,allszn,r) "--fraction-- seasonal max capacity adjustment for dispatchable hydro" / $offlisting diff --git a/tests/objective_function_params.yaml b/tests/objective_function_params.yaml index ce31971b..8836cef6 100644 --- a/tests/objective_function_params.yaml +++ b/tests/objective_function_params.yaml @@ -1,6 +1,6 @@ ### Notes # This file holds parameters used in the objective function. These parameters -# are checked using reeds/inputs/check_inputs.py, which will identify missing +# are checked using reeds/input_processing/check_inputs.py, which will identify missing # values in the parameters if there are any. # Instructions: From 1fc51cb8e31c1e624e7316e9f5905f29eaa3f1f6 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:00:03 -0600 Subject: [PATCH 11/34] fix a few more input_processing --- helpers/runstatus.py | 2 +- .../bokehpivot/in/reeds2/process_style.csv | 32 +++++++++---------- postprocessing/cleanup_files.py | 2 +- runreeds.py | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/helpers/runstatus.py b/helpers/runstatus.py index b749aae3..831ccd08 100644 --- a/helpers/runstatus.py +++ b/helpers/runstatus.py @@ -130,7 +130,7 @@ def get_run_status(reeds_path, batch_name): try: lastfile = lstfiles[-1] except IndexError: - print(f"{case:<{longest}}: failed in input processing") + print(f"{case:<{longest}}: failed in input_processing") print_log_if_verbose(fullcase, verbose) continue try: diff --git a/postprocessing/bokehpivot/in/reeds2/process_style.csv b/postprocessing/bokehpivot/in/reeds2/process_style.csv index 7ef7c82a..26d05cf1 100644 --- a/postprocessing/bokehpivot/in/reeds2/process_style.csv +++ b/postprocessing/bokehpivot/in/reeds2/process_style.csv @@ -1,20 +1,20 @@ order,color -inputs/copy_files.py,#393B79 -inputs/aggregate_regions.py,#5254A3 -inputs/calc_financial_inputs.py,#6B6ECF -inputs/fuelcostprep.py,#9C9EDE -inputs/writecapdat.py,#637939 -inputs/writesupplycurves.py,#8CA252 -inputs/plantcostprep.py,#B5CF6B -inputs/climateprep.py,#CEDB9C -inputs/hourly_load.py,#8C6D31 -inputs/recf.py,#BD9E39 -inputs/forecast.py,#E7BA52 -inputs/writehintage.py,#E7CB94 -inputs/transmission.py,#7B4173 -inputs/outage_rates.py,#A55194 -inputs/hourly_repperiods.py,#CE6DBD -inputs/check_inputs.py,#DE9ED6 +input_processing/copy_files.py,#393B79 +input_processing/aggregate_regions.py,#5254A3 +input_processing/calc_financial_inputs.py,#6B6ECF +input_processing/fuelcostprep.py,#9C9EDE +input_processing/writecapdat.py,#637939 +input_processing/writesupplycurves.py,#8CA252 +input_processing/plantcostprep.py,#B5CF6B +input_processing/climateprep.py,#CEDB9C +input_processing/hourly_load.py,#8C6D31 +input_processing/recf.py,#BD9E39 +input_processing/forecast.py,#E7BA52 +input_processing/writehintage.py,#E7CB94 +input_processing/transmission.py,#7B4173 +input_processing/outage_rates.py,#A55194 +input_processing/hourly_repperiods.py,#CE6DBD +input_processing/check_inputs.py,#DE9ED6 a_createmodel.gms,#31A354 d_solveoneyear.gms,#843C39 solver/barrier,#AD494A diff --git a/postprocessing/cleanup_files.py b/postprocessing/cleanup_files.py index 40480d9e..0ba36c28 100644 --- a/postprocessing/cleanup_files.py +++ b/postprocessing/cleanup_files.py @@ -29,7 +29,7 @@ ## All the other outputs/*.csv files are duplicates of data in outputs.h5. os.path.join('outputs', r'^((?!(neue|health|h2_price_month)).)*csv$'), ], - ## Large input files. Would need to rerun input processing to recreate. + ## Large input files. Would need to rerun input_processing to recreate. 2: [ os.path.join('inputs_case', 'inputs.gdx'), os.path.join('inputs_case', 'unitdata.csv'), diff --git a/runreeds.py b/runreeds.py index 08b12584..4a2e2170 100644 --- a/runreeds.py +++ b/runreeds.py @@ -1259,7 +1259,7 @@ def write_batch_script( OPATH.writelines("conda activate reeds2 \n") OPATH.writelines('export R_LIBS_USER="$HOME/rlib" \n\n\n') - #%% Write the input processing script calls + #%% Write the input_processing script calls big_comment('Input processing', OPATH) for s in [ 'copy_files', From 07038ebaaf65f6e4eb5ed616e050da4ce38573e0 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:10:36 -0600 Subject: [PATCH 12/34] add dev branch to CI --- .github/workflows/build-docs.yaml | 4 +++- .github/workflows/cache.yaml | 1 + .github/workflows/python-app.yaml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yaml b/.github/workflows/build-docs.yaml index fac37fb5..42daa974 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -12,9 +12,11 @@ on: push: branches: - main + - dev pull_request: branches: - - main + - main + - dev permissions: contents: write diff --git a/.github/workflows/cache.yaml b/.github/workflows/cache.yaml index aec36234..681e01ad 100644 --- a/.github/workflows/cache.yaml +++ b/.github/workflows/cache.yaml @@ -7,6 +7,7 @@ on: push: branches: - main + - dev workflow_dispatch: permissions: diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index cba98b27..89145616 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -5,6 +5,7 @@ on: pull_request: branches: - main + - dev paths-ignore: - 'docs/**' - '*.md' From 2e94718665af53fe213a9ec574ac8721c5a6e7a9 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:18:52 -0600 Subject: [PATCH 13/34] fix hourly_writetimeseries.py import; turn off sources.csv/sources_documentation.md build until we decide what to do about it --- .github/workflows/build-docs.yaml | 4 ---- reeds/resource_adequacy/stress_periods.py | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yaml b/.github/workflows/build-docs.yaml index 42daa974..4128fe29 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -51,10 +51,6 @@ jobs: - name: Set variables for internal github repo run: | echo "BASE_URL=https://github.com/ReEDS-Model/ReEDS" >> $GITHUB_ENV - cd ${GITHUB_WORKSPACE}/docs/source/documentation_tools/ - sh generate_sources_md_file.sh - cd ${GITHUB_WORKSPACE} - python docs/source/documentation_tools/generate_markdown.py --githubURL "https://github.com/ReEDS-Model/ReEDS/blob/main" --reedsPath "${GITHUB_WORKSPACE}" - name: Build Sphinx documentation env: diff --git a/reeds/resource_adequacy/stress_periods.py b/reeds/resource_adequacy/stress_periods.py index 424fd6d4..021424b7 100644 --- a/reeds/resource_adequacy/stress_periods.py +++ b/reeds/resource_adequacy/stress_periods.py @@ -8,6 +8,7 @@ import matplotlib.pyplot as plt import reeds +from reeds.input_processing import hourly_writetimeseries # #%% Debugging # sw['reeds_path'] = os.path.expanduser('~/github/ReEDS-2.0/') @@ -679,7 +680,7 @@ def main(sw, t, iteration=0, logging=True): #%% Write timeseries data for stress periods for the next iteration of ReEDS newstresspath = f'stress{t}i{iteration+1}' - reeds.input_processing.hourly_writetimeseries.main( + hourly_writetimeseries.main( sw=sw, reeds_path=sw['reeds_path'], inputs_case=os.path.join(sw['casedir'], 'inputs_case'), periodtype=newstresspath, From f62577c84825062c57c414a4234ca015ba5e2c12 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:29:18 -0600 Subject: [PATCH 14/34] Fix straggler script names Co-authored-by: Wesley Cole <49044852+wesleyjcole@users.noreply.github.com> --- runreeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runreeds.py b/runreeds.py index 68e66b19..e9ce9e43 100644 --- a/runreeds.py +++ b/runreeds.py @@ -741,7 +741,7 @@ def setup_window( OPATH.writelines(writeerrorcheck(os.path.join("g00files", restartfile + ".g*"))) ## solve via the window solve file OPATH.writelines( - f"gams {Path('reeds','core','d_solvewindow.gms')} o=" + f"gams {Path('reeds','core','3_solvewindow.gms')} o=" + os.path.join("lstfiles", batch_case + "_" + str(i) + ".lst") +" r=" + os.path.join("g00files", restartfile) + " gdxcompress=1 xs=g00files\\"+savefile + toLogGamsString + " --niter=" + str(i) From c5dfd105c2724430ba0850783866e4dfebb54ef1 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:32:28 -0600 Subject: [PATCH 15/34] remove references to dev branch --- .github/workflows/build-docs.yaml | 4 +--- .github/workflows/cache.yaml | 1 - .github/workflows/python-app.yaml | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yaml b/.github/workflows/build-docs.yaml index 4128fe29..d34bc5bb 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -12,11 +12,9 @@ on: push: branches: - main - - dev pull_request: branches: - - main - - dev + - main permissions: contents: write diff --git a/.github/workflows/cache.yaml b/.github/workflows/cache.yaml index bf825917..b44aa9f3 100644 --- a/.github/workflows/cache.yaml +++ b/.github/workflows/cache.yaml @@ -7,7 +7,6 @@ on: push: branches: - main - - dev workflow_dispatch: permissions: diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 1f738294..72ceadd3 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -5,7 +5,6 @@ on: pull_request: branches: - main - - dev paths-ignore: - 'docs/**' - '*.md' From f3594b093d9713b8dd039747d1e52ff01cdf9a19 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:34:42 -0600 Subject: [PATCH 16/34] Fix straggler script path Co-authored-by: Brian Sergi --- reeds/core/solve/3_solve_window.gms | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/core/solve/3_solve_window.gms b/reeds/core/solve/3_solve_window.gms index 99e7e699..325ada4f 100644 --- a/reeds/core/solve/3_solve_window.gms +++ b/reeds/core/solve/3_solve_window.gms @@ -149,6 +149,6 @@ $ifthene.lastiter %niter%=%maxiter% $eval nextwindow %window% + 1 tfix(t)$(tmodel(t)$(yeart(t) Date: Thu, 23 Apr 2026 08:48:54 -0600 Subject: [PATCH 17/34] more stragglers; simplify code copy --- reeds/core/terminus/report.gms | 2 +- runreeds.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/reeds/core/terminus/report.gms b/reeds/core/terminus/report.gms index d8e1f0c6..bb729367 100644 --- a/reeds/core/terminus/report.gms +++ b/reeds/core/terminus/report.gms @@ -2077,7 +2077,7 @@ h2_usage(r,h,t)$tmodel_new(t) = *======================================== $ifthene.powerfrac %GSw_calc_powfrac% == 1 -$include reeds%ds%core%ds%terminus%ds%e_powfrac_calc.gms +$include reeds%ds%core%ds%terminus%ds%powfrac_calc.gms $endif.powerfrac *======================================== diff --git a/runreeds.py b/runreeds.py index e9ce9e43..0d365e06 100644 --- a/runreeds.py +++ b/runreeds.py @@ -1235,13 +1235,12 @@ def write_batch_script( modeledyears = os.path.join('inputs_case','modeledyears.csv') toLogGamsString = ' logOption=4 logFile=gamslog.txt appendLog=1 ' - ## Copy code folders - for dirname in ['reeds']: - shutil.copytree( - os.path.join(reeds_path, dirname), - os.path.join(casedir, dirname), - ignore=shutil.ignore_patterns('test'), - ) + ## Copy code folder + shutil.copytree( + os.path.join(reeds_path, 'reeds'), + os.path.join(casedir, 'reeds'), + ignore=shutil.ignore_patterns('test'), + ) #make the reeds_data folder os.makedirs(os.path.join(casedir,'handoff','reeds_data'), exist_ok=True) From 7772a5c4287f14ff6af03c86e7ff6d8427eec786 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:58:30 -0600 Subject: [PATCH 18/34] clean up straggler filenames --- helpers/runstatus.py | 2 +- inputs/scalars.csv | 2 +- postprocessing/bokehpivot/in/reeds2/process_style.csv | 2 +- postprocessing/run_pcm.py | 3 ++- reeds/core/solve/solve.py | 11 +++-------- reeds/log.py | 6 +++--- runreeds.py | 2 +- 7 files changed, 12 insertions(+), 16 deletions(-) diff --git a/helpers/runstatus.py b/helpers/runstatus.py index 831ccd08..2002cc6e 100644 --- a/helpers/runstatus.py +++ b/helpers/runstatus.py @@ -176,7 +176,7 @@ def get_run_status(reeds_path, batch_name): elif "dual objective limit exceeded" in slurm: errortext = "(hit dual obj. limit)" # check if infeasible - elif "d_solveoneyear.gms failed with return code 3" in slurm: + elif "3_solve_oneyear.gms failed with return code 3" in slurm: errortext = "(infeasible)" else: errortext = "" diff --git a/inputs/scalars.csv b/inputs/scalars.csv index 3b3d0334..ed04d8d6 100644 --- a/inputs/scalars.csv +++ b/inputs/scalars.csv @@ -3,7 +3,7 @@ caa_coal_retire_year,2032,"--year-- year in which coal capacity is forced to eit caa_first_year,2024,"--year-- first year in which the emissions requirements under Clean Air Act, Section 111 are active" caa_rate_emis_standard,0.1039,"--metric tons CO2 per MWh-- rate equivalent to average emissions from a new coal-CCS plant, assuming 90% capture rate. Emissions rate from new coal-CCS plant is 0.051956 metric tons CO2 per MWh (see emit_rate) which assumes 95% capture. For 90% capture, the emissions rate is double that or 0.1039 metric tons CO2 per MWh." caa_gas_max_cf,0.40,"--fraction-- maximum cf that new gas plants (CCs or CTs) can operate at without being regulated under Clean Air Act, Section 111" -co2_capture_incentive_length,12,'--years-- length for the co2 captured incentive to be extended in d_solveoneyear if an upgrade occurs +co2_capture_incentive_length,12,'--years-- length for the co2 captured incentive to be extended in 4_post_solve_adjustments.gms if an upgrade occurs co2_capture_incentive_last_year_,2038,'--year-- last year the co2 captured incentive is available co2_emissions_2022,1539.251,"--million metric tons CO2-- 2022 emissions (used for tax credit phaseout)" coal_fom_adj,97.05,"--2004$/MW-year-- FOM cost annual escalation factor for coal. The product of this number and the coal plant's age is added to the plant's FOM cost. See page 17 of Assumptions to the Annual Energy Outlook 2025: Electricity Market Module." diff --git a/postprocessing/bokehpivot/in/reeds2/process_style.csv b/postprocessing/bokehpivot/in/reeds2/process_style.csv index 26d05cf1..61269c26 100644 --- a/postprocessing/bokehpivot/in/reeds2/process_style.csv +++ b/postprocessing/bokehpivot/in/reeds2/process_style.csv @@ -16,7 +16,7 @@ input_processing/outage_rates.py,#A55194 input_processing/hourly_repperiods.py,#CE6DBD input_processing/check_inputs.py,#DE9ED6 a_createmodel.gms,#31A354 -d_solveoneyear.gms,#843C39 +3_solve_oneyear.gms,#843C39 solver/barrier,#AD494A solver/crossover,#D6616B solver/remainder,#E7969C diff --git a/postprocessing/run_pcm.py b/postprocessing/run_pcm.py index 89098d7b..85a51dc8 100644 --- a/postprocessing/run_pcm.py +++ b/postprocessing/run_pcm.py @@ -12,6 +12,7 @@ import reeds from reeds.input_processing import hourly_repperiods from reeds.input_processing import hourly_writetimeseries +from reeds.core.terminus.report_dump import write_dfdict # %% Inferred inputs @@ -282,7 +283,7 @@ def main(casepath, t, switch_mods=switch_mods_default, label='', overwrite=False outputs_path = os.path.join(casepath, 'outputs', f'pcm_{label}_{_t}') os.makedirs(outputs_path, exist_ok=True) - reeds.core.terminus.report_dump.write_dfdict( + write_dfdict( dfdict=dict_out, outputs_path=outputs_path, rename=rename, diff --git a/reeds/core/solve/solve.py b/reeds/core/solve/solve.py index 4042d288..b06cdc01 100644 --- a/reeds/core/solve/solve.py +++ b/reeds/core/solve/solve.py @@ -11,14 +11,15 @@ #%% Main function -def run_reeds(casepath, t, onlygams=False, iteration=0): +def run_reeds(casepath, t, iteration=0, onlygams=False, onlyra=False): """ """ # #%% Arguments for testing # casepath = os.path.expanduser('~/github/ReEDS-2.0/runs/v20230512_prasM0_ERCOT') # t = 2020 - # onlygams = 0 # iteration = 0 + # onlygams = 0 + # onlyra = 0 # os.chdir(casepath) #%% Get the run settings @@ -153,10 +154,6 @@ def main(casepath, t, overwrite=False): help='year to run') parser.add_argument('--iteration', '-i', type=int, default=0, help='iteration counter for this run') - parser.add_argument('--onlygams', '-g', action='store_true', - help='Only run GAMS (skip resource adequacy)') - parser.add_argument('--onlyra', '-a', action='store_true', - help='Only run resource adequacy (RA) (skip GAMS)') parser.add_argument('--overwrite', '-o', action='store_true', help='Overwrite iterations that have already finished') @@ -164,8 +161,6 @@ def main(casepath, t, overwrite=False): casepath = args.casepath t = args.t iteration = args.iteration - onlygams = args.onlygams - onlyra = args.onlyra overwrite = args.overwrite #%% Switch to run folder diff --git a/reeds/log.py b/reeds/log.py index 2dee9a4a..ed2fcce5 100755 --- a/reeds/log.py +++ b/reeds/log.py @@ -103,7 +103,7 @@ def get_solve_times(path=''): process = line[len('--- Job ') : line.index(' Stop ')] x = f'--- Job {process} Stop ' y = ' elapsed ' - label = process if process != 'd_solveoneyear.gms' else stress_year + label = process if process != '3_solve_oneyear.gms' else stress_year lengths['total'][label] = pd.Timedelta(line[line.index(y) + len(y) :]) times['stop'][label] = pd.Timestamp(line[len(x) : line.index(y)]) times['start'][label] = times['stop'][label] - lengths['total'][label] @@ -164,7 +164,7 @@ def write_last_solve_time(path=''): scriptname = lasttime.name year = 0 else: - scriptname = 'd_solveoneyear.gms' + scriptname = '3_solve_oneyear.gms' year = int(lasttime.name.split('i')[0]) towrite = { 'gams': scriptname, @@ -173,7 +173,7 @@ def write_last_solve_time(path=''): 'remainder': 'solver/remainder', } with open(os.path.join(path, 'meta.csv'), 'a') as METAFILE: - if (scriptname == 'd_solveoneyear.gms') and all([i in lasttime for i in towrite]): + if (scriptname == '3_solve_oneyear.gms') and all([i in lasttime for i in towrite]): for i, process in enumerate(towrite): METAFILE.writelines( '{},{},{},{},{}\n'.format( diff --git a/runreeds.py b/runreeds.py index 0d365e06..550ffd62 100644 --- a/runreeds.py +++ b/runreeds.py @@ -550,7 +550,7 @@ def setup_sequential_year( cur_year, next_year, prev_year, restartfile, toLogGamsString, hpc, )) - OPATH.writelines(writescripterrorcheck(f"d_solveoneyear.gms_{cur_year}")) + OPATH.writelines(writescripterrorcheck(f"3_solve_oneyear.gms_{cur_year}")) OPATH.writelines(f'python {logger} --year={cur_year}\n') if int(caseSwitches['GSw_ValStr']): From 978a86d0297c88bd75f6126953dbdfbd53ad42f4 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:55:47 -0600 Subject: [PATCH 19/34] fix more filepaths --- cases.csv | 2 +- docs/source/developer_best_practices.md | 4 ++-- docs/source/postprocessing_tools.md | 8 ++++---- docs/source/user_guide.md | 2 +- helpers/interim_report_batch.py | 8 +++++++- helpers/restart_runs.py | 14 ++++++++------ inputs/national_generation/README.md | 4 ++-- postprocessing/run_pcm.py | 8 ++++---- postprocessing/run_reeds2pras.py | 2 +- reeds/core/setup/b_inputs.gms | 8 ++++---- reeds/core/solve/1_tc_phaseout.py | 6 +++--- reeds/core/solve/3_solve_oneyear.gms | 2 +- reeds/input_processing/WriteHintage.py | 2 +- reeds/input_processing/aggregate_regions.py | 2 +- reeds/input_processing/calc_financial_inputs.py | 2 +- reeds/input_processing/check_inputs.py | 2 +- reeds/input_processing/climateprep.py | 2 +- reeds/input_processing/copy_files.py | 2 +- reeds/input_processing/forecast.py | 2 +- reeds/input_processing/fuelcostprep.py | 2 +- reeds/input_processing/h2_storage.py | 2 +- reeds/input_processing/hourly_load.py | 2 +- reeds/input_processing/hourly_repperiods.py | 2 +- reeds/input_processing/hydcf.py | 2 +- reeds/input_processing/mcs_sampler.py | 2 +- reeds/input_processing/outage_rates.py | 2 +- reeds/input_processing/plantcostprep.py | 2 +- reeds/input_processing/recf.py | 2 +- reeds/input_processing/transmission.py | 2 +- reeds/input_processing/writecapdat.py | 2 +- reeds/input_processing/writedrshift.py | 2 +- reeds/input_processing/writesupplycurves.py | 2 +- runreeds.py | 14 ++++++++------ tests/objective_function_params.yaml | 4 ++-- 34 files changed, 68 insertions(+), 58 deletions(-) diff --git a/cases.csv b/cases.csv index 795e59e5..e813a404 100644 --- a/cases.csv +++ b/cases.csv @@ -355,7 +355,7 @@ cleanup_level,How aggressively to clean up the case folder (see postprocessing/c diagnose,Write A and B matrix of the model [0 = no diagnose ; 1 = diagnose ],0; 1,0, diagnose_year,Year in which to start report diagnose,N/A,2022, dump_alldata,switch to automatically dump data from final solve year into .gdx file,0; 1,0, -file_replacements,"List of files to replace from run folder, e.g: inputs_case/national_gen_frac.csv << //nrelnas01/ReEDS/some proj/national_gen_frac.csv || c_supplymodel.gms << //nrelnas01/ReEDS/some proj/c_supplymodel.gms",N/A,none, +file_replacements,"List of files to replace from run folder, e.g: inputs_case/national_gen_frac.csv << //nrelnas01/ReEDS/some_project/national_gen_frac.csv || c_model.gms << //nrelnas01/ReEDS/some_project/c_model.gms",N/A,none, input_processing_only,Only run input_processing scripts; stop before creating and solving model,0; 1,0, keep_resource_adequacy_files,Indicate whether to keep (1) or delete (0) resource adequacy csv and h5 files after RA calculations finish,0; 1,0, keep_g00_files,Keep (1) or delete (0) .g00 files for completed solve years,0; 1,0, diff --git a/docs/source/developer_best_practices.md b/docs/source/developer_best_practices.md index 8ecc29bf..635c4c50 100644 --- a/docs/source/developer_best_practices.md +++ b/docs/source/developer_best_practices.md @@ -17,7 +17,7 @@ Since we have not yet adopted strict formatting guidelines, do not make code *fo -------------| --------------- | ------------ | Folders | lowercase | `inputs`, `docs` | Files | typically lowercase with underscores (acronyms often left as uppercase); output files noun first | `battery_ATB_2024_moderate.csv`, `gen_ann` not `ann_gen` | -GAMS Files | letter-underscore prefix by category, alpha-ordered; numbering can help communicate ordering when multiple files share a category | `d1_financials.gms`, `d2_varfix.gms` | +GAMS Files | letter-underscore prefix by category, alpha-ordered; numbering can help communicate ordering when multiple files share a category | `2_financials.gms`, `5_varfix.gms` | Parameters | lowercase with underscores, noun first; costs prefixed with "cost" | `curt_marg` not `marg_curt`, `cost_cap` | Variables | all caps, noun first | `INV`, `INV_TRANS` | Equations (model constraints) | prefixed with `eq_`, lowercase with underscores | `eq_reserve_margin` | @@ -176,7 +176,7 @@ _Some exceptions to this might exist due to number scaling (e.g., emission rates * Preprocessing scripts in input_processing should not change the working directory or use relative filepaths; absolute filepaths should be used wherever possible. -* When feasible, inputs used in the objective function (c_supplyobjective.gms) should be included in tests/objective_function_params.yaml. Inputs included in this .yaml file will be checked for missing values using input_processing/check_inputs.py. +* When feasible, inputs used in the objective function (`d_objective.gms`) should be included in tests/objective_function_params.yaml. Inputs included in this .yaml file will be checked for missing values using input_processing/check_inputs.py. #### Input Data diff --git a/docs/source/postprocessing_tools.md b/docs/source/postprocessing_tools.md index 0c9367b7..7ba7ee13 100644 --- a/docs/source/postprocessing_tools.md +++ b/docs/source/postprocessing_tools.md @@ -88,13 +88,13 @@ python postprocessing/compare_cases.py /Users/username/github/ReEDS-2.0/runs/v20 Example for three cases: ```bash -python postprocessing/compare_cases.py /Users/username/github/ReEDS-2.0/runs/v20250310_main_USA /Users/username/github/ReEDS-2.0/runs/v20250310_newthing1_USA /Users/username/github/ReEDS-2.0/runs/v20250310_newthing2_USA +python postprocessing/compare_cases.py /Users/username/github/ReEDS/runs/v20250310_main_USA /Users/username/github/ReEDS/runs/v20250310_newthing1_USA /Users/username/github/ReEDS/runs/v20250310_newthing2_USA ``` Example for a .csv file of cases: ```bash -python postprocessing/compare_cases.py /Users/username/github/ReEDS-2.0/postprocessing/example.csv +python postprocessing/compare_cases.py /Users/username/github/ReEDS/postprocessing/example.csv ``` ### Run PRAS: `postprocessing/run_reeds2pras.py` @@ -103,10 +103,10 @@ The PRAS model is typically run multiple times during each ReEDS case (as long a This script reruns PRAS on a finished ReEDS case (provided by the single required command-line argument) and allows the settings to be changed. For example, to use a different number of samples than are specified by the default `pras_samples` switch, use the `-s/--samples` command-line argument. -### Run a dispatch model: `run_pcm.py` +### Run a dispatch model: `postprocessing/run_pcm.py` This script reruns a completed ReEDS case as a dispatch simulation at higher time resolution. -The operational constraints in `c_supplymodel.gms` are used directly, but the investment and capacity variables are fixed to their previously optimized values; only the operational variables are re-optimized. +The operational constraints in `c_model.gms` are used directly, but the investment and capacity variables are fixed to their previously optimized values; only the operational variables are re-optimized. 365 representative 1-day periods at 1-hour resolution are used by default, but these settings can be changed using the `-s/--switch_mods` switch. This approach is distinct from the [R2X](https://github.com/NatLabRockies/R2X) tool, which formats the results of a ReEDS case as inputs to a separate production cost modeling tool such as [Sienna](https://github.com/NREL-Sienna) or [PLEXOS](https://www.energyexemplar.com/plexos). diff --git a/docs/source/user_guide.md b/docs/source/user_guide.md index 36045174..314dbd89 100644 --- a/docs/source/user_guide.md +++ b/docs/source/user_guide.md @@ -568,7 +568,7 @@ Options are `capacity`, `transmission`, `rasharing`, and `co2`. - `GSw_MGA_SubObjective` (default `fossil`): Technology subset to minimize or maximize the capacity of (only used for `GSw_MGA_Objective = capacity`). Options are the column names in the `inputs/tech-subset-table.csv` file. -Users familiar with GAMS can add alternative objective functions to the `c_mga.gms` file and associated options to the `GSw_MGA_Objective` switch in `cases.csv`. +Users familiar with GAMS can add alternative objective functions to the `d_mga.gms` file and associated options to the `GSw_MGA_Objective` switch in `cases.csv`. diff --git a/helpers/interim_report_batch.py b/helpers/interim_report_batch.py index 4845e67b..0f295a9a 100644 --- a/helpers/interim_report_batch.py +++ b/helpers/interim_report_batch.py @@ -18,6 +18,9 @@ from glob import glob import argparse import sys +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent)) +import reeds parser = argparse.ArgumentParser() parser.add_argument('--batch_name', '-b', type=str, default='', help='Prefix for batch of runs') @@ -44,7 +47,10 @@ interim_report = os.path.join(case, "interim_report.py") if os.name=='posix': if hpc: - shutil.copy("srun_template.sh", os.path.join(case, "interim_report.sh")) + shutil.copy( + Path(reeds.io.reeds_path,'reeds','hpc','srun_template.sh'), + os.path.join(case, "interim_report.sh") + ) with open(os.path.join(case, "interim_report.sh"), 'a') as SPATH: #add the name for easy tracking of the case SPATH.writelines("\n#SBATCH --job-name=" + case_name + "_interim_report" + "\n\n") diff --git a/helpers/restart_runs.py b/helpers/restart_runs.py index bc093e30..5784bb58 100644 --- a/helpers/restart_runs.py +++ b/helpers/restart_runs.py @@ -20,7 +20,7 @@ parser.add_argument('--force', '-f', action='store_true', help='Proceed without double-checking') parser.add_argument('--more_copyfiles', '-m', type=str, default='', - help=',-delimited list of additional files to copy from reeds_path') + help=',-delimited list of additional relative filepaths to copy from reeds_path') parser.add_argument('--copy_reeds', '-r', action='store_true', help='Copy the reeds/ model folder from the repo to the run') parser.add_argument('--include_finished', '-i', action='store_true', @@ -80,7 +80,7 @@ #%% Copy the header from the srun_template.sh file if desired if copy_srun_template: - srun_template = os.path.join(reeds_path,'srun_template.sh') + srun_template = os.path.join(reeds_path,'reeds','hpc','srun_template.sh') writelines_srun = list() with open(srun_template, 'r') as f: for line in f: @@ -98,7 +98,7 @@ #%% Copy additional files if desired for f in more_copyfiles: - shutil.copy(os.path.join(reeds_path,f), os.path.join(case,f)) + shutil.copy(f, Path(case, f)) if copy_reeds: shutil.copytree(Path(reeds_path, 'reeds'), Path(case, 'reeds'), dirs_exist_ok=True) @@ -114,14 +114,16 @@ if any([os.path.basename(i).startswith('report') for i in lstfiles]): restart_tag = '# Output processing' elif len(lstfiles) < 2: - # If there is only 1 lst file, then it is an environment.csv so the run failed during inputs processing + # If there is only 1 lst file, then it is an environment.csv, + # so the run failed during inputs processing restart_tag = '# Input processing' elif len(lstfiles) == 2: - # If there are only 2 lst files, then one of them will be environment.csv and the other will be 1_inputs.lst so the run failed during the model compilation + # If there are only 2 lst files, then one of them will be environment.csv and + # the other will be 1_inputs.lst, so the run failed during the model compilation restart_tag = '# Compile model' else: # Drop environment and inputs .lst files - lstfiles = [l for l in lstfiles if ("environment.csv" not in l) and ('1_Inputs.lst' not in l)] + lstfiles = [i for i in lstfiles if ("environment.csv" not in i) and ('1_Inputs.lst' not in i)] lastfile = lstfiles[-1] restart_year = int(os.path.splitext(lastfile)[0].split('_')[-1].split('i')[0]) restart_tag = f'# Year: {restart_year}' diff --git a/inputs/national_generation/README.md b/inputs/national_generation/README.md index 5e7539e5..abae1d86 100644 --- a/inputs/national_generation/README.md +++ b/inputs/national_generation/README.md @@ -43,7 +43,7 @@ For new gas plants, this is the code implementation: 1. `inputs/scalars.csv` - `caa_gas_max_cf` = 0.40. This is the maximum capacity factor that new gas plants (CCs or CTs) can operate at without being regulated under Clean Air Act, Section 111, expressed as a fraction. -2. `c_supplymodel.gms` - `eq_caa_max_cf` enforces the maximum capacity factor for new gas plants. +2. `c_model.gms` - `eq_caa_max_cf` enforces the maximum capacity factor for new gas plants. For existing coal plants, this is the code implementation: @@ -68,7 +68,7 @@ For existing coal plants, this is the code implementation: - if `caa_coal_retire_year` is not in the set of years being modeled for this run, then set it to the first year that is modeled after `caa_coal_retire_year`. For example, if running 5 year solves, then instead of enforcing coal retirement in 2032, it will be enforced in 2035. -4. `c_supplymodel.gms` +4. `c_model.gms` - `eq_caa_rate_standard(st,t)` - this constraint enforces the rate-based emissions standard by setting the maximum coal emissions rate per state under Clean Air Act Section 111. ## Assumptions diff --git a/postprocessing/run_pcm.py b/postprocessing/run_pcm.py index 85a51dc8..30c7ed56 100644 --- a/postprocessing/run_pcm.py +++ b/postprocessing/run_pcm.py @@ -63,7 +63,7 @@ def solvestring_pcm( savefile = f"pcm_{label}_{batch_case}_{t}i{iteration}" _stress_year = f"{t}i{iteration}" if stress_year in ['keep', 'default'] else stress_year out = ( - "gams d_solvepcm.gms" + f"gams {Path('reeds','core','solve_pcm','solve_pcm.gms')}" + (" license=gamslice.txt" if hpc else '') + f" o={os.path.join('lstfiles', f'{savefile}.lst')}" + f" r={os.path.join('g00files', restartfile)}" @@ -123,11 +123,11 @@ def submit_job(casepath, command_string, jobname='', joblabel='', bigmem=0): """ Create a slurm job submission script for `command_string` at `casepath`, then submit it. - Uses the slurm settings from {reeds_path}/srun_template.sh. + Uses the slurm settings from {reeds_path}/reeds/hpc/srun_template.sh. """ ### Get the SLURM boilerplate commands_header, commands_sbatch, commands_other = [], [], [] - with open(os.path.join(reeds_path, 'srun_template.sh'), 'r') as f: + with open(os.path.join(reeds_path,'reeds','hpc','srun_template.sh'), 'r') as f: for line in f: if bigmem and ('--mem=' in line): line = '#SBATCH --mem=500000' @@ -247,7 +247,7 @@ def main(casepath, t, switch_mods=switch_mods_default, label='', overwrite=False ### Run GAMS LP result = subprocess.run(cmd_gams, shell=True) if result.returncode: - raise Exception(f'd_solvepcm.gms failed with return code {result.returncode}') + raise Exception(f'solve_pcm.gms failed with return code {result.returncode}') # %% Dump results to gdx cmd_report = pcm_report_string( diff --git a/postprocessing/run_reeds2pras.py b/postprocessing/run_reeds2pras.py index 95b7372d..0cdf040f 100644 --- a/postprocessing/run_reeds2pras.py +++ b/postprocessing/run_reeds2pras.py @@ -37,7 +37,7 @@ def submit_job( jobname = f'PRAS-{os.path.basename(case)}-{samples}' ## Get the SLURM boilerplate commands_header, commands_sbatch, commands_other = [], [], [] - with open(os.path.join(reeds_path,'srun_template.sh'), 'r') as f: + with open(os.path.join(reeds_path,'reeds','hpc','srun_template.sh'), 'r') as f: for line in f: if line.strip().startswith('#!'): commands_header.append(line.strip()) diff --git a/reeds/core/setup/b_inputs.gms b/reeds/core/setup/b_inputs.gms index 1778ef20..8964fad9 100644 --- a/reeds/core/setup/b_inputs.gms +++ b/reeds/core/setup/b_inputs.gms @@ -77,7 +77,7 @@ parameter Sw_Timetype(timetype) "Switch that specifies the type of time method u Sw_Timetype("%timetype%") = 1 ; -* Sw_PCM is always 0 except when running d_solvepcm.gms, where it's set to 1 +* Sw_PCM is always 0 except when running solve_pcm.gms, where it's set to 1 scalar Sw_PCM "Internal switch used when running PCM mode" / 0 / ; * Sw_MGA is always 0 except when running the optimization a second time for MGA, where it's set to 1 scalar Sw_MGA "Internal switch used when running MGA mode" / 0 / ; @@ -1481,7 +1481,7 @@ $onlisting /, * pvf_capital and pvf_onm here are for intertemporal mode. These parameters -* are overwritten for sequential mode in d_solveprep.gms. +* are overwritten for sequential mode in e_solveprep.gms. pvf_capital(t) "--unitless-- present value factor for overnight capital costs" / $offlisting @@ -6127,7 +6127,7 @@ cost_curt(t)$[yeart(t)>=model_builds_start_yr] = Sw_CurtMarket ; *====================== parameter emit_cap(eall,t) "--metric tons per year-- annual CO2 emissions cap", - yearweight(t) "--unitless-- weights applied to each solve year for the banking and borrowing cap - updated in d_solveprep.gms", + yearweight(t) "--unitless-- weights applied to each solve year for the banking and borrowing cap - updated in e_solveprep.gms", emit_tax(e,r,t) "--$ per metric ton-- tax applied to emissions" ; emit_cap(e,t) = 0 ; @@ -6601,7 +6601,7 @@ z_rep_op(t) = 0 ; *== h- and szn-dependent sets and parameters (declared here, populated in 2_temporal_params) === *================================================================================================ -* allh and allszn need to be populated here so they can be used in c_supplymodel and c_supplyobjective +* allh and allszn need to be populated here so they can be used in c_model and d_objective Set allh "all potentially modeled hours" / $offlisting diff --git a/reeds/core/solve/1_tc_phaseout.py b/reeds/core/solve/1_tc_phaseout.py index 812c220e..4fb89959 100644 --- a/reeds/core/solve/1_tc_phaseout.py +++ b/reeds/core/solve/1_tc_phaseout.py @@ -211,7 +211,7 @@ def calc_tc_phaseout_mult(year, case, use_historical=use_historical): #%% PROCEDURE if __name__ == '__main__': - parser = argparse.ArgumentParser(description="""Running tc_phaseout.py""") + parser = argparse.ArgumentParser(description="Running 1_tc_phaseout.py") parser.add_argument("year", help="ReEDS solve year", type=int) parser.add_argument("case", help="filepath for ReEDS case") args = parser.parse_args() @@ -224,6 +224,6 @@ def calc_tc_phaseout_mult(year, case, use_historical=use_historical): logpath=os.path.join(case,'gamslog.txt'), ) - print(f'starting tc_phaseout.py for {year}') + print(f'starting 1_tc_phaseout.py for {year}') calc_tc_phaseout_mult(year, case, use_historical=use_historical) - print(f'finished tc_phaseout.py for {year}') + print(f'finished 1_tc_phaseout.py for {year}') diff --git a/reeds/core/solve/3_solve_oneyear.gms b/reeds/core/solve/3_solve_oneyear.gms index 797c3ab0..dde80524 100644 --- a/reeds/core/solve/3_solve_oneyear.gms +++ b/reeds/core/solve/3_solve_oneyear.gms @@ -81,7 +81,7 @@ if(Sw_Upgrades = 1, m_capacity_exog(i,v,r,t)$[valcap(i,v,r,t)$sameas(t,"%cur_year%") $(sum{(ii,tt)$[(tt.val <= t.val)$(t.val - tt.val <= Sw_UpgradeLifespan) $valcap(ii,v,r,tt)$upgrade_from(ii,i)], UPGRADES.l(ii,v,r,tt) } ) ] = -* [maximum of] initial capacity recorded in d_solveprep +* [maximum of] initial capacity recorded in e_solveprep max( m_capacity_exog0(i,v,r,t), * -or- capacity of upgrades that have occurred from this i v r t combination sum{(ii,tt)$[(tt.val <= t.val)$(t.val - tt.val <= Sw_UpgradeLifespan) diff --git a/reeds/input_processing/WriteHintage.py b/reeds/input_processing/WriteHintage.py index 2414ac45..ac50b3c0 100644 --- a/reeds/input_processing/WriteHintage.py +++ b/reeds/input_processing/WriteHintage.py @@ -768,7 +768,7 @@ def main(reeds_path, inputs_case): #%% Run it main(reeds_path=reeds_path, inputs_case=inputs_case) - reeds.log.toc(tic=tic, year=0, process='inputs/WriteHintage.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/WriteHintage.py', path=os.path.join(inputs_case,'..')) print('Finished WriteHintage.py') diff --git a/reeds/input_processing/aggregate_regions.py b/reeds/input_processing/aggregate_regions.py index f750cd2c..74ac5621 100644 --- a/reeds/input_processing/aggregate_regions.py +++ b/reeds/input_processing/aggregate_regions.py @@ -1177,7 +1177,7 @@ def agg_disagg(filepath, r2aggreg_glob, r_ba_glob, runfiles_row): raise Exception(err) #%% Finish -reeds.log.toc(tic=tic, year=0, process='inputs/aggregate_regions.py', +reeds.log.toc(tic=tic, year=0, process='input_processing/aggregate_regions.py', path=os.path.join(inputs_case,'..')) print('Finished aggregate_regions.py') diff --git a/reeds/input_processing/calc_financial_inputs.py b/reeds/input_processing/calc_financial_inputs.py index 60dc7e0e..56e8aec8 100644 --- a/reeds/input_processing/calc_financial_inputs.py +++ b/reeds/input_processing/calc_financial_inputs.py @@ -521,7 +521,7 @@ def calc_financial_inputs(inputs_case): calc_financial_inputs(inputs_case) - reeds.log.toc(tic=tic, year=0, process='inputs/calc_financial_inputs.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/calc_financial_inputs.py', path=os.path.join(inputs_case,'..')) print('Finished calc_financial_inputs.py') diff --git a/reeds/input_processing/check_inputs.py b/reeds/input_processing/check_inputs.py index 69e0b5e4..4f32e34d 100644 --- a/reeds/input_processing/check_inputs.py +++ b/reeds/input_processing/check_inputs.py @@ -153,4 +153,4 @@ def check_param(param, kwargs, inputs, sw): dfcheck = check_param(param, kwargs, inputs, sw) #%% Done - reeds.log.toc(tic=tic, year=0, process='inputs/check_inputs.py', path=casepath) + reeds.log.toc(tic=tic, year=0, process='input_processing/check_inputs.py', path=casepath) diff --git a/reeds/input_processing/climateprep.py b/reeds/input_processing/climateprep.py index 86b67751..3e138a64 100644 --- a/reeds/input_processing/climateprep.py +++ b/reeds/input_processing/climateprep.py @@ -216,7 +216,7 @@ def readwrite( if not any([GSw_ClimateWater,GSw_ClimateHydro]): print("All climate switches are off.") -reeds.log.toc(tic=tic, year=0, process='inputs/climateprep.py', +reeds.log.toc(tic=tic, year=0, process='input_processing/climateprep.py', path=os.path.join(inputs_case,'..')) print('Finished climateprep.py') diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py index 0a37f335..d11c92a5 100644 --- a/reeds/input_processing/copy_files.py +++ b/reeds/input_processing/copy_files.py @@ -1686,6 +1686,6 @@ def main(reeds_path, inputs_case): print('Starting copy_files.py') main(reeds_path, inputs_case) - reeds.log.toc(tic=tic, year=0, process='inputs/copy_files.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/copy_files.py', path=os.path.join(inputs_case,'..')) print('Finished copy_files.py') diff --git a/reeds/input_processing/forecast.py b/reeds/input_processing/forecast.py index 8ceb654d..13843d74 100644 --- a/reeds/input_processing/forecast.py +++ b/reeds/input_processing/forecast.py @@ -509,7 +509,7 @@ def forecast( f' -> Projected from {lastdatayear} to {endyear}', flush=True) - reeds.log.toc(tic=tic, year=0, process='inputs/forecast.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/forecast.py', path=os.path.join(inputs_case,'..')) print('Finished forecast.py') diff --git a/reeds/input_processing/fuelcostprep.py b/reeds/input_processing/fuelcostprep.py index 8a7a0025..89c3a813 100644 --- a/reeds/input_processing/fuelcostprep.py +++ b/reeds/input_processing/fuelcostprep.py @@ -170,7 +170,7 @@ ngtotdemand.to_csv(os.path.join(inputs_case,'ng_demand_tot.csv')) alpha.to_csv(os.path.join(inputs_case,'alpha.csv')) -reeds.log.toc(tic=tic, year=0, process='inputs/fuelcostprep.py', +reeds.log.toc(tic=tic, year=0, process='input_processing/fuelcostprep.py', path=os.path.join(inputs_case,'..')) print('Finished fuelcostprep.py') diff --git a/reeds/input_processing/h2_storage.py b/reeds/input_processing/h2_storage.py index 83d5c419..6067c0bd 100644 --- a/reeds/input_processing/h2_storage.py +++ b/reeds/input_processing/h2_storage.py @@ -136,7 +136,7 @@ def main(reeds_path, inputs_case): #%% Run it main(reeds_path=reeds_path, inputs_case=inputs_case) - reeds.log.toc(tic=tic, year=0, process='inputs/h2_storage.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/h2_storage.py', path=os.path.join(inputs_case,'..')) print('Finished h2_storage.py') diff --git a/reeds/input_processing/hourly_load.py b/reeds/input_processing/hourly_load.py index 77d86e87..80cfde07 100644 --- a/reeds/input_processing/hourly_load.py +++ b/reeds/input_processing/hourly_load.py @@ -777,7 +777,7 @@ def main(reeds_path, inputs_case): #%% Run it main(reeds_path=reeds_path, inputs_case=inputs_case) - reeds.log.toc(tic=tic, year=0, process='inputs/hourly_load.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/hourly_load.py', path=os.path.join(inputs_case,'..')) print('Finished hourly_load.py') diff --git a/reeds/input_processing/hourly_repperiods.py b/reeds/input_processing/hourly_repperiods.py index 1fb3bac3..3262bdd0 100644 --- a/reeds/input_processing/hourly_repperiods.py +++ b/reeds/input_processing/hourly_repperiods.py @@ -972,6 +972,6 @@ def main( ) #%% All done - reeds.log.toc(tic=tic, year=0, process='inputs/hourly_repperiods.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/hourly_repperiods.py', path=os.path.join(inputs_case,'..')) print('Finished hourly_repperiods.py') diff --git a/reeds/input_processing/hydcf.py b/reeds/input_processing/hydcf.py index c9419c8c..29be6439 100644 --- a/reeds/input_processing/hydcf.py +++ b/reeds/input_processing/hydcf.py @@ -464,7 +464,7 @@ def main(reeds_path, inputs_case): #%% Run it main(reeds_path=reeds_path, inputs_case=inputs_case) - reeds.log.toc(tic=tic, year=0, process='inputs/hydcf.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/hydcf.py', path=os.path.join(inputs_case,'..')) print('Finished hydcf.py') diff --git a/reeds/input_processing/mcs_sampler.py b/reeds/input_processing/mcs_sampler.py index 419ae45e..ebea7d5d 100644 --- a/reeds/input_processing/mcs_sampler.py +++ b/reeds/input_processing/mcs_sampler.py @@ -1644,6 +1644,6 @@ def main( reeds.log.toc( tic=tic, year=0, - process='inputs/mcs_sampler.py', + process='input_processing/mcs_sampler.py', path=os.path.join(os.path.dirname(inputs_case)) ) diff --git a/reeds/input_processing/outage_rates.py b/reeds/input_processing/outage_rates.py index ea5aefd7..99fea031 100644 --- a/reeds/input_processing/outage_rates.py +++ b/reeds/input_processing/outage_rates.py @@ -501,7 +501,7 @@ def main(reeds_path, inputs_case, debug=0, interactive=False): #%% All done reeds.log.toc( - tic=tic, year=0, process='inputs/outage_rates.py', + tic=tic, year=0, process='input_processing/outage_rates.py', path=os.path.join(inputs_case,'..'), ) print('Finished outage_rates.py') diff --git a/reeds/input_processing/plantcostprep.py b/reeds/input_processing/plantcostprep.py index fe009411..31aa15da 100644 --- a/reeds/input_processing/plantcostprep.py +++ b/reeds/input_processing/plantcostprep.py @@ -499,7 +499,7 @@ def get_pvb_cost( outdac_elec.to_csv(os.path.join(inputs_case,'consumechardac.csv'), index=False) dac_gas.to_csv(os.path.join(inputs_case,'dac_gas.csv'), index=False) -reeds.log.toc(tic=tic, year=0, process='inputs/plantcostprep.py', +reeds.log.toc(tic=tic, year=0, process='input_processing/plantcostprep.py', path=os.path.join(inputs_case,'..')) print('Finished plantcostprep.py') diff --git a/reeds/input_processing/recf.py b/reeds/input_processing/recf.py index 8ed74fc2..b36e43bb 100644 --- a/reeds/input_processing/recf.py +++ b/reeds/input_processing/recf.py @@ -556,7 +556,7 @@ def main(reeds_path, inputs_case): #%% Run it main(reeds_path=reeds_path, inputs_case=inputs_case) - reeds.log.toc(tic=tic, year=0, process='inputs/recf.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/recf.py', path=os.path.join(inputs_case,'..')) print('Finished recf.py') diff --git a/reeds/input_processing/transmission.py b/reeds/input_processing/transmission.py index 0c9e674c..8b733df6 100644 --- a/reeds/input_processing/transmission.py +++ b/reeds/input_processing/transmission.py @@ -636,6 +636,6 @@ def getloss(row, trtype='AC'): ) #%% Finish the timer -reeds.log.toc(tic=tic, year=0, process='inputs/transmission.py', +reeds.log.toc(tic=tic, year=0, process='input_processing/transmission.py', path=os.path.join(inputs_case,'..')) print('Finished transmission.py', flush=True) diff --git a/reeds/input_processing/writecapdat.py b/reeds/input_processing/writecapdat.py index 64965f83..9af916a5 100644 --- a/reeds/input_processing/writecapdat.py +++ b/reeds/input_processing/writecapdat.py @@ -891,7 +891,7 @@ def main(reeds_path, inputs_case, agglevel, regions): index=keep_index.get(key, False), ) - reeds.log.toc(tic=tic, year=0, process='inputs/writecapdat.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/writecapdat.py', path=os.path.join(inputs_case,'..')) print('Finished writecapdat.py') diff --git a/reeds/input_processing/writedrshift.py b/reeds/input_processing/writedrshift.py index 92d7ab32..2d6d99dd 100644 --- a/reeds/input_processing/writedrshift.py +++ b/reeds/input_processing/writedrshift.py @@ -82,6 +82,6 @@ df = pd.DataFrame(columns=["i", "hour", "year"] + val_r) df.to_csv(os.path.join(inputs_case, file + ".csv"), index=False) - reeds.log.toc(tic=tic, year=0, process='inputs/writedrshift.py', + reeds.log.toc(tic=tic, year=0, process='input_processing/writedrshift.py', path=os.path.join(inputs_case,'..')) print('Finished writedrshift.py') diff --git a/reeds/input_processing/writesupplycurves.py b/reeds/input_processing/writesupplycurves.py index a0df3bbe..342b5e94 100644 --- a/reeds/input_processing/writesupplycurves.py +++ b/reeds/input_processing/writesupplycurves.py @@ -1133,7 +1133,7 @@ def main( main(reeds_path=reeds_path, inputs_case=inputs_case) reeds.log.toc( - tic=tic, year=0, process='inputs/writesupplycurves.py', + tic=tic, year=0, process='input_processing/writesupplycurves.py', path=os.path.join(inputs_case,'..')) print('Finished writesupplycurves.py') diff --git a/runreeds.py b/runreeds.py index 90e79447..80a3bd59 100644 --- a/runreeds.py +++ b/runreeds.py @@ -639,7 +639,7 @@ def setup_intertemporal( ): ### beginning year is passed to rabatch begyear = min(solveyears) - ### first save file from d_solveprep is just the case name + ### first save file from e_solveprep is just the case name savefile = batch_case ### if this is the first iteration if startiter == 0: @@ -668,7 +668,7 @@ def setup_intertemporal( OPATH.writelines(writeerrorcheck(os.path.join("g00files", restartfile + ".g*"))) OPATH.writelines( - f"gams {Path('reeds','core','d_solveallyears.gms')} o=" + f"gams {Path('reeds','core','3_solve_allyears.gms')} o=" +os.path.join("lstfiles",batch_case + "_" + str(i) + ".lst") +" r="+os.path.join("g00files", restartfile) + " gdxcompress=1 xs="+os.path.join("g00files", savefile) + toLogGamsString @@ -1418,7 +1418,7 @@ def write_batch_script( ### Make script to unload all data to .gdx file command = ( - 'gams dump_alldata.gms' + f"gams {Path('reeds','core','terminus','dump_alldata.gms')}" + ' o='+os.path.join('lstfiles','dump_alldata_{}_{}.lst'.format(BatchName,case)) ) command_write = ( @@ -1509,7 +1509,9 @@ def write_batch_script( ### Run dispatch mode if desired if int(caseSwitches['pcm']): - OPATH.writelines(f'\npython run_pcm.py {casedir} -b\n\n') + OPATH.writelines( + f"\npython {Path('reeds','postprocessing','run_pcm.py')} {casedir} -b\n\n" + ) def submit_slurm_parallel_jobs( @@ -1534,7 +1536,7 @@ def submit_slurm_parallel_jobs( stop_case_index = min(start_case_index + cases_per_node, num_cases) casenames_print = casenames[start_case_index:stop_case_index] run_script_fpath = os.path.join(batch_folder, f"run_{'-'.join(casenames_print)}.sh") - shutil.copy("srun_template.sh", run_script_fpath) + shutil.copy(Path(reeds_path,'reeds','hpc','srun_template.sh'), run_script_fpath) job_name = f"{BatchName}_({','.join(casenames_print)})" writelines = [] @@ -1604,7 +1606,7 @@ def write_case_submission_script( """ # Create a copy of the SLURM template slurm_script_path = os.path.join(casedir, batch_case + ".sh") - shutil.copy("srun_template.sh", slurm_script_path) + shutil.copy(Path(reeds.io.reeds_path,'reeds','hpc','srun_template.sh'), slurm_script_path) # If using debug node, comment out time and replace with short time if debugnode: diff --git a/tests/objective_function_params.yaml b/tests/objective_function_params.yaml index 8836cef6..8ffddb18 100644 --- a/tests/objective_function_params.yaml +++ b/tests/objective_function_params.yaml @@ -29,12 +29,12 @@ # Note that in many cases the specific configuration here does not match the GAMS # code. For example, the check for cost_cap excludes hydro and psh, even though -# those are not explicitly excluded in c_supplyobjective.gms. When values are +# those are not explicitly excluded in d_objective.gms. When values are # known to be zero they must be excluded otherwise they will be flagged as missing # (GAMS does not hold zero values, so values explicitly set to zero will appear # the same as missing values). -# The ordering of parameters here should follow the ordering in c_supplyobjective.gms. +# The ordering of parameters here should follow the ordering in d_objective.gms. # Most general-purpose parameters used in the objective function should be added to # this file. Parameters that are primarily user-defined or that define their own scope # without the use of separate index sets are not tested here. Examples of such skipped From 3cc2a232a404804ae2f70eb99f255345581928d5 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:08:38 -0600 Subject: [PATCH 20/34] restart_runs.py: change copy_cplex -> copy_solver_settings --- helpers/restart_runs.py | 31 ++++++++++++++----------------- reeds/io.py | 4 ++-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/helpers/restart_runs.py b/helpers/restart_runs.py index 5784bb58..71381707 100644 --- a/helpers/restart_runs.py +++ b/helpers/restart_runs.py @@ -7,14 +7,17 @@ from glob import glob from pathlib import Path sys.path.append(str(Path(__file__).parent.parent)) +import reeds from runreeds import submit_slurm_parallel_jobs from runstatus import get_run_status #%% Argument inputs parser = argparse.ArgumentParser(description='Restart failed runs on the HPC') parser.add_argument('batch_name', type=str, help='batch name (case prefix) to search for') -parser.add_argument('--copy_cplex', '-c', type=int, default=0, - help='Which cplex.opt file to copy (or 0 for none)') +parser.add_argument( + '--copy_solver_settings', '-c', action='store_true', + help='Copy the solver settings file used by this run from the repo to the run folder', +) parser.add_argument('--copy_srun_template', '-s', action='store_true', help='Copy current srun_template.sh to sbatch file') parser.add_argument('--force', '-f', action='store_true', @@ -28,7 +31,7 @@ args = parser.parse_args() batch_name = args.batch_name -copy_cplex = args.copy_cplex +copy_solver_settings = args.copy_solver_settings copy_srun_template = args.copy_srun_template force = args.force more_copyfiles = [i for i in args.more_copyfiles.split(',') if len(i)] @@ -37,7 +40,7 @@ # #%% Inputs for debugging # batch_name = 'v20231113_yamM0' -# copy_cplex = 1 +# copy_solver_settings = True # copy_srun_template = True # force = True # more_copyfiles = ['report.gms'] @@ -46,7 +49,7 @@ ###### Procedure #%% Shared parameters -reeds_path = os.path.dirname(os.path.abspath(__file__)) +reeds_path = reeds.io.reeds_path #%% Get all runs dictruns = get_run_status(reeds_path, batch_name) @@ -69,15 +72,6 @@ quit() -#%% Get the cplex file to copy -if copy_cplex: - if copy_cplex == 1: - cplex_file = os.path.join(reeds_path,'cplex.opt') - else: - cplex_file = os.path.join(reeds_path,f'cplex.op{copy_cplex}') -else: - cplex_file = None - #%% Copy the header from the srun_template.sh file if desired if copy_srun_template: srun_template = os.path.join(reeds_path,'reeds','hpc','srun_template.sh') @@ -92,9 +86,12 @@ for case in runs_failed: casename = os.path.basename(case) - #%% Copy the cplex file if desired - if copy_cplex: - shutil.copy(cplex_file, os.path.join(case,'')) + #%% Copy the solver settings file if desired + if copy_solver_settings: + fpath_settings = Path( + reeds.io.reeds_path, 'reeds', 'solver', reeds.inputs.get_optfile(case) + ) + shutil.copy(fpath_settings, os.path.join(case,'')) #%% Copy additional files if desired for f in more_copyfiles: diff --git a/reeds/io.py b/reeds/io.py index 98b7d4ad..8ec83cf3 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -622,7 +622,7 @@ def standardize_case(case=None): def get_switches(case=None, **kwargs): """ Get pd.Series of switch values from switches.csv, ra_switches.csv, - and CPLEX opt file. + and solver settings file. Accepts either {case} or {case}/inputs_case as input. If {case} is None, the default switch values listed in cases.csv are retrieved. @@ -680,7 +680,7 @@ def get_switches(case=None, **kwargs): int(y) for y in sw.get('GSw_FutureHydCF_RepYears', _fallback).split('_') ] ### Get number of threads to use in PRAS - opt_file = 'cplex.opt' if int(sw.GSw_gopt) == 1 else f'cplex.op{sw.GSw_gopt}' + opt_file = reeds.inputs.get_optfile(case) try: threads = get_param_value(os.path.join(case, 'reeds', 'solver', opt_file), "threads", dtype=int) except (FileNotFoundError, TypeError): From 1712bd3277388f8303d90cc3ed26fd91dba70ee4 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:26:25 -0600 Subject: [PATCH 21/34] move get_optfile() from reeds.inputs to reeds.io --- helpers/restart_runs.py | 2 +- reeds/input_processing/copy_files.py | 2 +- reeds/inputs.py | 19 --------- reeds/io.py | 63 ++++++++++++++++++++++------ 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/helpers/restart_runs.py b/helpers/restart_runs.py index 71381707..f1132e2e 100644 --- a/helpers/restart_runs.py +++ b/helpers/restart_runs.py @@ -89,7 +89,7 @@ #%% Copy the solver settings file if desired if copy_solver_settings: fpath_settings = Path( - reeds.io.reeds_path, 'reeds', 'solver', reeds.inputs.get_optfile(case) + reeds.io.reeds_path, 'reeds', 'solver', reeds.io.get_optfile(case) ) shutil.copy(fpath_settings, os.path.join(case,'')) diff --git a/reeds/input_processing/copy_files.py b/reeds/input_processing/copy_files.py index d11c92a5..4e15b49c 100644 --- a/reeds/input_processing/copy_files.py +++ b/reeds/input_processing/copy_files.py @@ -1302,7 +1302,7 @@ def write_miscellaneous_files( """ ### Solver file case = Path(inputs_case).parent - optfile = reeds.inputs.get_optfile(case) + optfile = reeds.io.get_optfile(case) shutil.copy(Path(reeds_path, 'reeds', 'solver', optfile), case) ### Parsed switches diff --git a/reeds/inputs.py b/reeds/inputs.py index 5c1639f5..416bebbd 100644 --- a/reeds/inputs.py +++ b/reeds/inputs.py @@ -338,25 +338,6 @@ def parse_cases( return dfcases_out -def get_optfile(case=None, **kwargs): - """ - Get the name of the optfile used by GAMS, formatted as described by - https://gams.com/49/docs/UG_GamsCall.html#GAMSAOoptfile - """ - sw = reeds.io.get_switches(case, **kwargs) - GSw_gopt = int(sw.GSw_gopt) - if GSw_gopt == 1: - suffix = 'opt' - elif len(str(GSw_gopt)) == 1: - suffix = f'op{GSw_gopt}' - elif len(str(GSw_gopt)) == 2: - suffix = f'o{GSw_gopt}' - else: - suffix = str(GSw_gopt) - optfile = f'{sw.solver}.{suffix}'.lower() - return optfile - - def solvestring_sequential( batch_case, caseSwitches, cur_year, next_year, prev_year, restartfile, diff --git a/reeds/io.py b/reeds/io.py index 8ec83cf3..c1c4841e 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -619,10 +619,9 @@ def standardize_case(case=None): return case -def get_switches(case=None, **kwargs): +def get_switches_base(case=None, **kwargs): """ - Get pd.Series of switch values from switches.csv, ra_switches.csv, - and solver settings file. + Get pd.Series of switch values from switches.csv. Accepts either {case} or {case}/inputs_case as input. If {case} is None, the default switch values listed in cases.csv are retrieved. @@ -646,6 +645,44 @@ def get_switches(case=None, **kwargs): index_col=0, header=None, ).squeeze(1) + return sw + + +def get_optfile(case=None, **kwargs): + """ + Get the name of the optfile used by GAMS, formatted as described by + https://gams.com/49/docs/UG_GamsCall.html#GAMSAOoptfile + """ + sw = reeds.io.get_switches_base(case, **kwargs) + GSw_gopt = int(sw.GSw_gopt) + if GSw_gopt == 1: + suffix = 'opt' + elif len(str(GSw_gopt)) == 1: + suffix = f'op{GSw_gopt}' + elif len(str(GSw_gopt)) == 2: + suffix = f'o{GSw_gopt}' + else: + suffix = str(GSw_gopt) + optfile = f'{sw.solver}.{suffix}'.lower() + return optfile + + +def get_switches(case=None, **kwargs): + """ + Get pd.Series of switch values from switches.csv, ra_switches.csv, + and solver settings file. + Accepts either {case} or {case}/inputs_case as input. + + If {case} is None, the default switch values listed in cases.csv are retrieved. + + If additional keyword arguments are provided, they replace the values specified + in {case}. This behavior can be used to read all the switches for a case (or all + the default settings) but change a single switch to a different value (when + making plots for different input settings, for example). If a key is provided + that is not a valid switch name, it is ignored. + """ + case = standardize_case(case) + sw = get_switches_base(case) ### Resource-adequacy-specific switches try: fpath_asw = os.path.join( @@ -679,22 +716,22 @@ def get_switches(case=None, **kwargs): sw['future_hydcf_rep_years_list'] = [ int(y) for y in sw.get('GSw_FutureHydCF_RepYears', _fallback).split('_') ] - ### Get number of threads to use in PRAS - opt_file = reeds.inputs.get_optfile(case) - try: - threads = get_param_value(os.path.join(case, 'reeds', 'solver', opt_file), "threads", dtype=int) - except (FileNotFoundError, TypeError): - threads = get_param_value(os.path.join(reeds_path, 'reeds', 'solver', opt_file), "threads", dtype=int) + ## Get number of threads to use in PRAS + opt_file = reeds.io.get_optfile(case) + fpath_opt = Path(case, 'reeds', 'solver', opt_file) + if not fpath_opt.is_file(): + fpath_opt = Path(reeds_path, 'reeds', 'solver', opt_file) + threads = get_param_value(fpath_opt, "threads", dtype=int) sw['threads'] = threads - ### Determine whether run is on HPC + ## Determine whether run is on HPC sw['hpc'] = True if int(os.environ.get('REEDS_USE_SLURM', 0)) else False - ### Add the run location + ## Add the run location sw['casedir'] = case sw['reeds_path'] = reeds_path if case is None else os.path.dirname(os.path.dirname(case)) - ### Get the number of hours per period to use in plots + ## Get the number of hours per period to use in plots sw['hoursperperiod'] = {'day': 24, 'wek': 120, 'year': 24}[sw['GSw_HourlyType']] sw['periodsperyear'] = {'day': 365, 'wek': 73, 'year': 365}[sw['GSw_HourlyType']] - + ### Overwrite values with keyword arguments if provided for key, value in kwargs.items(): if key in sw.keys(): sw[key] = value From e70602e50f62c016d7d4455879f57fdcce62aeab Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:30:35 -0600 Subject: [PATCH 22/34] move reeds.output_calc -> reeds.reports --- postprocessing/tableau/tableau_viz_suite.py | 24 ++++++++++----------- postprocessing/uncertainty_plots.py | 5 ++--- reeds/__init__.py | 2 +- reeds/core/terminus/report_dump.py | 4 ++-- reeds/{output_calc.py => results.py} | 5 ++--- 5 files changed, 19 insertions(+), 21 deletions(-) rename reeds/{output_calc.py => results.py} (99%) diff --git a/postprocessing/tableau/tableau_viz_suite.py b/postprocessing/tableau/tableau_viz_suite.py index 5e0e679f..6f36aa5b 100644 --- a/postprocessing/tableau/tableau_viz_suite.py +++ b/postprocessing/tableau/tableau_viz_suite.py @@ -417,7 +417,7 @@ def calc_peakload( ).rename(columns=int) dictout = {} - level_map = reeds.output_calc.get_level_map(case) + level_map = reeds.results.get_level_map(case) for level in levels: df_level = df.loc[level,years].stack().rename_axis(['r','t']).rename('Value').reset_index().astype({'t':int}).set_index(['r','t']).squeeze().reset_index() df_level['Spatial Resolution'] = level_map[level] @@ -426,7 +426,7 @@ def calc_peakload( df = pd.concat(dictout.values(), axis=0, ignore_index=True) # # convert MW to GW for national data - df = reeds.output_calc.scale_column(df,**{'scale_factor': 1e-3, 'column':'Value'}) + df = reeds.results.scale_column(df,**{'scale_factor': 1e-3, 'column':'Value'}) return df @@ -477,7 +477,7 @@ def calc_transmission_map(case,level='transgrp'): trans_total_new = pd.concat([trans_total_new, tran_new], axis=0) # convert MW to GW - trans_total_new = reeds.output_calc.scale_column(trans_total_new,**{'scale_factor': 1e-3, 'column':'Value'}) + trans_total_new = reeds.results.scale_column(trans_total_new,**{'scale_factor': 1e-3, 'column':'Value'}) return trans_total_new @@ -602,7 +602,7 @@ def export(dictin,output_dir): create_scenarios_csv(output_dir,cases) # Grab clean display names for the levels - level_map = reeds.output_calc.get_level_map(cases[basecase]) + level_map = reeds.results.get_level_map(cases[basecase]) # import some key inputs from ReEDS dictin_sw = {case: reeds.io.get_switches(cases[case]) for case in cases} @@ -630,7 +630,7 @@ def export(dictin,output_dir): dictin_cap = {} metric = 'Capacity (GW)' for case in tqdm(cases, desc=metric): - dictin_cap[case] = reeds.output_calc.calc_cap(cases[case]) + dictin_cap[case] = reeds.results.calc_cap(cases[case]) dictin_cap[case] = reformat(dictin_cap[case],case,metric,years[case]) dictin_cap = set_zero_values(dictin_cap) dictin[metric] = dictin_cap @@ -639,7 +639,7 @@ def export(dictin,output_dir): dictin_gen = {} metric = 'Generation (TWh)' for case in tqdm(cases, desc=metric): - dictin_gen[case] = reeds.output_calc.calc_gen(cases[case]) + dictin_gen[case] = reeds.results.calc_gen(cases[case]) dictin_gen[case] = reformat(dictin_gen[case],case,metric,years[case]) dictin_gen = set_zero_values(dictin_gen) dictin[metric] = dictin_gen @@ -648,7 +648,7 @@ def export(dictin,output_dir): dictin_emissions = {} metric = 'Emissions (million metric tonnes)' for case in tqdm(cases, desc=metric): - dictin_emissions[case] = reeds.output_calc.calc_emissions(cases[case]) + dictin_emissions[case] = reeds.results.calc_emissions(cases[case]) dictin_emissions[case] = dictin_emissions[case].loc[dictin_emissions[case]['e'] != 'H2'] # remove hydrogen emissions from the dataframe dictin_emissions[case] = reformat(dictin_emissions[case],case,metric,years[case]) dictin[metric] = dictin_emissions @@ -665,7 +665,7 @@ def export(dictin,output_dir): dictin_annualload = {} metric = 'Annual End-Use Electricity Demand (TWh)' for case in tqdm(cases, desc=metric): - dictin_annualload[case] = reeds.output_calc.calc_annualload(cases[case],dictin_scalars[case]) + dictin_annualload[case] = reeds.results.calc_annualload(cases[case],dictin_scalars[case]) dictin_annualload[case] = reformat(dictin_annualload[case],case,metric,years[case]) dictin[metric] = dictin_annualload @@ -673,7 +673,7 @@ def export(dictin,output_dir): dictin_systemcost = {} metric = 'System Cost (billion $)' for case in tqdm(cases, desc=metric): - dictin_systemcost[case] = reeds.output_calc.calc_systemcost(cases[case],group_r=False,drop_zeros=False).rename(columns={'year':'t','Discounted Cost (Bil $)':'Value'}) + dictin_systemcost[case] = reeds.results.calc_systemcost(cases[case],group_r=False,drop_zeros=False).rename(columns={'year':'t','Discounted Cost (Bil $)':'Value'}) del dictin_systemcost[case]['Cost (Bil $)'] dictin_systemcost[case].cost_cat = dictin_systemcost[case].cost_cat.map(lambda x: output_formatting['cost_cat_map'].get(x,x)) dictin_systemcost[case] = reformat(dictin_systemcost[case],case,metric,years[case]) @@ -703,7 +703,7 @@ def export(dictin,output_dir): metric = 'Transmission Capacity (TW-miles)' for case in tqdm(cases, desc=metric): # pull the total installed transmission capacity from ReEDS outputs - dictin_trans_total[case], _ = reeds.output_calc.calc_transmission_capacity(cases[case],levels=['transgrp']) + dictin_trans_total[case], _ = reeds.results.calc_transmission_capacity(cases[case],levels=['transgrp']) tran_total = dictin_trans_total[case] tran_total['Measure'] = 'Total installed' # make one df with the new capacity @@ -726,7 +726,7 @@ def export(dictin,output_dir): dictin_h2prod = {} metric = 'Hydrogen Production (million metric tonnes)' for case in tqdm(cases, desc=metric): - dictin_h2prod[case] = reeds.output_calc.calc_h2prod(cases[case]) + dictin_h2prod[case] = reeds.results.calc_h2prod(cases[case]) dictin_h2prod[case] = reformat(dictin_h2prod[case],case,metric,years[case]) dictin[metric] = dictin_h2prod @@ -734,7 +734,7 @@ def export(dictin,output_dir): dictin_sited_load = {} metric = 'Load Site Capacity (MW)' for case in tqdm(cases, desc=metric): - dictin_sited_load[case] = reeds.output_calc.calc_sited_load(cases[case]) + dictin_sited_load[case] = reeds.results.calc_sited_load(cases[case]) dictin_sited_load[case] = reformat(dictin_sited_load[case],case,metric,years[case]) dictin[metric] = dictin_sited_load diff --git a/postprocessing/uncertainty_plots.py b/postprocessing/uncertainty_plots.py index 1cc51802..84f4a0f4 100644 --- a/postprocessing/uncertainty_plots.py +++ b/postprocessing/uncertainty_plots.py @@ -21,7 +21,6 @@ import matplotlib.pyplot as plt from matplotlib.lines import Line2D from matplotlib.ticker import FuncFormatter -import pptx from pptx.util import Inches import io import seaborn as sns @@ -626,7 +625,7 @@ def _fetch_tran_mi_out_detail(self, case: str) -> pd.DataFrame: ) df_tech_trans = ( - reeds.output_calc.calc_reinforcement_spur_capacity_miles(self.cases_dict[case]) + reeds.results.calc_reinforcement_spur_capacity_miles(self.cases_dict[case]) ) df_tech_trans['rr'] = df_tech_trans['r'] # Duplicate region data # Convert "Trans (GW-mi)" to "Trans (TW-mi)" @@ -681,7 +680,7 @@ def _fetch_npv_r(self, case: str) -> pd.DataFrame: Returns: pd.DataFrame: Columns ['year', 'r', 'cost_cat', 'Cost (Bil $)', 'Discounted Cost (Bil $)'] """ - return reeds.output_calc.calc_systemcost(self.cases_dict[case]) + return reeds.results.calc_systemcost(self.cases_dict[case]) ### =========================================================================== diff --git a/reeds/__init__.py b/reeds/__init__.py index fc27b8ec..178654fb 100644 --- a/reeds/__init__.py +++ b/reeds/__init__.py @@ -6,13 +6,13 @@ from . import inputs as inputs from . import io as io from . import log as log -from . import output_calc as output_calc from . import plots as plots from . import prasplots as prasplots from . import reedsplots as reedsplots from . import remote as remote from . import report_utils as report_utils from . import resource_adequacy as resource_adequacy +from . import results as results from . import spatial as spatial from . import techs as techs from . import timeseries as timeseries diff --git a/reeds/core/terminus/report_dump.py b/reeds/core/terminus/report_dump.py index bbb63b93..1a8b388c 100644 --- a/reeds/core/terminus/report_dump.py +++ b/reeds/core/terminus/report_dump.py @@ -200,13 +200,13 @@ def postprocess_outputs(case, outputs_path=None, verbose=0): _outputs_path = os.path.join(case, 'outputs') if outputs_path is None else outputs_path ## System cost - reeds.output_calc.calc_systemcost(case).to_csv( + reeds.results.calc_systemcost(case).to_csv( os.path.join(_outputs_path, 'post_systemcost_annualized.csv'), index=False, ) ## Reinforcement and spur-line - reeds.output_calc.calc_reinforcement_spur_capacity_miles(case).to_csv( + reeds.results.calc_reinforcement_spur_capacity_miles(case).to_csv( os.path.join(_outputs_path, 'post_tech_transmission.csv'), index=False, ) diff --git a/reeds/output_calc.py b/reeds/results.py similarity index 99% rename from reeds/output_calc.py rename to reeds/results.py index a641bfe5..d2f9bb26 100644 --- a/reeds/output_calc.py +++ b/reeds/results.py @@ -10,7 +10,6 @@ import os import sys import pandas as pd -import numpy as np from itertools import product sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) @@ -660,7 +659,7 @@ def calc_transmission_capacity(case,levels): # pull intra and interregional transmission data into dictionaries for processing dict_inter = {} - level_map = reeds.output_calc.get_level_map(case) + level_map = get_level_map(case) hierarchy = reeds.io.get_hierarchy(case) for level in levels: @@ -679,7 +678,7 @@ def calc_transmission_capacity(case,levels): dict_inter[spatial_resolution] = pd.concat({ r: ( trans_r.loc[ - (trans_r[f'inter_transreg'] == 1) + (trans_r['inter_transreg'] == 1) ].groupby(['trtype','t']).MW.sum() .rename('GW') / 1e3 From 1bd27a8e8b8da658de692fa1adb377e9a9ec7d54 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:38:46 -0600 Subject: [PATCH 23/34] fix optfile path --- reeds/io.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reeds/io.py b/reeds/io.py index c1c4841e..ecd4cec2 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -653,7 +653,7 @@ def get_optfile(case=None, **kwargs): Get the name of the optfile used by GAMS, formatted as described by https://gams.com/49/docs/UG_GamsCall.html#GAMSAOoptfile """ - sw = reeds.io.get_switches_base(case, **kwargs) + sw = get_switches_base(case, **kwargs) GSw_gopt = int(sw.GSw_gopt) if GSw_gopt == 1: suffix = 'opt' @@ -717,8 +717,8 @@ def get_switches(case=None, **kwargs): int(y) for y in sw.get('GSw_FutureHydCF_RepYears', _fallback).split('_') ] ## Get number of threads to use in PRAS - opt_file = reeds.io.get_optfile(case) - fpath_opt = Path(case, 'reeds', 'solver', opt_file) + opt_file = get_optfile(case) + fpath_opt = Path(case, opt_file) if not fpath_opt.is_file(): fpath_opt = Path(reeds_path, 'reeds', 'solver', opt_file) threads = get_param_value(fpath_opt, "threads", dtype=int) From 278571f024735dd9fad9843f0fbc151e5e8e7b3b Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:00:52 -0600 Subject: [PATCH 24/34] fix runstatus.py and optfile path --- helpers/runstatus.py | 9 +++++---- reeds/io.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/helpers/runstatus.py b/helpers/runstatus.py index 2002cc6e..12a11548 100644 --- a/helpers/runstatus.py +++ b/helpers/runstatus.py @@ -1,10 +1,14 @@ #%% Imports import os import re +import sys import datetime import subprocess import argparse from glob import glob +from pathlib import Path +sys.path.append(str(Path(__file__).parent.parent)) +import reeds # #%% Inputs for debugging # batch_name = 'v20250812_mcK0' @@ -91,11 +95,8 @@ def get_run_status(reeds_path, batch_name): include_finished = args.include_finished verbose = args.verbose - #%% Shared parameters - reeds_path = os.path.dirname(os.path.abspath(__file__)) - dictruns = get_run_status(reeds_path, batch_name) - #%%### Loop through categories and runs and report their status + dictruns = get_run_status(reeds.io.reeds_path, batch_name) for key, runs in dictruns.items(): text = f'{key}: {len(runs)}' print(f"\n{text}\n{'-'*len(text)}") diff --git a/reeds/io.py b/reeds/io.py index ecd4cec2..963d1474 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -717,10 +717,15 @@ def get_switches(case=None, **kwargs): int(y) for y in sw.get('GSw_FutureHydCF_RepYears', _fallback).split('_') ] ## Get number of threads to use in PRAS + ## (read from case folder; fall back to repo if case folder doesn't exist yet) opt_file = get_optfile(case) - fpath_opt = Path(case, opt_file) - if not fpath_opt.is_file(): - fpath_opt = Path(reeds_path, 'reeds', 'solver', opt_file) + fpath_repo = Path(reeds_path, 'reeds', 'solver', opt_file) + if case is None: + fpath_opt = fpath_repo + else: + fpath_opt = Path(case, opt_file) + if not fpath_opt.is_file(): + fpath_opt = fpath_repo threads = get_param_value(fpath_opt, "threads", dtype=int) sw['threads'] = threads ## Determine whether run is on HPC From 187ff2b1acc1c9a50688b50bf6876a71047f87b5 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:14:53 -0600 Subject: [PATCH 25/34] reeds.io.read_output(): add backwards-compatibility to keep compare_cases.py working across the transition --- reeds/io.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/reeds/io.py b/reeds/io.py index 963d1474..be9f3256 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -490,10 +490,11 @@ def read_output( df[col] = df[col].str.decode('utf-8') except KeyError: ## Empty dataframes aren't written to h5 file, so make one ourselves - report_params = pd.read_csv( - os.path.join(case, 'reeds', 'core', 'terminus', 'report_params.csv'), - comment='#', - ) + fpath = Path(case, 'reeds', 'core', 'terminus', 'report_params.csv') + ## Fall back to older params list if necessary for backwards compatibility + if not fpath.is_file(): + fpath = Path(case, 'e_report_params.csv') + report_params = pd.read_csv(fpath, comment='#') _index = report_params.loc[ report_params.param.map(lambda x: x.split('(')[0]) == key, 'param' ].squeeze() From 60683240e41e50c39f4122d5983cfb0d75ca7fcd Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:38:29 -0600 Subject: [PATCH 26/34] import gdxpds first to avoid warning --- postprocessing/raw_value_streams.py | 4 ++-- .../retail_rate_module/retail_rate_calculations.py | 2 +- reeds/__init__.py | 9 +++++---- reeds/core/solve/1_tc_phaseout.py | 2 +- reeds/core/terminus/report_dump.py | 2 +- reeds/input_processing/aggregate_regions.py | 2 +- reeds/input_processing/forecast.py | 2 +- reeds/resource_adequacy/capacity_credit.py | 2 +- reeds/resource_adequacy/diagnostic_plots.py | 2 +- reeds/resource_adequacy/prep_data.py | 2 +- reeds/resource_adequacy/ra_calcs.py | 2 +- runreeds.py | 2 +- 12 files changed, 17 insertions(+), 16 deletions(-) diff --git a/postprocessing/raw_value_streams.py b/postprocessing/raw_value_streams.py index d2f95961..7ed4b033 100644 --- a/postprocessing/raw_value_streams.py +++ b/postprocessing/raw_value_streams.py @@ -3,8 +3,8 @@ from GAMS gdx solution file to produce value streams for the variables of the model. ''' -import pandas as pd import gdxpds +import pandas as pd import subprocess from datetime import datetime import logging @@ -250,4 +250,4 @@ def get_df_symbols(dfs, symbols): df_syms = pd.concat(df_syms).reset_index(drop=True) for col in ['sym_name','sym_set']: df_syms[col] = df_syms[col].str.lower() - return df_syms \ No newline at end of file + return df_syms diff --git a/postprocessing/retail_rate_module/retail_rate_calculations.py b/postprocessing/retail_rate_module/retail_rate_calculations.py index df48bb7b..f29c6422 100644 --- a/postprocessing/retail_rate_module/retail_rate_calculations.py +++ b/postprocessing/retail_rate_module/retail_rate_calculations.py @@ -4,9 +4,9 @@ import argparse import datetime import itertools +import gdxpds import pandas as pd import numpy as np -import gdxpds import os import sys import urllib diff --git a/reeds/__init__.py b/reeds/__init__.py index 178654fb..ff723dfd 100644 --- a/reeds/__init__.py +++ b/reeds/__init__.py @@ -1,8 +1,10 @@ -from . import checks as checks +from . import input_processing as input_processing from . import core as core -from . import financials as financials from . import hpc as hpc -from . import input_processing as input_processing +from . import resource_adequacy as resource_adequacy + +from . import checks as checks +from . import financials as financials from . import inputs as inputs from . import io as io from . import log as log @@ -11,7 +13,6 @@ from . import reedsplots as reedsplots from . import remote as remote from . import report_utils as report_utils -from . import resource_adequacy as resource_adequacy from . import results as results from . import spatial as spatial from . import techs as techs diff --git a/reeds/core/solve/1_tc_phaseout.py b/reeds/core/solve/1_tc_phaseout.py index 4fb89959..089e8a0c 100644 --- a/reeds/core/solve/1_tc_phaseout.py +++ b/reeds/core/solve/1_tc_phaseout.py @@ -14,9 +14,9 @@ ########### #%% IMPORTS import argparse +import gdxpds import pandas as pd import numpy as np -import gdxpds import os import sys from pathlib import Path diff --git a/reeds/core/terminus/report_dump.py b/reeds/core/terminus/report_dump.py index 1a8b388c..7e3ef747 100644 --- a/reeds/core/terminus/report_dump.py +++ b/reeds/core/terminus/report_dump.py @@ -7,8 +7,8 @@ import os import traceback import sys -import pandas as pd import gdxpds +import pandas as pd from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent.parent)) import reeds diff --git a/reeds/input_processing/aggregate_regions.py b/reeds/input_processing/aggregate_regions.py index 74ac5621..08bf607e 100644 --- a/reeds/input_processing/aggregate_regions.py +++ b/reeds/input_processing/aggregate_regions.py @@ -21,8 +21,8 @@ import argparse import numpy as np import os -import pandas as pd import gdxpds +import pandas as pd import shutil import sys import datetime diff --git a/reeds/input_processing/forecast.py b/reeds/input_processing/forecast.py index 13843d74..eca399d8 100644 --- a/reeds/input_processing/forecast.py +++ b/reeds/input_processing/forecast.py @@ -16,9 +16,9 @@ ### =========================================================================== import argparse +import gdxpds import pandas as pd import numpy as np -import gdxpds import os import sys import shutil diff --git a/reeds/resource_adequacy/capacity_credit.py b/reeds/resource_adequacy/capacity_credit.py index c320deff..0440e102 100644 --- a/reeds/resource_adequacy/capacity_credit.py +++ b/reeds/resource_adequacy/capacity_credit.py @@ -1,8 +1,8 @@ #%% IMPORTS import os +import gdxpds import numpy as np import pandas as pd -import gdxpds import reeds diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index bc22f1a3..d8731ba1 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -1,6 +1,7 @@ #%%### Imports import os import sys +import gdxpds import pandas as pd import numpy as np import matplotlib as mpl @@ -8,7 +9,6 @@ from matplotlib import patheffects as pe from glob import glob import traceback -import gdxpds import cmocean ### Local imports from pathlib import Path diff --git a/reeds/resource_adequacy/prep_data.py b/reeds/resource_adequacy/prep_data.py index 872cdbd0..f7ad96c6 100644 --- a/reeds/resource_adequacy/prep_data.py +++ b/reeds/resource_adequacy/prep_data.py @@ -25,9 +25,9 @@ #%% General imports import os import re +import gdxpds import pandas as pd import numpy as np -import gdxpds ### Local imports import reeds diff --git a/reeds/resource_adequacy/ra_calcs.py b/reeds/resource_adequacy/ra_calcs.py index d3df43aa..fc296808 100644 --- a/reeds/resource_adequacy/ra_calcs.py +++ b/reeds/resource_adequacy/ra_calcs.py @@ -4,8 +4,8 @@ import sys import subprocess import datetime -import pandas as pd import gdxpds +import pandas as pd from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent)) import reeds diff --git a/runreeds.py b/runreeds.py index 80a3bd59..66665326 100644 --- a/runreeds.py +++ b/runreeds.py @@ -2,6 +2,7 @@ ### --- IMPORTS --- ### =========================================================================== +import reeds import os import git import queue @@ -17,7 +18,6 @@ from datetime import datetime import argparse from pathlib import Path -import reeds # Assert core programs are accessible CORE_PROGRAMS = ["gams"] From dfb68348d8cee8143e1e6efb6142461c293b3f15 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:38:43 -0600 Subject: [PATCH 27/34] fix casemaker.py yearset --- preprocessing/casematrix_example.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preprocessing/casematrix_example.yaml b/preprocessing/casematrix_example.yaml index 664f8639..bf676e10 100644 --- a/preprocessing/casematrix_example.yaml +++ b/preprocessing/casematrix_example.yaml @@ -4,7 +4,7 @@ ### Shared settings are applied to all cases. ### Any settings not specified use the defaults from cases.csv. shared: - yearset: 2010_2015_2020_2025_2030_2035_2040_2045_2050 + yearset: 2010..2050..5 ################## ### Dimensions ### From 2d282ec9671c41b13adad0e529d62f32eec98bc1 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 4 May 2026 10:05:34 -0600 Subject: [PATCH 28/34] remove references to non-existing rabatch.py/augurbatch.py --- runreeds.py | 40 +++++++--------------------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/runreeds.py b/runreeds.py index 66665326..33bada60 100644 --- a/runreeds.py +++ b/runreeds.py @@ -637,8 +637,6 @@ def setup_intertemporal( caseSwitches, startiter, niter, ccworkers, solveyears, endyear, batch_case, toLogGamsString, modeledyears, OPATH, ): - ### beginning year is passed to rabatch - begyear = min(solveyears) ### first save file from e_solveprep is just the case name savefile = batch_case ### if this is the first iteration @@ -677,21 +675,11 @@ def setup_intertemporal( ## check to see if the save file exists OPATH.writelines(writeerrorcheck(os.path.join("g00files",savefile + ".g*"))) - ## start threads for cc/curt - ## no need to run cc curt scripts for final iteration + ## Run resource adequacy calculations (no need to run for final iteration) if i < niter-1: - ## batch out calls to rabatch - OPATH.writelines( - "python rabatch.py " + batch_case + " " + str(ccworkers) + " " - + modeledyears + " " + savefile + " " + str(begyear) + " " - + str(endyear) + " " + caseSwitches['distpvscen'] + " " - + str(caseSwitches['calc_csp_cc']) + " " - + str(caseSwitches['timetype']) + " " - + str(caseSwitches['GSw_WaterMain']) + " " + str(i) + " " - + str(caseSwitches['marg_vre_mw']) + " " - + str(caseSwitches['marg_stor_mw']) + " " - + str(caseSwitches['marg_evmc_mw']) + " " - + '\n') + ## TODO: Run the RA calculations via ra_calcs.py for each solve year + ## (needs to be reimplemented) + ## merge all the resulting gdx files ## the output file will be for the next iteration nextiter = i+1 @@ -725,12 +713,6 @@ def setup_window( ### for windows indicated in the csv file for win in win_in[1:]: - - ## beginning year is the first column (start) - begyear = win[1] - ## end year is the second column (end) - endyear = win[2] - ## for the number of iterations we have... for i in range(startiter,niter): big_comment(f'Window: {win}', OPATH) comment(f'Iteration: {i}', OPATH) @@ -748,17 +730,9 @@ def setup_window( + " --maxiter=" + str(niter-1) + " --case=" + batch_case + " --window=" + win[0] + ' \n') ## start threads for cc/curt OPATH.writelines(writeerrorcheck(os.path.join("g00files",savefile + ".g*"))) - OPATH.writelines( - "python rabatch.py " + batch_case + " " + str(ccworkers) + " " - + modeledyears + " " + savefile + " " + str(begyear) + " " - + str(endyear) + " " + caseSwitches['distpvscen'] + " " - + str(caseSwitches['calc_csp_cc']) + " " - + str(caseSwitches['timetype']) + " " - + str(caseSwitches['GSw_WaterMain']) + " " + str(i) + " " - + str(caseSwitches['marg_vre_mw']) + " " - + str(caseSwitches['marg_stor_mw']) + " " - + str(caseSwitches['marg_evmc_mw']) + " " - + '\n') + ## TODO: Run the RA calculations via ra_calcs.py for each solve year + ## in window (needs to be reimplemented) + ## merge all the resulting r2_in gdx files ## the output file will be for the next iteration nextiter = i+1 From 0856d36cd1eb7fd43cef1da1cf16a7c021afbeb3 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 4 May 2026 10:54:05 -0600 Subject: [PATCH 29/34] update developer_best_practices.md for pathlib; remove reference to nonexistent runreeds_aws.py --- docs/source/developer_best_practices.md | 3 ++- reeds/hpc/aws_setup.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/developer_best_practices.md b/docs/source/developer_best_practices.md index 092a3310..2e54f6e0 100644 --- a/docs/source/developer_best_practices.md +++ b/docs/source/developer_best_practices.md @@ -169,7 +169,8 @@ _Some exceptions to this might exist due to number scaling (e.g., emission rates * In general, if inputs require calculations before they are ingested into b_inputs, those calculations should be done in Python rather than in GAMS. GAMS can be used for calculations where the GAMS syntax simplifies the calculation or where upstream dependencies make it challenging for the calculations to happen in Python preprocessing scripts. -* In Python, file paths should be added using os.path.join() rather than writing out the filepath with slashes. +* In Python, file paths should be specified using `from pathlib import Path` and `Path(reeds.io.reeds_path, 'foldername', 'maybe_more_foldernames', 'filename.extension')` instead of writing out the filepath as a string with slashes. +Use absolute filepaths whenever possible. * Data column headers should use the ReEDS set names when practical. * Example: data that include regions should use "r" for the column name rather than "ba", "reeds_ba", or "region". diff --git a/reeds/hpc/aws_setup.sh b/reeds/hpc/aws_setup.sh index b6fdd6c3..9dd9f317 100644 --- a/reeds/hpc/aws_setup.sh +++ b/reeds/hpc/aws_setup.sh @@ -97,7 +97,7 @@ git clone git@github.nrel.gov:ReEDS/ReEDS-2.0 # Run ReEDS! # (using nohup to keep the process from dying when you end your ssh session) -#nohup python runreeds_aws.py -c weekendcentroid -r 4 -b centwknd > myout.txt & +#nohup python runreeds.py -c weekendcentroid -r 4 -b centwknd > myout.txt & #======================================== From c93ccda3dbc3f32f9a532d440da3954c1f7541d4 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 11 May 2026 17:33:08 -0600 Subject: [PATCH 30/34] adapt labeler.yaml for new structure --- .github/labeler.yaml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/labeler.yaml b/.github/labeler.yaml index e0558a5b..5268fb03 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -9,6 +9,7 @@ dependencies: - "Project.toml" - "Manifest.toml" - ".github/dependabot.yml" + - "instantiate.jl" github_actions: - changed-files: @@ -42,15 +43,8 @@ tests: model_changes: - changed-files: - any-glob-to-any-file: - - "*.gms" - - "**/*.gms" - - "runbatch.py" - - "d_solve*.py" - "reeds/**" - - "ReEDS_Augur/**" - - "Augur.py" - - "instantiate.jl" - - "reeds2pras/**" + - "runreeds.py" data_changes: - changed-files: @@ -60,19 +54,11 @@ data_changes: - "postprocessing/**/inputs/**" - "**/*.csv" - "**/*.h5" - - "sources.csv" - - "sources_documentation.md" hourlize: - changed-files: - any-glob-to-any-file: "hourlize/**" -input_processing: - - changed-files: - - any-glob-to-any-file: - - "input_processing/**" - - "preprocessing/**" - postprocessing: - changed-files: - any-glob-to-any-file: "postprocessing/**" From b080e30794fcd97fa93dc2808044575cdb594001 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 11 May 2026 17:35:26 -0600 Subject: [PATCH 31/34] move run_r2x.py from scripts/ to postprocessing/ --- .github/workflows/python-app.yaml | 2 +- {scripts => postprocessing}/run_r2x.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {scripts => postprocessing}/run_r2x.py (100%) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 1edbcbcd..1b00e616 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -290,7 +290,7 @@ jobs: R2X_WEATHER_YEAR: "2012" R2X_SYSTEM_JSON: ${{ format('{0}_system.json', matrix.scenario) }} run: | - uvx --from "r2x-reeds>=0.3.5" python scripts/run_r2x.py \ + uvx --from "r2x-reeds>=0.3.5" python postprocessing/run_r2x.py \ --reeds-run-path "$R2X_REEDS_RUN_PATH" \ --scenario "$R2X_SCENARIO" \ --solve-year "$R2X_SOLVE_YEAR" \ diff --git a/scripts/run_r2x.py b/postprocessing/run_r2x.py similarity index 100% rename from scripts/run_r2x.py rename to postprocessing/run_r2x.py From 90ca0b313ef269d68d1ef661df408cb7e1acdc4c Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Mon, 11 May 2026 17:41:50 -0600 Subject: [PATCH 32/34] move get_last_iter to reeds/core/terminus --- reeds/{ => core/terminus}/get_last_iter.py | 0 runreeds.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename reeds/{ => core/terminus}/get_last_iter.py (100%) diff --git a/reeds/get_last_iter.py b/reeds/core/terminus/get_last_iter.py similarity index 100% rename from reeds/get_last_iter.py rename to reeds/core/terminus/get_last_iter.py diff --git a/runreeds.py b/runreeds.py index f93cdd1d..6bcd6664 100644 --- a/runreeds.py +++ b/runreeds.py @@ -1349,11 +1349,12 @@ def write_batch_script( ) ### Otherwise, run for the last iteration (selected numerically) else: + fpath = os.path.join(casedir, "reeds", "core", "terminus", "get_last_iter.py") OPATH.writelines( - f'r=$(python {os.path.join(casedir, "reeds", "get_last_iter.py")} {batch_case} {max(solveyears)})\n' + f'r=$(python {fpath} {batch_case} {max(solveyears)})\n' if LINUXORMAC else f'for /f "delims=" %%i in ' - f'(\'python {os.path.join(casedir, "reeds", "get_last_iter.py")} {batch_case} {max(solveyears)}\')' + f'(\'python {fpath} {batch_case} {max(solveyears)}\')' f' do set "r=%%i"\n' ) OPATH.writelines( From 364d1650633ef22bacac03f76035275685e57bf6 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Tue, 12 May 2026 08:46:06 -0600 Subject: [PATCH 33/34] PR template: More specific filepaths Co-authored-by: kodiobika <35176195+kodiobika@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4f2982cb..74c3fa18 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -72,7 +72,7 @@ Include additional illustrative plots describing input data, methods, testing, a - [ ] No large data file(s) added/modified - [ ] No substantive impact on runtime for full-US reference case - [ ] No substantive impact on folder size for full-US reference case -- [ ] No change to process flow (runreeds.py, solve.py) +- [ ] No change to process flow (runreeds.py, reeds/core/solve/solve.py) - [ ] No change to code organization - [ ] No change to package requirements (environment.yml or Project.toml) From 0765044968eb32ef649ffdb190b32388b1015c18 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Tue, 12 May 2026 08:53:50 -0600 Subject: [PATCH 34/34] remove no-longer-used files --- hourlize/.pylintrc | 473 ------------------------- postprocessing/.pre-commit-config.yaml | 30 -- 2 files changed, 503 deletions(-) delete mode 100644 hourlize/.pylintrc delete mode 100644 postprocessing/.pre-commit-config.yaml diff --git a/hourlize/.pylintrc b/hourlize/.pylintrc deleted file mode 100644 index 7dc59014..00000000 --- a/hourlize/.pylintrc +++ /dev/null @@ -1,473 +0,0 @@ -# This file was created based on https://github.com/NatLabRockies/reVXOrdinances/blob/main/.pylintrc, -# and then modified for the use with ReEDS-2.0 -[MAIN] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CSV, config, data - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-paths=^.*hourlize/(?!(?:reeds_to_rev)|(?:test)).*?py$, - ^.*hourlize/tests/data/.*$ - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" - -# Use multiple processes to speed up Pylint. -jobs=1 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable= - import-error, - unspecified-encoding, - fixme, - too-many-locals, - import-error, - too-many-instance-attributes, - logging-fstring-interpolation, - no-else-break - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable= - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[BASIC] - -# Naming style matching correct argument names -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style -#argument-rgx= - -# Naming style matching correct attribute names -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style -#class-attribute-rgx= - -# Naming style matching correct class names -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming-style -#class-rgx= - -# Naming style matching correct constant names -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma -good-names=reVXOrdinances, - e, - i, - j, - s, - o, - df, - n, - c, - fh, - h, - sw, - yr, - r, - f - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=yes - -# Naming style matching correct inline iteration names -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style -#inlinevar-rgx= - -# Naming style matching correct method names -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style -#method-rgx= - -# Naming style matching correct module names -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style -#variable-rgx= - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module -max-module-lines=50000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=yes - -# Minimum lines number of a similarity. -min-similarity-lines=15 - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=10 - -# Maximum number of attributes for a class (see R0902). -max-attributes=10 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=builtins.Exception diff --git a/postprocessing/.pre-commit-config.yaml b/postprocessing/.pre-commit-config.yaml deleted file mode 100644 index 7d082ffd..00000000 --- a/postprocessing/.pre-commit-config.yaml +++ /dev/null @@ -1,30 +0,0 @@ -repos: -- repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - language_version: python3.9 - files: ^(hourlize/tests/.*\.py?|hourlize/reeds_to_rev.*\.py?)$ -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 - hooks: - - id: check-json - - id: check-yaml - exclude: ^conda.recipe/ - - id: end-of-file-fixer - exclude_types: [csv] - files: ^(hourlize/tests/.*\.py?|hourlize/reeds_to_rev.*\.py?)$ - - id: trailing-whitespace - - id: check-merge-conflict - - id: check-symlinks - - id: mixed-line-ending - exclude_types: [csv] - files: ^(hourlize/tests/.*\.py?|hourlize/reeds_to_rev.*\.py?)$ - - id: requirements-txt-fixer -- repo: https://github.com/PyCQA/pylint - rev: v2.16.2 - hooks: - - id: pylint - args: [ - "hourlize" - ] \ No newline at end of file