diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py index b1a27629..a8beca2d 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_producer_mapper.py @@ -14,10 +14,17 @@ # along with this program. If not, see . """Module containing the Esdl to asset mapper class.""" +from typing import Optional + +import pandas as pd + from omotes_simulator_core.entities.assets.controller.asset_controller_abstract import ( AssetControllerAbstract, ) from omotes_simulator_core.entities.assets.controller.controller_producer import ControllerProducer +from omotes_simulator_core.entities.assets.controller.profile_interpolation import ( + ProfileInterpolator, +) from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract @@ -29,7 +36,9 @@ def to_esdl(self, entity: AssetControllerAbstract) -> EsdlAssetObject: """Map an Entity to a EsdlAsset.""" raise NotImplementedError("EsdlAssetControllerProducerMapper.to_esdl()") - def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerProducer: + def to_entity( + self, esdl_asset: EsdlAssetObject, timestep: Optional[int] = None + ) -> ControllerProducer: """Method to map an esdl asset to a producer entity class. :param EsdlAssetObject model: Object to be converted to an asset entity. @@ -41,6 +50,19 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerProducer: temperature_in = esdl_asset.get_temperature("In", "Return") temperature_out = esdl_asset.get_temperature("Out", "Supply") strategy_priority = esdl_asset.get_strategy_priority() + + if esdl_asset.has_constraint(): + profile = esdl_asset.get_constraint_max_profile() + self.profile_interpolator = ProfileInterpolator( + profile=profile, + sampling_method=esdl_asset.get_sampling_method(), + interpolation_method=esdl_asset.get_interpolation_method(), + timestep=timestep, + ) + resampled_profile = self.profile_interpolator.get_resampled_profile() + else: + resampled_profile = pd.DataFrame() + contr_producer = ControllerProducer( name=esdl_asset.esdl_asset.name, identifier=esdl_asset.esdl_asset.id, @@ -48,6 +70,7 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerProducer: temperature_out=temperature_out, power=power, marginal_costs=marginal_costs, + profile=resampled_profile, # Empty DataFrame is added if there is no profile. priority=strategy_priority, ) return contr_producer diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_network.py b/src/omotes_simulator_core/entities/assets/controller/controller_network.py index baa645b3..1011b11a 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_network.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_network.py @@ -115,24 +115,24 @@ def get_total_charge_storage(self) -> float: * self.factor_to_first_network ) - def get_total_supply(self) -> float: + def get_total_supply(self, time: datetime.datetime) -> float: """Method to get the total heat supply of the network. :return float: Total heat supply of all producers. """ return ( - float(sum([producer.power for producer in self.producers])) + float(sum([producer.get_max_power(time) for producer in self.producers])) * self.factor_to_first_network ) - def set_supply_to_max(self, priority: int = 0) -> dict: + def set_supply_to_max(self, time: datetime.datetime, priority: int = 0) -> dict: """Method to set the producers to the max power. :return dict: Dict with key= asset-id and value=setpoints for the producers. """ - return self.set_supply(factor=1, priority=priority) + return self.set_supply(time, factor=1, priority=priority) - def set_supply(self, factor: float = 1, priority: int = 0) -> dict: + def set_supply(self, time: datetime.datetime, factor: float = 1, priority: int = 0) -> dict: """Method to set the producers with the given priority to max power times the factor. :param float factor: Factor to multiply the max power with. @@ -147,7 +147,7 @@ def set_supply(self, factor: float = 1, priority: int = 0) -> dict: elif source.priority != priority: continue producers[source.id] = { - PROPERTY_HEAT_DEMAND: source.power * factor, + PROPERTY_HEAT_DEMAND: source.get_max_power(time) * factor, PROPERTY_TEMPERATURE_OUT: source.temperature_out, PROPERTY_TEMPERATURE_IN: source.temperature_in, PROPERTY_SET_PRESSURE: False, diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_producer.py b/src/omotes_simulator_core/entities/assets/controller/controller_producer.py index 573a7da8..4a8c2511 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_producer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_producer.py @@ -14,6 +14,10 @@ # along with this program. If not, see . """Module containing the classes for the controller.""" +import datetime + +import pandas as pd + from omotes_simulator_core.entities.assets.controller.asset_controller_abstract import ( AssetControllerAbstract, ) @@ -30,6 +34,7 @@ def __init__( temperature_out: float, power: float, marginal_costs: float, + profile: pd.DataFrame, priority: None | int = 1, ): """Constructor for the source. @@ -48,3 +53,20 @@ def __init__( self.power: float = power self.marginal_costs: float = marginal_costs self.priority: None | int = priority + self.profile: pd.DataFrame = profile.set_index("date") if not profile.empty else profile + + def get_max_power(self, time: datetime.datetime) -> float: + """Gets the maximum producer power at the given timestep. + + If there is a profile, it will look it up, otherwise it returns the + maximum power defined in the esdl parameter. + """ + if self.profile.empty: + max_power = self.power + else: + try: + max_power = float(self.profile.loc[time, "values"]) + except KeyError: + max_power = self.power + + return max_power diff --git a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py index 13ea7fea..c6e0f57e 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -90,7 +90,7 @@ def get_property(self, esdl_property_name: str, default_value: Any) -> Any: return getattr(self.esdl_asset, esdl_property_name, default_value) def get_profile(self) -> pd.DataFrame: - """Get the profile of the asset.""" + """Get the profile of the asset's ports.""" for esdl_port in self.esdl_asset.port: if esdl_port.profile: return get_data_from_profile(esdl_port.profile[0]) @@ -100,6 +100,15 @@ def get_profile(self) -> pd.DataFrame: ) raise ValueError(f"No profile found for asset: {self.esdl_asset.name}") + def get_constraint_max_profile(self) -> pd.DataFrame: + """Get the profile from the asset's maximum constraint.""" + for constraint in self.esdl_asset.constraint: + if constraint.maximum: + profile = constraint.maximum + else: + return pd.DataFrame() + return get_data_from_profile(profile) + def get_sampling_method(self) -> ProfileSamplingMethod: """Get the interpolation method of the asset.""" # TODO: Get sampling method from ESDL properties if available @@ -194,6 +203,20 @@ def is_heat_transfer_asset(self) -> bool: self.esdl_asset, esdl.HeatExchange ) + def has_profile(self) -> bool: + """Checks if an asset has a profile assigned to any of its ports.""" + for esdl_port in self.esdl_asset.port: + if esdl_port.profile: + return True + return False + + def has_constraint(self) -> bool: + """Checks if an asset has a constraint assigned to it.""" + if self.esdl_asset.constraint.items: + return True + else: + return False + def get_return_temperature(esdl_port: esdl.Port) -> float: """Get the temperature of the port.""" diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 52f8c2ae..f9b74728 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -83,7 +83,7 @@ def update_setpoints(self, time: datetime.datetime) -> dict: """ self.update_networks_factor() total_demand = sum([network.get_total_heat_demand(time) for network in self.networks]) - total_supply = sum([network.get_total_supply() for network in self.networks]) + total_supply = sum([network.get_total_supply(time) for network in self.networks]) total_charge_storage = sum( [network.get_total_charge_storage() for network in self.networks] ) @@ -97,7 +97,7 @@ def update_setpoints(self, time: datetime.datetime) -> dict: f"Consumers are capped to the available power." ) factor = (total_supply + total_discharge_storage) / total_demand - producers = self._set_producers_to_max() + producers = self._set_producers_to_max(time) producers.update(self._set_all_storages_discharge_to_max()) producers.update(self._set_consumer_to_demand(time, factor=factor)) else: @@ -107,19 +107,19 @@ def update_setpoints(self, time: datetime.datetime) -> dict: surplus_supply = total_supply - total_demand if surplus_supply <= total_charge_storage: storages = self._set_storages_charge_power(surplus_supply) - producers = self._set_producers_to_max() + producers = self._set_producers_to_max(time) else: # need to cap the power of the source based on priority storages = self._set_storages_charge_power(total_charge_storage) producers = self._set_producers_based_on_priority( - total_demand + total_charge_storage + time, total_demand + total_charge_storage ) else: # there is a deficit of supply we can discharge the storage, storage becomes # producer. deficit_supply = total_demand - total_supply storages = self._set_storages_discharge_power(deficit_supply) - producers = self._set_producers_to_max() + producers = self._set_producers_to_max(time) producers.update(consumers) producers.update(storages) # Getting the settings for the heat transfer assets @@ -153,10 +153,10 @@ def update_setpoints(self, time: datetime.datetime) -> dict: producers[pressure_set_asset][PROPERTY_SET_PRESSURE] = True return producers - def _set_producers_to_max(self) -> dict: + def _set_producers_to_max(self, time: datetime.datetime) -> dict: result = {} for network in self.networks: - result.update(network.set_supply_to_max()) + result.update(network.set_supply_to_max(time)) return result def _set_all_storages_discharge_to_max(self) -> dict: @@ -199,7 +199,9 @@ def _set_storages_discharge_power(self, power: float) -> dict: results.update(network.set_storage_discharge_power(factor=factor)) return results - def _set_producers_based_on_priority(self, required_supply: float) -> dict: + def _set_producers_based_on_priority( + self, time: datetime.datetime, required_supply: float + ) -> dict: """Method to set the producers based on the priority of the source.""" producers = {} priority = 0 @@ -210,12 +212,12 @@ def _set_producers_based_on_priority(self, required_supply: float) -> dict: if required_supply > 0: # set the producers with the priority to the max for network in self.networks: - producers.update(network.set_supply_to_max(priority)) + producers.update(network.set_supply_to_max(time, priority)) else: # set the producers with the priority with a factor. factor = 1 + required_supply / max_supply_priority for network in self.networks: - producers.update(network.set_supply(factor=factor, priority=priority)) + producers.update(network.set_supply(time, factor=factor, priority=priority)) if len(producers) < sum([len(network.producers) for network in self.networks]): # not al producers are set need to set the remaining to zero. for network in self.networks: diff --git a/src/omotes_simulator_core/infrastructure/app.py b/src/omotes_simulator_core/infrastructure/app.py index a7f9d91e..8ef26cbb 100644 --- a/src/omotes_simulator_core/infrastructure/app.py +++ b/src/omotes_simulator_core/infrastructure/app.py @@ -43,7 +43,7 @@ def run(file_path: str | None = None) -> pd.DataFrame: name="test run", timestep=3600, start=datetime.strptime("2019-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S"), - stop=datetime.strptime("2019-01-01T01:00:00", "%Y-%m-%dT%H:%M:%S"), + stop=datetime.strptime("2019-01-08T01:00:00", "%Y-%m-%dT%H:%M:%S"), ) esdl_file_path = sys.argv[1] if file_path is None else file_path diff --git a/testdata/test1_prod_profile.esdl b/testdata/test1_prod_profile.esdl new file mode 100644 index 00000000..025692ee --- /dev/null +++ b/testdata/test1_prod_profile.esdl @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/unit_test/adapters/transforms/controller_mappers/test_controller_producer_mapper.py b/unit_test/adapters/transforms/controller_mappers/test_controller_producer_mapper.py index 883deb03..c74f655d 100644 --- a/unit_test/adapters/transforms/controller_mappers/test_controller_producer_mapper.py +++ b/unit_test/adapters/transforms/controller_mappers/test_controller_producer_mapper.py @@ -26,8 +26,9 @@ class TestControllerProducerMapper(unittest.TestCase): """Test ControllerProducerMapper.""" - def setUp(self) -> None: - """Set up test case.""" + def test_to_entity_method(self): + """Test settings of controller for producer.""" + # Arrange # Load the test esdl file esdl_file_path = ( Path(__file__).parent / ".." / ".." / ".." / ".." / "testdata" / "test_ates.esdl" @@ -35,10 +36,6 @@ def setUp(self) -> None: self.esdl_object = EsdlObject(pyesdl_from_file(esdl_file_path)) # Create a ControllerProducerMapper object self.mapper = ControllerProducerMapper() - - def test_to_entity_method(self): - """Test settings of controller for producer.""" - # Arrange producer_assets = self.esdl_object.get_all_assets_of_type("producer") # Act @@ -49,3 +46,26 @@ def test_to_entity_method(self): self.assertEqual(controller_producer.temperature_out, 353.15) self.assertEqual(controller_producer.power, 1e8) self.assertEqual(controller_producer.marginal_costs, 0) + + def test_get_max_constraint(self): + """Test to check if the maximum constraint is grabbed correctly.""" + # Arrange + esdl_file_path_constraint = ( + Path(__file__).parent + / ".." + / ".." + / ".." + / ".." + / "testdata" + / "test1_prod_profile.esdl" + ) + self.esdl_object = EsdlObject(pyesdl_from_file(esdl_file_path_constraint)) + self.mapper = ControllerProducerMapper() + producer_assets = self.esdl_object.get_all_assets_of_type("producer") + + # Act + constraint_profile = producer_assets[0].get_constraint_max_profile() + + # Assert + self.assertEqual(producer_assets[0].has_constraint(), True) + self.assertEqual(constraint_profile.iloc[0].values[1], 143277.6298) diff --git a/unit_test/entities/controller/test_controller_network.py b/unit_test/entities/controller/test_controller_network.py index b79e0aad..7ec058ef 100644 --- a/unit_test/entities/controller/test_controller_network.py +++ b/unit_test/entities/controller/test_controller_network.py @@ -124,12 +124,14 @@ def test_get_total_supply(self): # arrange producer1 = Mock() producer1.power = 10 + producer1.get_max_power = Mock(return_value=10) producer2 = Mock() producer2.power = 20 + producer2.get_max_power = Mock(return_value=20) self.controller_network.factor_to_first_network = 2.0 self.controller_network.producers = [producer1, producer2] # act - res = self.controller_network.get_total_supply() + res = self.controller_network.get_total_supply(datetime.datetime.now()) # assert self.assertEqual(res, 60) @@ -141,13 +143,15 @@ def test_set_supply_to_max_priority(self): producer1.temperature_out = 50 producer1.temperature_in = 40 producer1.priority = 1 + producer1.get_max_power = Mock(return_value=10) producer2 = Mock() producer2.id = "producer2" producer2.power = 20 producer2.priority = 2 + producer2.get_max_power = Mock(return_value=20) self.controller_network.producers = [producer1, producer2] # act - res = self.controller_network.set_supply_to_max(priority=1) + res = self.controller_network.set_supply_to_max(datetime.datetime.now(), priority=1) # assert self.assertEqual( res, @@ -169,15 +173,17 @@ def test_set_supply_to_max_priority_zero(self): producer1.temperature_out = 50 producer1.temperature_in = 40 producer1.priority = 1 + producer1.get_max_power = Mock(return_value=10) producer2 = Mock() producer2.id = "producer2" producer2.power = 20 producer2.priority = 2 producer2.temperature_out = 80 producer2.temperature_in = 100 + producer2.get_max_power = Mock(return_value=20) self.controller_network.producers = [producer1, producer2] # act - res = self.controller_network.set_supply_to_max(priority=0) + res = self.controller_network.set_supply_to_max(datetime.datetime.now(), priority=0) # assert self.assertEqual( res, @@ -205,13 +211,15 @@ def test_set_supply(self): producer1.temperature_out = 50 producer1.temperature_in = 40 producer1.priority = 1 + producer1.get_max_power = Mock(return_value=10) producer2 = Mock() producer2.id = "producer2" producer2.power = 20 producer2.priority = 2 + producer2.get_max_power = Mock(return_value=20) self.controller_network.producers = [producer1, producer2] # act - res = self.controller_network.set_supply(priority=1, factor=0.5) + res = self.controller_network.set_supply(datetime.datetime.now(), priority=1, factor=0.5) # assert self.assertEqual( res, diff --git a/unit_test/entities/controller/test_controller_new_class.py b/unit_test/entities/controller/test_controller_new_class.py index a1144ba9..3b5be63f 100644 --- a/unit_test/entities/controller/test_controller_new_class.py +++ b/unit_test/entities/controller/test_controller_new_class.py @@ -17,6 +17,8 @@ import unittest from unittest.mock import Mock +import pandas as pd + from omotes_simulator_core.entities.assets.asset_defaults import ( PRIMARY, PROPERTY_HEAT_DEMAND, @@ -104,6 +106,7 @@ def setup_update_set_points(self): power=50, marginal_costs=1, priority=2, + profile=pd.DataFrame(), ) producer2 = ControllerProducer( name="producer2", @@ -113,6 +116,7 @@ def setup_update_set_points(self): power=40, marginal_costs=1, priority=3, + profile=pd.DataFrame(), ) consumer1 = Mock(spec=ControllerConsumer) consumer1.id = "consumer1" @@ -299,7 +303,7 @@ def test_set_producers_to_max(self): return_value={"id3": {"prop1": 9, "prop2": 15}} ) # act - result = self.controller._set_producers_to_max() + result = self.controller._set_producers_to_max(datetime.datetime.now()) # assert self.assertEqual( result, @@ -389,6 +393,7 @@ def test_set_producers_based_on_priority(self): power=50, marginal_costs=1, priority=2, + profile=pd.DataFrame(), ) producer2 = ControllerProducer( name="producer2", @@ -398,6 +403,7 @@ def test_set_producers_based_on_priority(self): power=40, marginal_costs=1, priority=3, + profile=pd.DataFrame(), ) producer3 = ControllerProducer( name="producer3", @@ -407,6 +413,7 @@ def test_set_producers_based_on_priority(self): power=40, marginal_costs=1, priority=1, + profile=pd.DataFrame(), ) producer4 = ControllerProducer( name="producer4", @@ -416,6 +423,7 @@ def test_set_producers_based_on_priority(self): power=20, marginal_costs=1, priority=3, + profile=pd.DataFrame(), ) self.controller.networks[0] = ControllerNetwork( heat_transfer_assets_prim_in=[], @@ -440,7 +448,7 @@ def test_set_producers_based_on_priority(self): storages_in=[], ) # act - result = self.controller._set_producers_based_on_priority(120) + result = self.controller._set_producers_based_on_priority(datetime.datetime.now(), 120) # assert self.assertEqual(result["producer1"][PROPERTY_HEAT_DEMAND], 50) @@ -458,6 +466,7 @@ def test_get_total_supply_priority(self): power=50, marginal_costs=1, priority=2, + profile=pd.DataFrame(), ) producer2 = ControllerProducer( name="producer2", @@ -467,6 +476,7 @@ def test_get_total_supply_priority(self): power=40, marginal_costs=1, priority=3, + profile=pd.DataFrame(), ) producer3 = ControllerProducer( name="producer3", @@ -476,6 +486,7 @@ def test_get_total_supply_priority(self): power=40, marginal_costs=1, priority=1, + profile=pd.DataFrame(), ) producer4 = ControllerProducer( name="producer4", @@ -485,6 +496,7 @@ def test_get_total_supply_priority(self): power=20, marginal_costs=1, priority=3, + profile=pd.DataFrame(), ) self.controller.networks[0] = ControllerNetwork( heat_transfer_assets_prim_in=[], diff --git a/unit_test/entities/controller/test_controller_producer.py b/unit_test/entities/controller/test_controller_producer.py index d0ee406d..c842b6a6 100644 --- a/unit_test/entities/controller/test_controller_producer.py +++ b/unit_test/entities/controller/test_controller_producer.py @@ -14,6 +14,9 @@ # along with this program. If not, see . """Test controller producer class.""" import unittest +from datetime import datetime + +import pandas as pd from omotes_simulator_core.entities.assets.asset_defaults import ( DEFAULT_TEMPERATURE, @@ -25,10 +28,16 @@ class ControllerProducerTest(unittest.TestCase): """Testcase for ControllerProducer class.""" - def test_controller_producer_init(self) -> None: - """Init test for ControllerProducer.""" - # Arrange - producer = ControllerProducer( + def setUp(self): + """Set up the test case.""" + self.values = [100, 200] + self.profile = pd.DataFrame( + { + "date": [datetime(2021, 1, 1, 0, 0, 0), datetime(2021, 1, 1, 1, 0, 0)], + "values": self.values, + } + ) + self.producer = ControllerProducer( "producer", "id", temperature_out=DEFAULT_TEMPERATURE + DEFAULT_TEMPERATURE_DIFFERENCE, @@ -36,19 +45,21 @@ def test_controller_producer_init(self) -> None: power=1000, marginal_costs=0.1, priority=1, + profile=self.profile, ) - # Act + def test_controller_producer_init(self) -> None: + """Init test for ControllerProducer.""" # Assert - self.assertEqual(producer.name, "producer") - self.assertEqual(producer.id, "id") - self.assertEqual(producer.temperature_in, DEFAULT_TEMPERATURE) + self.assertEqual(self.producer.name, "producer") + self.assertEqual(self.producer.id, "id") + self.assertEqual(self.producer.temperature_in, DEFAULT_TEMPERATURE) self.assertEqual( - producer.temperature_out, DEFAULT_TEMPERATURE + DEFAULT_TEMPERATURE_DIFFERENCE + self.producer.temperature_out, DEFAULT_TEMPERATURE + DEFAULT_TEMPERATURE_DIFFERENCE ) - self.assertEqual(producer.power, 1000) - self.assertEqual(producer.marginal_costs, 0.1) - self.assertEqual(producer.priority, 1) + self.assertEqual(self.producer.power, 1000) + self.assertEqual(self.producer.marginal_costs, 0.1) + self.assertEqual(self.producer.priority, 1) def test_controller_producer_none_priority(self) -> None: """Test to ensure a None priority does not break the CotrollerProducer. @@ -57,25 +68,22 @@ def test_controller_producer_none_priority(self) -> None: producer with no priority assigned to it. """ # Arrange - producer = ControllerProducer( - "producer", - "id", - temperature_out=DEFAULT_TEMPERATURE + DEFAULT_TEMPERATURE_DIFFERENCE, - temperature_in=DEFAULT_TEMPERATURE, - power=1000, - marginal_costs=0.1, - priority=None, - ) + self.producer.priority = None + + # Assert + self.assertEqual(self.producer.priority, None) + + def test_get_max_power_profile(self) -> None: + """Test to check if get_max_power returns the profile value or the power esdl value.""" + # Arrange + time = datetime(2021, 1, 1, 0, 0, 0) # Act + self.producer.profile = pd.DataFrame() + max_power_1 = self.producer.get_max_power(time) # No constraint profile present. + self.producer.profile = self.profile.set_index("date") + max_power_2 = self.producer.get_max_power(time) # Assert - self.assertEqual(producer.name, "producer") - self.assertEqual(producer.id, "id") - self.assertEqual(producer.temperature_in, DEFAULT_TEMPERATURE) - self.assertEqual( - producer.temperature_out, DEFAULT_TEMPERATURE + DEFAULT_TEMPERATURE_DIFFERENCE - ) - self.assertEqual(producer.power, 1000) - self.assertEqual(producer.marginal_costs, 0.1) - self.assertEqual(producer.priority, None) + self.assertEqual(max_power_1, self.producer.power) + self.assertEqual(max_power_2, self.values[0])