Skip to content

Commit

Permalink
Merge pull request #383 from Breakthrough-Energy/daniel/storage_params
Browse files Browse the repository at this point in the history
feat: let user specify more parameters for storage
  • Loading branch information
danielolsen authored Feb 3, 2021
2 parents b8d1445 + fd7596c commit 5682255
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 82 deletions.
3 changes: 3 additions & 0 deletions powersimdata/input/abstract_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ def storage_template():
"max_stor": None, # ratio
"InEff": None,
"OutEff": None,
"LossFactor": None, # stored energy fraction / hour
"energy_price": None, # $/MWh
"terminal_min": None,
"terminal_max": None,
}
return storage
67 changes: 47 additions & 20 deletions powersimdata/input/change_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,31 +462,58 @@ def scale_congested_mesh_branches(self, ref_scenario, **kwargs):
"""
scale_congested_mesh_branches(self, ref_scenario, **kwargs)

def add_storage_capacity(self, bus_id):
def add_storage_capacity(self, info):
"""Sets storage parameters in change table.
:param dict bus_id: key(s) for the id of bus(es), value(s) is (are)
capacity of the energy storage system in MW.
:raises TypeError: if bus_id is not a dict.
:raises ValueError: if bus_id contains any bus ids not present in the grid,
or any non-positive values are given.
:param list info: each entry is a dictionary. The dictionary gathers
the information needed to create a new storage device.
:raises TypeError: if info is not a list.
:raises ValueError: if any of the new storages to be added have bad values.
"""
if not isinstance(bus_id, dict):
raise TypeError("bus_id must be a dict")
if not isinstance(info, list):
raise TypeError("Argument enclosing new storage(s) must be a list")

info = copy.deepcopy(info)
new_storages = []
required = {"bus_id", "capacity"}
optional = {
"duration",
"min_stor",
"max_stor",
"energy_value",
"InEff",
"OutEff",
"LossFactor",
"terminal_min",
"terminal_max",
}
anticipated_bus = self._get_new_bus()
diff = set(bus_id.keys()).difference(set(anticipated_bus.index))
if len(diff) != 0:
raise ValueError(f"No bus with the following id: {', '.join(diff)}")
for k, v in bus_id.items():
if not isinstance(v, (float, int)):
raise ValueError(f"values must be numeric, bad type for bus {k}")
if v <= 0:
raise ValueError(f"values must be positive, bad value for bus {k}")
for i, storage in enumerate(info):
self._check_entry_keys(storage, i, "storage", required, None, optional)
if storage["bus_id"] not in anticipated_bus.index:
raise ValueError(
f"No bus id {storage['bus_id']} available for {ordinal(i)} storage"
)
for o in optional:
if o not in storage:
storage[o] = self.grid.storage[o]
for k, v in storage.items():
if not isinstance(v, (int, float)):
err_msg = f"values must be numeric, bad type for {ordinal(i)} {k}"
raise ValueError(err_msg)
if v < 0:
raise ValueError(
f"values must be non-negative, bad value for {ordinal(i)} {k}"
)
for k in {"min_stor", "max_stor", "InEff", "OutEff", "LossFactor"}:
if storage[k] > 1:
raise ValueError(
f"value for {k} must be <=1, bad value for {ordinal(i)} storage"
)
new_storages.append(storage)
if "storage" not in self.ct:
self.ct["storage"] = {}
if "bus_id" not in self.ct["storage"]:
self.ct["storage"]["bus_id"] = {}
self.ct["storage"]["bus_id"].update(bus_id)
self.ct["storage"] = []
self.ct["storage"] += new_storages

def add_dcline(self, info):
"""Adds HVDC line(s).
Expand Down
11 changes: 2 additions & 9 deletions powersimdata/input/grid.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from powersimdata.input.scenario_grid import FromREISE, FromREISEjl
from powersimdata.network.usa_tamu.constants import storage as tamu_storage
from powersimdata.network.usa_tamu.usa_tamu_model import TAMU
from powersimdata.utility.helpers import MemoryCache, cache_key

Expand Down Expand Up @@ -102,15 +103,7 @@ def _univ_eq(ref, test, failure_flag=None):
# compare storage
_univ_eq(len(self.storage["gen"]), len(other.storage["gen"]), "storage")
_univ_eq(self.storage.keys(), other.storage.keys(), "storage")
ignored_subkeys = {
"duration",
"min_stor",
"max_stor",
"InEff",
"OutEff",
"energy_price",
"gencost",
}
ignored_subkeys = {"gencost"} | set(tamu_storage.defaults.keys())
for subkey in set(self.storage.keys()) - ignored_subkeys:
# REISE will modify some gen columns
self_data = self.storage[subkey]
Expand Down
4 changes: 2 additions & 2 deletions powersimdata/input/tests/test_change_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,15 +422,15 @@ def test_add_bus_bad_type(ct):


