Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ determines Scenario-wide how much of the new capacity should be solar
Basic independent call, using the demand from the reference scenario to
approximate the future demand:
```python
from powersimdata.design.clean_capacity_scaling import calculate_clean_capacity_scaling
from powersimdata.design.generation.clean_capacity_scaling import calculate_clean_capacity_scaling
from powersimdata.scenario.scenario import Scenario

ref_scenario = Scenario('403')
Expand All @@ -405,7 +405,7 @@ targets_and_new_capacities_df = calculate_clean_capacity_scaling(

Complex collaborative call, using all optional parameters:
```python
from powersimdata.design.clean_capacity_scaling import calculate_clean_capacity_scaling
from powersimdata.design.generation.clean_capacity_scaling import calculate_clean_capacity_scaling
from powersimdata.scenario.scenario import Scenario

ref_scenario = Scenario('403')
Expand Down Expand Up @@ -440,7 +440,7 @@ the exception of solar and wind generators, which will be scaled up to meet clea
energy goals.

```python
from powersimdata.design.clean_capacity_scaling import create_change_table
from powersimdata.design.generation.clean_capacity_scaling import create_change_table

change_table = create_change_table(targets_and_new_capacities_df, ref_scenario)
# The change table method only accepts zone names, not zone IDs, so we have to translate
Expand All @@ -457,6 +457,28 @@ for resource in change_table:
)
```

## 4. Analyzing Scenario Designs

### A. Analysis of Transmission Upgrades

#### I. Cumulative Upgrade Quantity
Using the change table of a scenario, the number of upgrades lines/transformers
and their cumulative upgraded capacity (for transformers) and cumulative
upgraded megawatt-miles (for lines) can be calculated with:
```
powersimdata.design.transmission.mwmiles.calculate_mw_miles(scenario)
```
where `scenario` is a Scenario instance.

#### II. Classify Upgrades
The upgraded branches can also be classified into either interstate or
intrastate branches by calling:

```
powersimdata.design.transmission.statelines.classify_interstate_intrastate(scenario)
```
where `scenario` is a Scenario instance.

[PreREISE]: https://github.com/Breakthrough-Energy/PreREISE
[PostREISE]: https://github.com/Breakthrough-Energy/PostREISE
[zenodo]: https://zenodo.org/record/3530898
8 changes: 7 additions & 1 deletion powersimdata/design/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
__all__ = ["tests"]
__all__ = [
"generation",
"mimic_grid",
"scenario_info",
"tests",
"transmission",
]
1 change: 1 addition & 0 deletions powersimdata/design/generation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["clean_capacity_scaling"]
1 change: 1 addition & 0 deletions powersimdata/design/generation/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["test_clean_capacity_scaling"]
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pandas.testing import assert_frame_equal, assert_series_equal

