From cd84a336d7dafdcdf304c27af51b2e19e25dff2f Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Fri, 13 Mar 2026 14:47:26 +0100 Subject: [PATCH 1/6] Added the functionality to prioritise profiles on the out port. In producers, it now checks if there is a profile on the out port and takes that if existing and ignores the constraint. --- .../controller_producer_mapper.py | 14 +++++++++++--- .../entities/assets/esdl_asset_object.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) 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 a8beca2d..11226a21 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 @@ -51,8 +51,15 @@ def to_entity( 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() + resampled_profile = None + # Check if there is a profile as a constraint or out port. If there is both, + # only the port profile is used. + if esdl_asset.has_out_profile() or esdl_asset.has_constraint(): + if esdl_asset.has_out_profile(): + profile = esdl_asset.get_out_port_profile() + else: + profile = esdl_asset.get_constraint_max_profile() + self.profile_interpolator = ProfileInterpolator( profile=profile, sampling_method=esdl_asset.get_sampling_method(), @@ -60,7 +67,8 @@ def to_entity( timestep=timestep, ) resampled_profile = self.profile_interpolator.get_resampled_profile() - else: + + if resampled_profile is None: resampled_profile = pd.DataFrame() contr_producer = ControllerProducer( 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 6e466b43..5d2be3ab 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -100,6 +100,17 @@ def get_profile(self) -> pd.DataFrame: extra={"esdl_object_id": self.get_id()}, ) raise ValueError(f"No profile found for asset: {self.esdl_asset.name}") + + def get_out_port_profile(self) -> pd.DataFrame: + """Get the profile of the asset's out ports.""" + for port in self.esdl_asset.port: + if isinstance(port, esdl.OutPort) and port.profile.items: + return get_data_from_profile(port.profile[0]) + logger.error( + f"No profile found for asset: {self.esdl_asset.name}", + extra={"esdl_object_id": self.get_id()}, + ) + raise ValueError(f"No profile found at out port for asset: {self.esdl_asset.name}") def get_constraint_max_profile(self) -> pd.DataFrame: """Get the profile from the asset's maximum constraint.""" @@ -210,6 +221,14 @@ def has_profile(self) -> bool: if esdl_port.profile: return True return False + + def has_out_profile(self) -> bool: + """Checks if an asset has a profile assigned to its out port.""" + # TODO: Additional checks will be needed to see if the correct type of profile is assigned here. + for port in self.esdl_asset.port: + if isinstance(port, esdl.OutPort) and port.profile.items: + return True + return False def has_constraint(self) -> bool: """Checks if an asset has a constraint assigned to it.""" From 90cb6a940aa2ce817c4c291be27327be5dc364f0 Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Fri, 13 Mar 2026 17:08:22 +0100 Subject: [PATCH 2/6] When setting up the simulation, it now checks if the producer profile comes from the optimizer before setting the production maximum. --- .../controller_producer_mapper.py | 8 ++++---- .../entities/assets/esdl_asset_object.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) 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 11226a21..7e8aa844 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 @@ -52,10 +52,10 @@ def to_entity( strategy_priority = esdl_asset.get_strategy_priority() resampled_profile = None - # Check if there is a profile as a constraint or out port. If there is both, - # only the port profile is used. - if esdl_asset.has_out_profile() or esdl_asset.has_constraint(): - if esdl_asset.has_out_profile(): + # Check if there is an optimizer out port profile or a constraint profile. If there is both, + # only the optimizer profile is used. + if esdl_asset.has_out_optimizer_profile() or esdl_asset.has_constraint(): + if esdl_asset.has_out_optimizer_profile(): profile = esdl_asset.get_out_port_profile() else: profile = esdl_asset.get_constraint_max_profile() 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 5d2be3ab..ad5d1a90 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -222,13 +222,16 @@ def has_profile(self) -> bool: return True return False - def has_out_profile(self) -> bool: - """Checks if an asset has a profile assigned to its out port.""" - # TODO: Additional checks will be needed to see if the correct type of profile is assigned here. + def has_out_optimizer_profile(self) -> bool: + """Checks if an asset has an optimizer profile assigned to its out port.""" for port in self.esdl_asset.port: - if isinstance(port, esdl.OutPort) and port.profile.items: - return True - return False + if isinstance(port, esdl.OutPort) and port.profile.items: + # There is a profile on the out port + data_source = port.profile[0].dataSource + if data_source is not None: + if data_source.attribution == "optimizer": + return True + return False def has_constraint(self) -> bool: """Checks if an asset has a constraint assigned to it.""" From 8b216283f0d14ca77b9035fda643fe90c7e489bd Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Fri, 20 Mar 2026 17:27:54 +0100 Subject: [PATCH 3/6] Fixed issue with how the profiles were loaded. --- .../entities/assets/esdl_asset_object.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 ad5d1a90..b6f3a894 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -105,7 +105,9 @@ def get_out_port_profile(self) -> pd.DataFrame: """Get the profile of the asset's out ports.""" for port in self.esdl_asset.port: if isinstance(port, esdl.OutPort) and port.profile.items: - return get_data_from_profile(port.profile[0]) + for profile in port.profile: + if profile.field == "HeatIn.Q": + return get_data_from_profile(profile) logger.error( f"No profile found for asset: {self.esdl_asset.name}", extra={"esdl_object_id": self.get_id()}, @@ -227,10 +229,14 @@ def has_out_optimizer_profile(self) -> bool: for port in self.esdl_asset.port: if isinstance(port, esdl.OutPort) and port.profile.items: # There is a profile on the out port - data_source = port.profile[0].dataSource - if data_source is not None: - if data_source.attribution == "optimizer": - return True + for profile in port.profile: + try: + data_source = profile.dataSource + if data_source is not None: + if data_source.attribution == "optimizer": + return True + except AttributeError: + continue return False def has_constraint(self) -> bool: From b991b74975e6380da563c8ec7ed72270813f6487 Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Thu, 2 Apr 2026 17:08:48 +0200 Subject: [PATCH 4/6] Fixed influxdb importer so it can now take profiles from the optimizer. --- .../entities/assets/esdl_asset_object.py | 4 +- .../entities/utility/influxdb_reader.py | 60 +++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) 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 b6f3a894..ec9f7ffe 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -106,7 +106,7 @@ def get_out_port_profile(self) -> pd.DataFrame: for port in self.esdl_asset.port: if isinstance(port, esdl.OutPort) and port.profile.items: for profile in port.profile: - if profile.field == "HeatIn.Q": + if profile.field == "Heat_flow": return get_data_from_profile(profile) logger.error( f"No profile found for asset: {self.esdl_asset.name}", @@ -233,7 +233,7 @@ def has_out_optimizer_profile(self) -> bool: try: data_source = profile.dataSource if data_source is not None: - if data_source.attribution == "optimizer": + if data_source.name == "Optimizer": return True except AttributeError: continue diff --git a/src/omotes_simulator_core/entities/utility/influxdb_reader.py b/src/omotes_simulator_core/entities/utility/influxdb_reader.py index 615ab1df..34ba8b6e 100644 --- a/src/omotes_simulator_core/entities/utility/influxdb_reader.py +++ b/src/omotes_simulator_core/entities/utility/influxdb_reader.py @@ -15,16 +15,40 @@ """Module to read the esdl profiles from an energy system.""" import logging +from datetime import datetime +from typing import cast import esdl import pandas as pd from esdl.esdl_handler import EnergySystemHandler -from esdl.profiles.influxdbprofilemanager import InfluxDBProfileManager +from esdl.profiles.influxdbprofilemanager import ConnectionSettings, InfluxDBProfileManager from esdl.units.conversion import ENERGY_IN_J, POWER_IN_W, convert_to_unit logger = logging.getLogger(__name__) +def _normalize_influx_filters(filters: str | None) -> list[dict[str, str]]: + """Parse and normalize filters to the format expected by load_influxdb. + + The upstream parser may return dictionaries with a `key` field while + load_influxdb expects `tag`. This shim keeps compatibility across versions. + """ + parsed_filters = InfluxDBProfileManager._parse_esdl_profile_filters(filters) + normalized_filters: list[dict[str, str]] = [] + for influx_filter in parsed_filters: + tag = influx_filter.get("tag") or influx_filter.get("key") + value = influx_filter.get("value") + if not tag or value is None: + continue + normalized_filters.append( + { + "tag": str(tag), + "value": str(value).strip().strip("\"").strip("'"), + } + ) + return normalized_filters + + def parse_esdl_profiles(esh: EnergySystemHandler) -> dict[str, pd.DataFrame]: """Method to parse the esdl profiles from an energy system. @@ -48,7 +72,11 @@ def get_data_from_profile(esdl_profile: esdl.InfluxDBProfile) -> pd.DataFrame: :return: pandas.DataFrame with the data """ influx_cred_map: dict[str, tuple[str, str]] = {} - profile_host = esdl_profile.host + profile_host = str(esdl_profile.host) + profile_port = int(esdl_profile.port) + profile_database = str(esdl_profile.database) + profile_measurement = str(esdl_profile.measurement) + profile_field = str(esdl_profile.field) ssl_setting = False if "https" in profile_host: profile_host = profile_host[8:] @@ -56,25 +84,35 @@ def get_data_from_profile(esdl_profile: esdl.InfluxDBProfile) -> pd.DataFrame: elif "http" in profile_host: profile_host = profile_host[7:] # why is this here? - if esdl_profile.port == 443: + if profile_port == 443: ssl_setting = True - influx_host = f"{profile_host}:{esdl_profile.port}" + influx_host = f"{profile_host}:{profile_port}" if influx_host in influx_cred_map: (username, password) = influx_cred_map[influx_host] else: username = None password = None - time_series_data = InfluxDBProfileManager.create_esdl_influxdb_profile_manager( - esdl_profile, - username, - password, - ssl_setting, - ssl_setting, + conn_settings = ConnectionSettings( + host=profile_host, + port=profile_port, + database=profile_database, + username=username or "", + password=password or "", + ssl=ssl_setting, + verify_ssl=ssl_setting, + ) + time_series_data = InfluxDBProfileManager(conn_settings) + time_series_data.load_influxdb( + measurement=profile_measurement, + fields=[profile_field], + from_datetime=cast(datetime, esdl_profile.startDate), + to_datetime=cast(datetime, esdl_profile.endDate), + filters=_normalize_influx_filters(str(esdl_profile.filters) if esdl_profile.filters else None), ) # Error check start and end dates of profiles - # I do not thing this is required since you set it in mapeditor. + # I do not think this is required since you set it in mapeditor. if time_series_data.end_datetime != esdl_profile.endDate: logger.error( f"The user input profile end datetime: {esdl_profile.endDate} does not match the end" From e5ccfce7d17d98eda47801796066f8ae1f2a0793 Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Wed, 8 Apr 2026 15:12:19 +0200 Subject: [PATCH 5/6] Added tests. --- .../entities/assets/esdl_asset_object.py | 13 ++++--- unit_test/entities/test_esdl_object.py | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) 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 ec9f7ffe..bdef6dad 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -230,13 +230,12 @@ def has_out_optimizer_profile(self) -> bool: if isinstance(port, esdl.OutPort) and port.profile.items: # There is a profile on the out port for profile in port.profile: - try: - data_source = profile.dataSource - if data_source is not None: - if data_source.name == "Optimizer": - return True - except AttributeError: - continue + if ( + hasattr(profile, "dataSource") and + hasattr(profile.dataSource, "name") and + profile.dataSource.name == "Optimizer" + ): + return True return False def has_constraint(self) -> bool: diff --git a/unit_test/entities/test_esdl_object.py b/unit_test/entities/test_esdl_object.py index 6bbf7a86..c70d1312 100644 --- a/unit_test/entities/test_esdl_object.py +++ b/unit_test/entities/test_esdl_object.py @@ -16,6 +16,7 @@ """Test esdl object class.""" import unittest from pathlib import Path +from unittest.mock import MagicMock, Mock import esdl from pandas.testing import assert_frame_equal @@ -423,6 +424,41 @@ def test_get_asset_by_id(self): # Assert self.assertEqual(asset.esdl_asset, producer.esdl_asset) + def test_has_out_optimizer_profile(self): + """Test to see if the method to check for optimizer profiles works correctly.""" + + # Arrange + asset_with_mocked_profiles = self.esdl_object.get_all_assets_of_type("producer")[0] + + non_optimizer_profile = Mock() + non_optimizer_profile.dataSource = Mock() + non_optimizer_profile.dataSource.name = "NotOptimizer" + + optimizer_profile = Mock() + optimizer_profile.dataSource = Mock() + optimizer_profile.dataSource.name = "Optimizer" + + mocked_profiles = MagicMock() + mocked_profiles.items = [non_optimizer_profile, optimizer_profile] + mocked_profiles.__iter__.return_value = iter([non_optimizer_profile, optimizer_profile]) + + mocked_out_port = Mock(spec=esdl.OutPort) + mocked_out_port.profile = mocked_profiles + + mocked_esdl_asset = Mock() + mocked_esdl_asset.port = [mocked_out_port] + asset_with_mocked_profiles.esdl_asset = mocked_esdl_asset + + asset_without_profile = self.esdl_object.get_all_assets_of_type("pipe")[0] + + # Act + has_optimizer_profile = asset_with_mocked_profiles.has_out_optimizer_profile() + has_no_optimizer_profile = asset_without_profile.has_out_optimizer_profile() + + # Assert + self.assertTrue(has_optimizer_profile) + self.assertIs(has_no_optimizer_profile, False) + class StringEsdlAssetMapperTest(unittest.TestCase): """Class to test conversion from esdl asset to string and back.""" From d2b7d2d3ddf748e73ea3cb7622fdfda7bc0eeae4 Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Wed, 8 Apr 2026 15:20:49 +0200 Subject: [PATCH 6/6] Linting and formatting. --- .../controller_mappers/controller_producer_mapper.py | 2 +- .../entities/assets/esdl_asset_object.py | 12 ++++++------ .../entities/utility/influxdb_reader.py | 3 ++- unit_test/entities/test_esdl_object.py | 1 - 4 files changed, 9 insertions(+), 9 deletions(-) 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 7e8aa844..c278e614 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 @@ -67,7 +67,7 @@ def to_entity( timestep=timestep, ) resampled_profile = self.profile_interpolator.get_resampled_profile() - + if resampled_profile is None: resampled_profile = pd.DataFrame() 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 bdef6dad..78462f3d 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -100,7 +100,7 @@ def get_profile(self) -> pd.DataFrame: extra={"esdl_object_id": self.get_id()}, ) raise ValueError(f"No profile found for asset: {self.esdl_asset.name}") - + def get_out_port_profile(self) -> pd.DataFrame: """Get the profile of the asset's out ports.""" for port in self.esdl_asset.port: @@ -223,17 +223,17 @@ def has_profile(self) -> bool: if esdl_port.profile: return True return False - + def has_out_optimizer_profile(self) -> bool: """Checks if an asset has an optimizer profile assigned to its out port.""" for port in self.esdl_asset.port: - if isinstance(port, esdl.OutPort) and port.profile.items: + if isinstance(port, esdl.OutPort) and port.profile.items: # There is a profile on the out port for profile in port.profile: if ( - hasattr(profile, "dataSource") and - hasattr(profile.dataSource, "name") and - profile.dataSource.name == "Optimizer" + hasattr(profile, "dataSource") + and hasattr(profile.dataSource, "name") + and profile.dataSource.name == "Optimizer" ): return True return False diff --git a/src/omotes_simulator_core/entities/utility/influxdb_reader.py b/src/omotes_simulator_core/entities/utility/influxdb_reader.py index 34ba8b6e..c225c738 100644 --- a/src/omotes_simulator_core/entities/utility/influxdb_reader.py +++ b/src/omotes_simulator_core/entities/utility/influxdb_reader.py @@ -108,7 +108,8 @@ def get_data_from_profile(esdl_profile: esdl.InfluxDBProfile) -> pd.DataFrame: fields=[profile_field], from_datetime=cast(datetime, esdl_profile.startDate), to_datetime=cast(datetime, esdl_profile.endDate), - filters=_normalize_influx_filters(str(esdl_profile.filters) if esdl_profile.filters else None), + filters=_normalize_influx_filters(str(esdl_profile.filters) + if esdl_profile.filters else None), ) # Error check start and end dates of profiles diff --git a/unit_test/entities/test_esdl_object.py b/unit_test/entities/test_esdl_object.py index c70d1312..1b57cbcd 100644 --- a/unit_test/entities/test_esdl_object.py +++ b/unit_test/entities/test_esdl_object.py @@ -426,7 +426,6 @@ def test_get_asset_by_id(self): def test_has_out_optimizer_profile(self): """Test to see if the method to check for optimizer profiles works correctly.""" - # Arrange asset_with_mocked_profiles = self.esdl_object.get_all_assets_of_type("producer")[0]