def test_add_new_elements_at_new_buses(ct):
max_existing_index = grid.bus.index.max()
max_existing_index = int(grid.bus.index.max())
new_buses = [
{"lat": 40, "lon": 50.5, "zone_id": 2, "baseKV": 69},
{"lat": -40.5, "lon": -50, "zone_name": "Massachusetts", "Pd": 10},
]
ct.add_bus(new_buses)
new_bus1 = max_existing_index + 1
new_bus2 = max_existing_index + 2
ct.add_storage_capacity(bus_id={new_bus1: 100})
ct.add_storage_capacity([{"bus_id": new_bus1, "capacity": 100}])
ct.add_dcline([{"from_bus_id": new_bus1, "to_bus_id": new_bus2, "capacity": 200}])
ct.add_branch([{"from_bus_id": new_bus1, "to_bus_id": new_bus2, "capacity": 300}])
ct.add_plant([{"type": "wind", "bus_id": new_bus2, "Pmax": 400}])
Expand Down
10 changes: 7 additions & 3 deletions powersimdata/input/tests/test_transform_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,16 +489,20 @@ def test_add_gen_add_entries_in_gencost_data_frame(ct):


def test_add_storage(ct):
storage = {2021005: 116.0, 2028827: 82.5, 2028060: 82.5}
storage = [
{"bus_id": 2021005, "capacity": 116.0},
{"bus_id": 2028827, "capacity": 82.5},
{"bus_id": 2028060, "capacity": 82.5},
]
ct.add_storage_capacity(storage)
new_grid = TransformGrid(grid, ct.ct).get_grid()

pmin = new_grid.storage["gen"].Pmin.values
pmax = new_grid.storage["gen"].Pmax.values

assert new_grid.storage["gen"].shape[0] != grid.storage["gen"].shape[0]
assert np.array_equal(pmin, -1 * np.array(list(storage.values())))
assert np.array_equal(pmax, np.array(list(storage.values())))
assert np.array_equal(pmin, -1 * np.array([d["capacity"] for d in storage]))
assert np.array_equal(pmax, np.array([d["capacity"] for d in storage]))


def test_add_bus(ct):
Expand Down
81 changes: 44 additions & 37 deletions powersimdata/input/transform_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,32 +356,33 @@ def _add_gencost(self):

def _add_storage(self):
"""Adds storage to the grid."""
storage_id = self.grid.plant.shape[0]
for bus_id, value in self.ct["storage"]["bus_id"].items():
storage_id += 1
self._add_storage_unit(bus_id, value)
first_storage_id = self.grid.plant.index.max() + 1
for i, entry in enumerate(self.ct["storage"]):
storage_id = first_storage_id + i
self._add_storage_unit(entry)
self._add_storage_gencost()
self._add_storage_genfuel()
self._add_storage_data(storage_id, value)
self._add_storage_data(storage_id, entry)

def _add_storage_unit(self, bus_id, value):
def _add_storage_unit(self, entry):
"""Add storage unit.
:param int bus_id: bus identification number.
:param float value: storage capacity.
:param dict entry: storage details, containing at least "bus_id" and "capacity".
"""
gen = {g: 0 for g in self.grid.storage["gen"].columns}
gen["bus_id"] = bus_id
storage = self.grid.storage
gen = {g: 0 for g in storage["gen"].columns}
gen["bus_id"] = entry["bus_id"]
gen["Vg"] = 1
gen["mBase"] = 100
gen["status"] = 1
gen["Pmax"] = value
gen["Pmin"] = -1 * value
gen["ramp_10"] = value
gen["ramp_30"] = value
self.grid.storage["gen"] = self.grid.storage["gen"].append(
gen, ignore_index=True, sort=False
)
gen["Pmax"] = entry["capacity"]
gen["Pmin"] = -1 * entry["capacity"]
gen["ramp_10"] = entry["capacity"]
gen["ramp_30"] = entry["capacity"]
storage["gen"] = storage["gen"].append(gen, ignore_index=True, sort=False)
# Maintain int columns after the append converts them to float
storage["gen"] = storage["gen"].astype({"bus_id": "int", "status": "int"})

def _add_storage_gencost(self):
"""Sets generation cost of storage unit."""
Expand All @@ -396,37 +397,43 @@ def _add_storage_genfuel(self):
"""Sets fuel type of storage unit."""
self.grid.storage["genfuel"].append("ess")