from powersimdata.tests.mock_scenario import MockScenario
from powersimdata.design.clean_capacity_scaling import (
from powersimdata.design.generation.clean_capacity_scaling import (
add_resource_data_to_targets,
add_demand_to_targets,
add_shortfall_to_targets,
Expand Down
1 change: 1 addition & 0 deletions powersimdata/design/transmission/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["mwmiles", "statelines", "tests", "upgrade"]
80 changes: 80 additions & 0 deletions powersimdata/design/transmission/mwmiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from powersimdata.utility.distance import haversine
from powersimdata.input.grid import Grid


def calculate_mw_miles(scenario, exclude_branches=None):
"""Given a Scenario object, calculate the number of upgraded lines and
transformers, and the total upgrade quantity (in MW and MW-miles).
Currently only supports change tables that specify branches' id, not
zone name. Currently lumps Transformer and TransformerWinding upgrades
together.

:param powersimdata.scenario.scenario.Scenario scenario: scenario instance.
:param list/tuple/set/None exclude_branches: branches to exclude.
:return: (*dict*) -- Upgrades to the branches.
"""

original_grid = Grid(scenario.info["interconnect"].split("_"))
ct = scenario.state.get_ct()
upgrades = _calculate_mw_miles(original_grid, ct, exclude_branches)
return upgrades


def _calculate_mw_miles(original_grid, ct, exclude_branches=None):
"""Given a base grid and a change table, calculate the number of upgraded
lines and transformers, and the total upgrade quantity (in MW & MW-miles).
This function is separate from calculate_mw_miles() for testing purposes.
Currently only supports change_tables that specify branches, not zone_name.
Currently lumps Transformer and TransformerWinding upgrades together.

:param powersimdata.input.grid.Grid original_grid: grid instance.
:param dict ct: change table instance.
:param list/tuple/set/None exclude_branches: branches to exclude.
:raises ValueError: if not all values in exclude_branches are in the grid.
:raises TypeError: if exclude_branches gets the wrong type.
:return: (*dict*) -- Upgrades to the branches.
"""

upgrade_categories = ("mw_miles", "transformer_mw", "num_lines", "num_transformers")
upgrades = {u: 0 for u in upgrade_categories}

if "branch" not in ct or "branch_id" not in ct["branch"]:
return upgrades

if exclude_branches is None:
exclude_branches = {}
elif isinstance(exclude_branches, (list, set, tuple)):
good_branch_indices = original_grid.branch.index
if not all([e in good_branch_indices for e in exclude_branches]):
raise ValueError("not all branches are present in grid!")
exclude_branches = set(exclude_branches)
else:
raise TypeError("exclude_branches must be None, list, tuple, or set")

base_branch = original_grid.branch
upgraded_branches = ct["branch"]["branch_id"]
for b, v in upgraded_branches.items():
if b in exclude_branches:
continue
# 'upgraded' capacity is v-1 because a scale of 1 = an upgrade of 0
upgraded_capacity = base_branch.loc[b, "rateA"] * (v - 1)
device_type = base_branch.loc[b, "branch_device_type"]
if device_type == "Line":
from_coords = (
base_branch.loc[b, "from_lat"],
base_branch.loc[b, "from_lon"],
)
to_coords = (base_branch.loc[b, "to_lat"], base_branch.loc[b, "to_lon"])
addtl_mw_miles = upgraded_capacity * haversine(from_coords, to_coords)
upgrades["mw_miles"] += addtl_mw_miles
upgrades["num_lines"] += 1
elif device_type == "Transformer":
upgrades["transformer_mw"] += upgraded_capacity
upgrades["num_transformers"] += 1
elif device_type == "TransformerWinding":
upgrades["transformer_mw"] += upgraded_capacity
upgrades["num_transformers"] += 1
else:
raise Exception("Unknown branch: " + str(b))

return upgrades
56 changes: 56 additions & 0 deletions powersimdata/design/transmission/statelines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from powersimdata.network.usa_tamu.constants.zones import id2state

Comment thread
danielolsen marked this conversation as resolved.

def classify_interstate_intrastate(scenario):
"""Classifies branches in a change_table as interstate or intrastate.

:param powersimdata.scenario.scenario.Scenario scenario: scenario instance.
:return: (*dict*) -- keys are *'interstate'*, *'intrastate'*. Values are
list of branch ids.
"""

ct = scenario.state.get_ct()
grid = scenario.state.get_grid()
upgraded_branches = _classify_interstate_intrastate(ct, grid)
return upgraded_branches


def _classify_interstate_intrastate(ct, grid):
"""Classifies branches in a change_table as interstate or intrastate.
This function is separate from classify_interstate_intrastate() for testing
purposes.

:param dict ct: change_table dictionary.
:param powersimdata.input.grid.Grid grid: Grid instance.
:return: (*dict*) -- keys are *'interstate'*, *'intrastate'*. Values are
list of branch ids.
"""

branch = grid.branch
upgraded_branches = {"interstate": [], "intrastate": []}

if "branch" not in ct or "branch_id" not in ct["branch"]:
return upgraded_branches

all_upgraded_branches = ct["branch"]["branch_id"].keys()
for b in all_upgraded_branches:
# Alternatively: bus.loc[branch.loc[b, 'from_bus_id'], 'from_zone_id']
try:
from_zone = branch.loc[b, "from_zone_id"]
to_zone = branch.loc[b, "to_zone_id"]
except KeyError:
raise ValueError(f"ct entry not found in branch: {b}")
try:
from_state = id2state[from_zone]
except KeyError:
raise ValueError(f"zone not found in id2state: {from_zone}")
try:
to_state = id2state[to_zone]
except KeyError:
raise ValueError(f"zone not found in id2state: {to_zone}")
if from_state == to_state:
upgraded_branches["intrastate"].append(b)
else:
upgraded_branches["interstate"].append(b)

return upgraded_branches
1 change: 1 addition & 0 deletions powersimdata/design/transmission/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["test_mwmiles", "test_statelines", "test_upgrade"]
105 changes: 105 additions & 0 deletions powersimdata/design/transmission/tests/test_mwmiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import unittest

from powersimdata.tests.mock_grid import MockGrid
from powersimdata.design.transmission.mwmiles import _calculate_mw_miles

# branch 11 from Seattle to San Francisco (~679 miles)
# branch 12 from Seattle to Spokane (~229 miles)
# branch 13-15 are transformers (0 miles)
mock_branch = {
"branch_id": [11, 12, 13, 14, 15],
"rateA": [10, 20, 30, 40, 50],
"from_lat": [47.61, 47.61, 47.61, 47.61, 47.61],
"from_lon": [-122.33, -122.33, -122.33, -122.33, -122.33],
"to_lat": [37.78, 47.66, 47.61, 47.61, 47.61],
"to_lon": [-122.42, -117.43, -122.33, -122.33, -122.33],
"branch_device_type": 2 * ["Line"] + 3 * ["Transformer"],
}

expected_keys = {"mw_miles", "transformer_mw", "num_lines", "num_transformers"}


class TestCalculateMWMiles(unittest.TestCase):
def setUp(self):
self.grid = MockGrid(grid_attrs={"branch": mock_branch})

def _check_expected_values(self, mw_miles, expected_mw_miles):
"""Check for proper structure and that values match expected.

:param dict mw_miles: dict of upgrade metrics.
:param dict expected_mw_miles: dict of expected upgrade metrics.
"""
self.assertIsInstance(mw_miles, dict, "dict not returned")
self.assertEqual(
expected_keys, mw_miles.keys(), msg="Dict keys not as expected"
)
for v in mw_miles.values():
self.assertIsInstance(v, (float, int))
for k in expected_mw_miles.keys():
err_msg = "Did not get expected value for " + str(k)
self.assertAlmostEqual(mw_miles[k], expected_mw_miles[k], msg=err_msg)

def test_calculate_mw_miles_no_scale(self):
mock_ct = {"branch": {"branch_id": {}}}
expected_mw_miles = {k: 0 for k in expected_keys}
mw_miles = _calculate_mw_miles(self.grid, mock_ct)
self._check_expected_values(mw_miles, expected_mw_miles)

def test_calculate_mw_miles_one_line_scaled(self):
mock_ct = {"branch": {"branch_id": {11: 2}}}
expected_mw_miles = {
"mw_miles": 6792.03551523,
"transformer_mw": 0,
"num_lines": 1,
"num_transformers": 0,
}
mw_miles = _calculate_mw_miles(self.grid, mock_ct)
self._check_expected_values(mw_miles, expected_mw_miles)

def test_calculate_mw_miles_one_transformer_scaled(self):
mock_ct = {"branch": {"branch_id": {13: 2.5}}}
expected_mw_miles = {
"mw_miles": 0,
"transformer_mw": 45,
"num_lines": 0,
"num_transformers": 1,
}
mw_miles = _calculate_mw_miles(self.grid, mock_ct)
self._check_expected_values(mw_miles, expected_mw_miles)

def test_calculate_mw_miles_many_scaled(self):
mock_ct = {"branch": {"branch_id": {11: 2, 12: 3, 13: 1.5, 14: 1.2, 15: 3}}}
expected_mw_miles = {
"mw_miles": 15917.06341095,
"transformer_mw": 123,
"num_lines": 2,
"num_transformers": 3,
}
mw_miles = _calculate_mw_miles(self.grid, mock_ct)
self._check_expected_values(mw_miles, expected_mw_miles)

def test_calculate_mw_miles_many_scaled_one_branch_excluded(self):
mock_ct = {"branch": {"branch_id": {11: 2, 12: 3, 13: 1.5, 14: 1.2, 15: 3}}}
expected_mw_miles = {
"mw_miles": 9125.027895725,
"transformer_mw": 123,
"num_lines": 1,
"num_transformers": 3,
}
mw_miles = _calculate_mw_miles(self.grid, mock_ct, exclude_branches={11})
self._check_expected_values(mw_miles, expected_mw_miles)

def test_calculate_mw_miles_many_scaled_two_branches_excluded(self):
mock_ct = {"branch": {"branch_id": {11: 2, 12: 3, 13: 1.5, 14: 1.2, 15: 3}}}
expected_mw_miles = {
"mw_miles": 9125.027895725,
"transformer_mw": 108,
"num_lines": 1,
"num_transformers": 2,
}
mw_miles = _calculate_mw_miles(self.grid, mock_ct, exclude_branches=[11, 13])
self._check_expected_values(mw_miles, expected_mw_miles)


if __name__ == "__main__":
unittest.main()
Loading