Skip to content

Update interzonal transmission costs/distances and use directly calculated values#95

Merged
patrickbrown4 merged 61 commits into
mainfrom
pb/transcost
Jun 5, 2026
Merged

Update interzonal transmission costs/distances and use directly calculated values#95
patrickbrown4 merged 61 commits into
mainfrom
pb/transcost

Conversation

@patrickbrown4
Copy link
Copy Markdown
Contributor

@patrickbrown4 patrickbrown4 commented May 15, 2026

Summary

In the same vein as 2003, this PR:

Technical details

Implementation notes

Least-cost paths

There are a few updates to the interzonal least-cost path procedure:

  • The interzonal transmission costs have been out of sync with interconnection costs since 1836. This PR brings them in sync, so both are now using costs from the MISO 2025 Transmission Cost Estimation Guide (parent, description, data)
    • A new pair of data files from MISO, inputs/transmission/conductor_{ac or dc}.csv, provide lookup tables for converting from the assumed kV to MW capacity
  • We used to assume 500 kV costs for all interzonal greenfield lines, regardless of location or scale (so tiny counties with low-voltage lines got to use 500 kV costs, which are cheaper on a USD/MW basis than lower-voltage lines). Here, we instead use the maximum AC voltage currently connecting each pair of zones, with a 138 kV floor. So interfaces that are crossed by higher-voltage lines get lower USD/MW expansion than interfaces crossed by lower-voltage lines.

Spatial flexibility

  • The old future-capacity scenarios (which were specific to z134) have been removed. The scenarios used in NTP have been maintained (planned_lines-NTP_{MT or P2P}.csv), but now specify individual from/to endpoints
  • Similarly, planned additions are now specified as individual endpoints
    • TransWest Express and SunZia have been added; see the descriptions in inputs/transmission/README.md
    • The offshore zone backbones are in inputs/transmission/newlinks_offshore_backbone.csv
    • The offshore zone radial connections are in inputs/zones/{GSw_ZoneSet}/newlinks_offshore_radial.csv
      • These need to be specified for each GSw_ZoneSet because the user might want to control which zone they make landfall in, particularly at county resolution
      • Offshore zones and county resolution are now compatible (they were not before)
  • We keep the old single-bin transmission upgrade supply curve (TSC) file for greenfield 500 kV with GSw_ZoneSet=z134 as an example of the required file format when using TSC, but will eventually remove it when we add a full supply curve
    • The greenfield costs are put into this format automatically in transmission.py

Additional changes

  • Features that we no longer need with the new format have been removed:
    • inputs/shapefiles/transmission_endpoints (now in inputs/zones/{GSw_ZoneSet}/zonehash.csv)
    • Transmission aggregation functionality in copy_files.py
    • aggregate_regions.py (Disaggregate and re-aggregate inputs with regional scope of legacy (134) zones #102 removed everything except for transmission aggregation from aggregate_regions.py; given the removal of transmission aggregation here, we no longer need the script)
    • Zone aggregation functionality in reeds.io.get_zonemap()
  • transmission.py has been reorganized to make it importable
  • min_co2_spurline_miles (= 20): moved from hard-coded in transmission.py to scalars.csv

Switches added/removed/changed

  • GSw_TranScen: Allowable options changed to none (default, meaning only interfaces with existing capacity can be expanded), NTP_MT, or NTP_P2P

Issues resolved

Validation, testing, and comparison report(s)

I ran the full cases_test.csv suite and everything worked except for WECC_county, which failed in 2015, the same year in which it fails on the main branch.

Here's some background info and plots of the new costs: 20260515 - transmission costs distances.pptx

Compare reports are available in the same deck.

  • There's more LCC and VSC in the default solution since we've added existing/planned lines that were originally left out
    • image
    • image
  • The big line from Chicago to Michigan now goes to Indiana since we only include directly connected AC interfaces
    • image
  • I made sure the WECC_county case runs through 2015

Checklist for author

Details to double-check

  • Make sure SunZia and TransWestExpress show up in the solution
  • Add the SunZia cost/distance to keep the WECC_county case no more broken than on main
  • Update documentation text and figures as laid out in Calculate greenfield transmission costs for all zone geometries directly #37
  • Decide on MISO default (rename from 'default' to 'miso') or ACSR by default for new conductors
  • Open issue on updating environment
  • Charge code provided to reviewers
  • Included comparison reports for appropriate test cases
  • Documentation updated if necessary
  • If input data added/modified:
    • Dollar year recorded and converted to 2004$ for GAMS
    • Units are specified
    • Preprocessing steps have been documented and committed to ReEDS_Input_Processing
    • New large data files handled with .h5 instead of .csv <- No, because we will add to the costs/distances as new zone geometries are added
  • Code formatting standardized
  • Reusable functions used where possible instead of copy/pasted code

General information to guide review

  • Zero impact on results of default case
  • 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, reeds/core/solve/solve.py)
  • No change to code organization
  • No change to package requirements (environment.yml or Project.toml)