def _add_storage_data(self, storage_id, value):
def _add_storage_data(self, storage_id, entry):
"""Sets storage data.
:param int storage_id: storage identification number.
:param float value: storage capacity.
:param dict entry: storage details, containing at least:
"bus_id", "capacity".
"""
data = {g: 0 for g in self.grid.storage["StorageData"].columns}
storage = self.grid.storage
data = {g: 0 for g in storage["StorageData"].columns}

duration = self.grid.storage["duration"]
min_stor = self.grid.storage["min_stor"]
max_stor = self.grid.storage["max_stor"]
energy_price = self.grid.storage["energy_price"]
capacity = entry["capacity"]
duration = entry["duration"]
min_stor = entry["min_stor"]
max_stor = entry["max_stor"]
energy_value = entry["energy_value"]
terminal_min = entry["terminal_min"]
terminal_max = entry["terminal_max"]

data["UnitIdx"] = storage_id
data["ExpectedTerminalStorageMax"] = value * duration * max_stor
data["ExpectedTerminalStorageMin"] = value * duration / 2
data["InitialStorage"] = value * duration / 2
data["InitialStorageLowerBound"] = value * duration / 2
data["InitialStorageUpperBound"] = value * duration / 2
data["InitialStorageCost"] = energy_price
data["TerminalStoragePrice"] = energy_price
data["MinStorageLevel"] = value * duration * min_stor
data["MaxStorageLevel"] = value * duration * max_stor
data["OutEff"] = self.grid.storage["OutEff"]
data["InEff"] = self.grid.storage["InEff"]
data["LossFactor"] = 0
data["ExpectedTerminalStorageMax"] = capacity * duration * terminal_max
data["ExpectedTerminalStorageMin"] = capacity * duration * terminal_min
data["InitialStorage"] = capacity * duration / 2 # Start with half
data["InitialStorageLowerBound"] = capacity * duration / 2 # Start with half
data["InitialStorageUpperBound"] = capacity * duration / 2 # Start with half
data["InitialStorageCost"] = energy_value
data["TerminalStoragePrice"] = energy_value
data["MinStorageLevel"] = capacity * duration * min_stor
data["MaxStorageLevel"] = capacity * duration * max_stor
data["OutEff"] = entry["OutEff"]
data["InEff"] = entry["InEff"]
data["LossFactor"] = entry["LossFactor"]
data["rho"] = 1
prev_storage_data = self.grid.storage["StorageData"]
self.grid.storage["StorageData"] = prev_storage_data.append(
storage["StorageData"] = storage["StorageData"].append(
data, ignore_index=True, sort=False
)
# Maintain int columns after the append converts them to float
storage["StorageData"] = storage["StorageData"].astype({"UnitIdx": "int"})


def voltage_to_x_per_distance(grid):
Expand Down
1 change: 0 additions & 1 deletion powersimdata/network/usa_tamu/constants/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
__all__ = ["plants", "zones"]
11 changes: 11 additions & 0 deletions powersimdata/network/usa_tamu/constants/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defaults = {
"duration": 4,
"min_stor": 0.05,
"max_stor": 0.95,
"InEff": 0.9,
"OutEff": 0.9,
"energy_value": 20,
"LossFactor": 0,
"terminal_min": 0,
"terminal_max": 1,
}
12 changes: 2 additions & 10 deletions powersimdata/network/usa_tamu/usa_tamu_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
csv_to_data_frame,
)
from powersimdata.network.csv_reader import CSVReader
from powersimdata.network.usa_tamu.constants.storage import defaults
from powersimdata.network.usa_tamu.constants.zones import (
abv2state,
interconnect2loadzone,
Expand Down Expand Up @@ -41,15 +42,6 @@ def _set_data_loc(self):
else:
self.data_loc = data_loc

def _set_storage(self):
"""Sets storage properties."""
self.storage["duration"] = 4
self.storage["min_stor"] = 0.05
self.storage["max_stor"] = 0.95
self.storage["InEff"] = 0.9
self.storage["OutEff"] = 0.9
self.storage["energy_price"] = 20

def _build_network(self):
"""Build network."""
reader = CSVReader(self.data_loc)
Expand All @@ -59,7 +51,7 @@ def _build_network(self):
self.dcline = reader.dcline
self.gencost["after"] = self.gencost["before"] = reader.gencost

self._set_storage()
self.storage.update(defaults)

add_information_to_model(self)

Expand Down

0 comments on commit 5682255

Please sign in to comment.