From 3d30e0e4c314c54af5391aa277a092e1af3e4695 Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 12:03:58 -0800 Subject: [PATCH 1/9] refactor: add storage defaults to usa_tamu constants --- powersimdata/network/usa_tamu/constants/storage.py | 11 +++++++++++ powersimdata/network/usa_tamu/usa_tamu_model.py | 12 ++---------- 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 powersimdata/network/usa_tamu/constants/storage.py diff --git a/powersimdata/network/usa_tamu/constants/storage.py b/powersimdata/network/usa_tamu/constants/storage.py new file mode 100644 index 000000000..a680a7ae0 --- /dev/null +++ b/powersimdata/network/usa_tamu/constants/storage.py @@ -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, +} diff --git a/powersimdata/network/usa_tamu/usa_tamu_model.py b/powersimdata/network/usa_tamu/usa_tamu_model.py index 70d586a8a..d15000995 100644 --- a/powersimdata/network/usa_tamu/usa_tamu_model.py +++ b/powersimdata/network/usa_tamu/usa_tamu_model.py @@ -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, @@ -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) @@ -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) From a37d8be99a52a1ae9e243648e5c060a2fe1f229d Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 12:04:45 -0800 Subject: [PATCH 2/9] chore: remove unnecessary __all__ in __init__ --- powersimdata/network/usa_tamu/constants/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/powersimdata/network/usa_tamu/constants/__init__.py b/powersimdata/network/usa_tamu/constants/__init__.py index 8e8e023f4..e69de29bb 100644 --- a/powersimdata/network/usa_tamu/constants/__init__.py +++ b/powersimdata/network/usa_tamu/constants/__init__.py @@ -1 +0,0 @@ -__all__ = ["plants", "zones"] From 041b1ad33b42cc5c5eb39cbd5658b0cd437f6bff Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 12:05:33 -0800 Subject: [PATCH 3/9] chore: update abstract_grid with new storage parameters --- powersimdata/input/abstract_grid.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/powersimdata/input/abstract_grid.py b/powersimdata/input/abstract_grid.py index 5c1bb57fa..76197ac5b 100644 --- a/powersimdata/input/abstract_grid.py +++ b/powersimdata/input/abstract_grid.py @@ -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 From 8430d3d1e44817d8e73528d5d262a84d516078ab Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 12:07:15 -0800 Subject: [PATCH 4/9] refactor: populate ignored storage subkeys automatically in grid equality --- powersimdata/input/grid.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/powersimdata/input/grid.py b/powersimdata/input/grid.py index 1bf4a5d91..fe41b7570 100644 --- a/powersimdata/input/grid.py +++ b/powersimdata/input/grid.py @@ -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 @@ -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] From f3252e0db1302bb6b0231c75af6ede1208bd8d4a Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 12:09:03 -0800 Subject: [PATCH 5/9] feat: add more new storage parameter specifications to ChangeTable --- powersimdata/input/change_table.py | 67 +++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/powersimdata/input/change_table.py b/powersimdata/input/change_table.py index 5a70cb004..db1f48322 100644 --- a/powersimdata/input/change_table.py +++ b/powersimdata/input/change_table.py @@ -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). From 90d0a431c8a4074b7f9e35bb6369a97e517c0b1c Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 12:09:32 -0800 Subject: [PATCH 6/9] feat: interpret new storage change table parameters in TransformGrid --- powersimdata/input/transform_grid.py | 67 +++++++++++++++------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/powersimdata/input/transform_grid.py b/powersimdata/input/transform_grid.py index 3719cb6b1..2f725efcc 100644 --- a/powersimdata/input/transform_grid.py +++ b/powersimdata/input/transform_grid.py @@ -356,29 +356,29 @@ 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.shape[0] + 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 + 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 + gen["Pmax"] = entry["capacity"] + gen["Pmin"] = -1 * entry["capacity"] + gen["ramp_10"] = entry["capacity"] + gen["ramp_30"] = entry["capacity"] self.grid.storage["gen"] = self.grid.storage["gen"].append( gen, ignore_index=True, sort=False ) @@ -396,35 +396,38 @@ 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} - 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( + self.grid.storage["StorageData"] = self.grid.storage["StorageData"].append( data, ignore_index=True, sort=False ) From 48a94d7206c2d3f802df8fd7f36e00bc21a4df7d Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 12:30:56 -0800 Subject: [PATCH 7/9] test: update tests for new change table storage format --- powersimdata/input/tests/test_change_table.py | 4 ++-- powersimdata/input/tests/test_transform_grid.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/powersimdata/input/tests/test_change_table.py b/powersimdata/input/tests/test_change_table.py index 0ba5deeaa..777cff772 100644 --- a/powersimdata/input/tests/test_change_table.py +++ b/powersimdata/input/tests/test_change_table.py @@ -422,7 +422,7 @@ 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}, @@ -430,7 +430,7 @@ def test_add_new_elements_at_new_buses(ct): 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}]) diff --git a/powersimdata/input/tests/test_transform_grid.py b/powersimdata/input/tests/test_transform_grid.py index 3f557cf57..eb7d7a33b 100644 --- a/powersimdata/input/tests/test_transform_grid.py +++ b/powersimdata/input/tests/test_transform_grid.py @@ -489,7 +489,11 @@ 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() @@ -497,8 +501,8 @@ def test_add_storage(ct): 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): From 8ab8e847f69011dd36b5cc285126172c9c49f748 Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 20:03:06 -0800 Subject: [PATCH 8/9] fix: use max id for storage id, not plant shape --- powersimdata/input/transform_grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powersimdata/input/transform_grid.py b/powersimdata/input/transform_grid.py index 2f725efcc..681d89c74 100644 --- a/powersimdata/input/transform_grid.py +++ b/powersimdata/input/transform_grid.py @@ -356,7 +356,7 @@ def _add_gencost(self): def _add_storage(self): """Adds storage to the grid.""" - first_storage_id = self.grid.plant.shape[0] + 1 + 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) From fd7596c777af7b97dc95aff74129443740b401dc Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Thu, 28 Jan 2021 20:18:43 -0800 Subject: [PATCH 9/9] fix: ensure that storage table int columns remain int --- powersimdata/input/transform_grid.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/powersimdata/input/transform_grid.py b/powersimdata/input/transform_grid.py index 681d89c74..5a4dfbe14 100644 --- a/powersimdata/input/transform_grid.py +++ b/powersimdata/input/transform_grid.py @@ -370,7 +370,8 @@ def _add_storage_unit(self, entry): :param int bus_id: bus identification number. :param dict entry: storage details, containing at least "bus_id" and "capacity". """ - gen = {g: 0 for g in self.grid.storage["gen"].columns} + 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 @@ -379,9 +380,9 @@ def _add_storage_unit(self, entry): gen["Pmin"] = -1 * entry["capacity"] gen["ramp_10"] = entry["capacity"] gen["ramp_30"] = entry["capacity"] - self.grid.storage["gen"] = self.grid.storage["gen"].append( - gen, ignore_index=True, sort=False - ) + 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.""" @@ -403,7 +404,8 @@ def _add_storage_data(self, storage_id, entry): :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} capacity = entry["capacity"] duration = entry["duration"] @@ -427,9 +429,11 @@ def _add_storage_data(self, storage_id, entry): data["InEff"] = entry["InEff"] data["LossFactor"] = entry["LossFactor"] data["rho"] = 1 - self.grid.storage["StorageData"] = self.grid.storage["StorageData"].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):