Did you use LLM tools (chatbot or copilot) in the preparation of this PR? If so, describe how

No

…; only define co2_routes for r,rr pairs with nonzero distance
…ap() (no longer need to modify zone map in aggregate_regions.py)
Network reinforcement costs are approximated by tracing a path along existing transmission lines from each wind/solar POI to each zone "center" within the same state;
the zone center is usually taken as the largest population center in the model zone but is sometimes (for zones without large urban centers) assigned to a high-voltage substation within the zone.[^ref35]
A cost for each reinforcement route is calculated using the cost surface described above, with capital expenditure (CAPEX) costs multiplied by 50% to approximate the lower cost for reconductoring compared to greenfield transmission construction.
Network reinforcement costs are approximated by tracing a path along existing transmission lines from each wind/solar POI to a nearby urban center.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you quantify "nearby" and "urban center"? I can't remember the specifics, but this seems like a good place to document those.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some lines to describe the current approach.

@@ -0,0 +1,9 @@
voltage_kv,kcmil,conductor_type,conductor_quantity,amp
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is kcmil?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a silly U.S. unit but it's the standard measurement of wire gauge: https://en.wikipedia.org/wiki/Circular_mil

Comment thread inputs/transmission/hvdc_lines.csv Outdated
Cross Sound Cable,330,-72.90222222,41.28666667,-72.8675,40.95916667
Neptune Cable,660,-73.55111111,40.76055556,-74.35308333,40.47371667
Trans Bay Cable,400,-121.89666667,38.03083333,-122.38583333,37.75472222
name,MW,year_online,trtype,certain,from_lon,from_lat,to_lon,to_lat
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is year_online the number of years until it is online? If so, it might be worth updating the name, as I was expecting something like "2028" for the value rather than "0".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a description to inputs/transmission/README.md and changed these to their actual historical online year

@@ -0,0 +1,3 @@
name,MW,year_online,trtype,certain,from_lon,from_lat,to_lon,to_lat
TransWestExpress,3000,2032,VSC,1,-107.176147,41.747242,-112.590781,39.541825
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one has year_online as what I would expect, so I think I'm not understanding this column correcting, or what a zero value for this column means.

Comment thread inputs/transmission/README.md
Comment thread inputs/scalars.csv
Comment thread reeds/input_processing/copy_files.py
Comment thread reeds/input_processing/transmission.py Outdated
Comment on lines +339 to +348
### TEMPORARY 20260402: Drop county interfaces with no distance/cost
if (level == 'r') and (sw.GSw_RegionResolution in ['county', 'mixed']):
transmission_line_fom = get_transmission_fom(case, interface_params)
indices = ['r', 'rr', 'trtype']
drop = (
dfout
.merge(transmission_line_fom.reset_index(), on=indices, how='left')
)
drop = list(drop.loc[drop.USDperMWyear.isnull(), indices].itertuples(index=False))
dfout = dfout.set_index(indices).drop(drop).reset_index()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what is making WECC county runs fail (mentioned in your PR text)? Or does this make it not fail?

Yunzhi is nearly done with the WECC county bug fix, so it would be nice to not have it immediately broken again if that can be avoided, even if that meant a temporary patched solution until new reV data is available.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's a bit different; this is to keep GSw_ZoneSet = z3109 (county-level, which includes counties with no transmission links to neighbors) working until we finish reformatting the spatial pipeline, which will allow use of GSw_ZoneSet = z2972 (which aggregates those counties into their neighbors and avoids making islanded zones).

