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 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). 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] 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): diff --git a/powersimdata/input/transform_grid.py b/powersimdata/input/transform_grid.py index 3719cb6b1..5a4dfbe14 100644 --- a/powersimdata/input/transform_grid.py +++ b/powersimdata/input/transform_grid.py @@ -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.""" @@ -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): 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"] 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)