The missing SunZia line at county resolution is caught in check_inputs.py. But I'll add it before merging.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SunZia cost/distance has been added, so the WECC_county case now runs through 2015, the same as on main.

Comment thread runreeds.py Outdated
@patrickbrown4 patrickbrown4 requested a review from wesleyjcole June 1, 2026 20:37
Copy link
Copy Markdown
Contributor

@wesleyjcole wesleyjcole left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updated explanations and documentation.

Copy link
Copy Markdown
Contributor

@kodiobika kodiobika left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this! LGTM

Comment thread reeds/inputs.py Outdated
Comment thread reeds/inputs.py Outdated
Comment thread reeds/inputs.py


def get_hvdc_lines():
def get_hvdc_lines(filename='hvdc_existing.csv'):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action required here, but this got me wondering - is there a well-defined difference between reeds/inputs.py and reeds/io.py? My instinct would've been to put something like this in the latter but I'm not actually sure which, if any, makes more sense in this case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My very rough notion has been to use inputs.py for things that are "further up" in the pipeline (closer to the direct inputs/ data) or that require more processing, and io.py for interacting with intermediate files from the {case}/inputs_case folder. I'm sure I haven't been super consistent though (inputs.py is newer and I didn't port everything to it from io.py that would fit those guidelines.) I was also worried about io.py becoming too huge if we use it as a catch-all for everything, but I'm not sure how important that is.

Maybe we could do a standalone cleanup/organizational PR after we're done with all the spatial stuff? We'll need some more layers of organization anyway as we start moving b_inputs.gms into Python.

Comment thread reeds/io.py


def get_dfmap(case=None, levels=None, exclude_water_areas=False):
def get_dfmap(case=None, levels=None, exclude_water_areas=True):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking - the fact that all the instances throughout the code where get_dfmap is currently being used and exclude_water_areas isn't specified will now exclude water areas (where they were previously including them) is intentional right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_dfmap() until now has used the 134-zone shapefile for everything except county zones, and that shapefile excludes water areas. So I think excluding them by default is most consistent, but I didn't actually go through and check all the areas where water areas might matter.

Do you remember offhand the places where including water areas is intended and important?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah got it, thanks that SGTM. No instances come to mind (and from a cursory scan it looks like there are none) so I think we're good

Comment thread reeds/input_processing/copy_files.py Outdated
Comment thread reeds/input_processing/copy_files.py Outdated
@@ -0,0 +1,9313 @@
start,end,polarity,voltage,cost_MUSD,length_miles
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It'd be nice for consistency to specify the voltage units too, but no need if it's too much of a hassle at this point

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah fair; I kept these because they're the column names for inputs to and outputs from the reV Routing (reVRt) model. I've added notes on the units to inputs/transmission/README.md; think that's enough for now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, yup that SGTM

@@ -0,0 +1,499 @@
name,from_lon,from_lat,to_lon,to_lat,polarity,voltage,cost_MUSD,length_miles
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Ditto on voltage units ('voltage' -> 'voltage_kv')

interface_params['geometry'] = interface_params.apply(_make_line, axis=1)
interface_params = gpd.GeoDataFrame(interface_params, crs='EPSG:4326')
interface_params['straight_miles'] = (
interface_params.geometry.to_crs('EPSG:5070').length
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why 'EPSG:5070' here instead of the usual 'ESRI:102008'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're both equal-area projections, but EPSG:5070 is zoomed in more on the contiguous US, while ESRI:102008 is for all of North America (there are examples of the two at https://gis.stackexchange.com/a/457667). That means that within the CONUS, there's a bit less distortion in distances and areas using EPSG:5070 than ESRI:102008. (Eventually it'd be nice to change everything to EPSG:5070 and make it controlled by a global switch.)

Comment thread reeds/input_processing/transmission.py Outdated
@patrickbrown4 patrickbrown4 merged commit 62e6948 into main Jun 5, 2026
10 checks passed
@patrickbrown4 patrickbrown4 deleted the pb/transcost branch June 5, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants