diff --git a/src/mesido/esdl/asset_to_component_base.py b/src/mesido/esdl/asset_to_component_base.py
index 426308b4..532ab7f6 100644
--- a/src/mesido/esdl/asset_to_component_base.py
+++ b/src/mesido/esdl/asset_to_component_base.py
@@ -900,6 +900,7 @@ def _get_connected_i_nominal_and_max(self, asset: Asset) -> Tuple[float, float]:
or asset.asset_type == "Electrolyzer"
or asset.asset_type == "ElectricBoiler"
or asset.asset_type == "HeatPump"
+ or asset.asset_type == "GeothermalSource"
):
for port in asset.in_ports:
if isinstance(port.carrier, esdl.ElectricityCommodity):
diff --git a/src/mesido/esdl/esdl_heat_model.py b/src/mesido/esdl/esdl_heat_model.py
index 2d3d1cc2..1af489b5 100644
--- a/src/mesido/esdl/esdl_heat_model.py
+++ b/src/mesido/esdl/esdl_heat_model.py
@@ -42,6 +42,7 @@
GasSubstation,
GasTankStorage,
GeothermalSource,
+ GeothermalSourceElec,
HeatBuffer,
HeatDemand,
HeatExchanger,
@@ -1450,8 +1451,24 @@ def convert_heat_source(self, asset: Asset) -> Tuple[Type[HeatSource], MODIFIERS
f"{asset.asset_type} '{asset.name}' has no desired flow rate specified. "
f"'{asset.name}' will not be actuated in a constant manner"
)
-
- return GeothermalSource, modifiers
+ modifiers["elec_power_nominal"] = max_supply
+ modifiers["cop"] = asset.attributes["COP"] if asset.attributes["COP"] else 0.0
+ if len(asset.in_ports) == 2:
+ for port in asset.in_ports:
+ if isinstance(port.carrier, esdl.ElectricityCommodity):
+ min_voltage = port.carrier.voltage
+ i_max, i_nom = self._get_connected_i_nominal_and_max(asset)
+ modifiers.update(
+ min_voltage=min_voltage,
+ ElectricityIn=dict(
+ Power=dict(min=0.0, max=max_supply, nominal=max_supply / 2.0),
+ I=dict(min=0.0, max=i_max, nominal=i_nom),
+ V=dict(min=min_voltage, nominal=min_voltage),
+ ),
+ )
+ return GeothermalSourceElec, modifiers
+ else:
+ return GeothermalSource, modifiers
elif asset.asset_type == "HeatPump":
modifiers["cop"] = asset.attributes["COP"]
return AirWaterHeatPump, modifiers
diff --git a/src/mesido/esdl/esdl_model_base.py b/src/mesido/esdl/esdl_model_base.py
index 66b06d94..83a96247 100644
--- a/src/mesido/esdl/esdl_model_base.py
+++ b/src/mesido/esdl/esdl_model_base.py
@@ -180,7 +180,7 @@ def __set_primary_secondary_heat_ports():
f"milp(4) and electricity (1) ports"
)
elif (
- asset.asset_type == "HeatPump"
+ (asset.asset_type == "HeatPump")
and len(asset.out_ports) == 1
and len(asset.in_ports) in [1, 2]
):
@@ -201,11 +201,12 @@ def __set_primary_secondary_heat_ports():
)
else:
raise Exception(
- f"{asset.name} has incorrect number of in/out ports. HeatPumps are allows "
- f"to have 1 in and 1 out port for air-water HP, 2 in ports and 2 out ports "
- f"when modelling a water-water HP, or 3 in ports and 2 out ports when the "
- f"electricity connection of the water-water HP is modelled."
+ f"{asset.name} has incorrect number of in/out ports. HeatPumps allow "
+ f"to have 1 in and 1 out port for air-water HP, 2 in ports and 2 out "
+ f"ports when modelling a water-water HP, or 3 in ports and 2 out ports "
+ f"when the electricity connection of the water-water HP is modelled."
)
+
elif (
asset.asset_type == "GasHeater"
and len(asset.out_ports) == 1
@@ -226,6 +227,7 @@ def __set_primary_secondary_heat_ports():
)
elif (
asset.asset_type == "ElectricBoiler"
+ or asset.asset_type == "GeothermalSource"
and len(asset.out_ports) == 1
and len(asset.in_ports) == 2
):
@@ -258,6 +260,7 @@ def __set_primary_secondary_heat_ports():
raise Exception(
f"{asset.name} must have one inport for electricity and one outport for gas"
)
+
elif (
asset.in_ports is None
and isinstance(asset.out_ports[0].carrier, esdl.ElectricityCommodity)
diff --git a/src/mesido/pycml/component_library/milp/__init__.py b/src/mesido/pycml/component_library/milp/__init__.py
index 6557874c..77e4a40c 100644
--- a/src/mesido/pycml/component_library/milp/__init__.py
+++ b/src/mesido/pycml/component_library/milp/__init__.py
@@ -21,6 +21,7 @@
from .heat.cold_demand import ColdDemand
from .heat.control_valve import ControlValve
from .heat.geothermal_source import GeothermalSource
+from .heat.geothermal_source_elec import GeothermalSourceElec
from .heat.heat_buffer import HeatBuffer
from .heat.heat_demand import HeatDemand
from .heat.heat_exchanger import HeatExchanger
@@ -68,6 +69,7 @@
"GasSubstation",
"GasTankStorage",
"GeothermalSource",
+ "GeothermalSourceElec",
"HeatExchanger",
"HeatFourPort",
"HeatPipe",
diff --git a/src/mesido/pycml/component_library/milp/heat/geothermal_source.py b/src/mesido/pycml/component_library/milp/heat/geothermal_source.py
index a91a4099..b01049fb 100644
--- a/src/mesido/pycml/component_library/milp/heat/geothermal_source.py
+++ b/src/mesido/pycml/component_library/milp/heat/geothermal_source.py
@@ -1,3 +1,4 @@
+from mesido.pycml import Variable
from mesido.pycml.pycml_mixin import add_variables_documentation_automatically
from numpy import nan
@@ -9,11 +10,14 @@
class GeothermalSource(HeatSource):
"""
The geothermal source component is used to model geothermal doublets. It is equivilent to a
- normal source with the only difference being in the modelling of doublets. The main reason for
- this component instead of using just a regular source is that to have the integer behaviour of
- increasing the amount of doublets. In the HeatMixin an integer is created _aggregation_count to
- model the amount of doublets and the maximum power will scale with this integer instead of
- continuous. This will also ensure that the cost will scale with this integer.
+ normal source with the only difference being the modelling of doublets and power consumption.
+ The main reason for this component instead of using just a regular source is that to have the
+ integer behaviour of increasing the amount of doublets. In the HeatMixin an integer is created
+ _aggregation_count to model the amount of doublets and the maximum power will scale with this
+ integer instead of continuous. This will also ensure that the cost will scale with this integer.
+ The power consumption is computed through a COP calculation, directly linked to the heat source
+ production. COP is set to a default value of 0 in order to ensure power and its associated costs
+ are only inlcuded in the computations intentionally.
Variables created:
{add_variable_names_for_documentation_here}
@@ -31,4 +35,13 @@ def __init__(self, name, **modifiers):
self.target_flow_rate = nan
self.single_doublet_power = nan
+ self.cop = nan
+ self.elec_power_nominal = nan
self.nr_of_doublets = 1.0
+
+ self.add_variable(Variable, "Power_elec", min=0.0, nominal=self.elec_power_nominal)
+
+ if self.cop == 0.0:
+ self.add_equation((self.Power_elec))
+ else:
+ self.add_equation(((self.Power_elec * self.cop - self.Heat_source) / self.Heat_nominal))
diff --git a/src/mesido/pycml/component_library/milp/heat/geothermal_source_elec.py b/src/mesido/pycml/component_library/milp/heat/geothermal_source_elec.py
new file mode 100644
index 00000000..8b9b6996
--- /dev/null
+++ b/src/mesido/pycml/component_library/milp/heat/geothermal_source_elec.py
@@ -0,0 +1,35 @@
+from mesido.pycml.component_library.milp.electricity.electricity_base import ElectricityPort
+from mesido.pycml.component_library.milp.heat.geothermal_source import GeothermalSource
+from mesido.pycml.pycml_mixin import add_variables_documentation_automatically
+
+from numpy import nan
+
+
+@add_variables_documentation_automatically
+class GeothermalSourceElec(GeothermalSource):
+ """
+ The geothermal source electric asset is almost identical to the geothermal source one. The
+ only difference is that this one includes an electricity in port. The electricity power
+ calculation that it inherits from the geothermal source asset needs to be satisfied by an
+ electricity carrier supplied through this new in port.
+
+ Variables created:
+ {add_variable_names_for_documentation_here}
+
+ Parameters:
+ name : The name of the asset. \n
+ modifiers : Dictionary with asset information.
+ """
+
+ def __init__(self, name, **modifiers):
+ super().__init__(
+ name,
+ **modifiers,
+ )
+
+ self.component_subtype = "geothermal_source_elec"
+ self.min_voltage = nan
+
+ self.add_variable(ElectricityPort, "ElectricityIn")
+
+ self.add_equation(((self.ElectricityIn.Power - self.Power_elec) / self.elec_power_nominal))
diff --git a/tests/models/source_pipe_sink/model/sourcesink_with_geo.esdl b/tests/models/source_pipe_sink/model/sourcesink_with_geo.esdl
new file mode 100644
index 00000000..f69ec905
--- /dev/null
+++ b/tests/models/source_pipe_sink/model/sourcesink_with_geo.esdl
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/models/source_pipe_sink/model/sourcesink_with_geo_elec.esdl b/tests/models/source_pipe_sink/model/sourcesink_with_geo_elec.esdl
new file mode 100644
index 00000000..c5afad36
--- /dev/null
+++ b/tests/models/source_pipe_sink/model/sourcesink_with_geo_elec.esdl
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/models/source_pipe_sink/model/sourcesink_with_geo_elec_no_cop.esdl b/tests/models/source_pipe_sink/model/sourcesink_with_geo_elec_no_cop.esdl
new file mode 100644
index 00000000..a164e3e6
--- /dev/null
+++ b/tests/models/source_pipe_sink/model/sourcesink_with_geo_elec_no_cop.esdl
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_multicommodity.py b/tests/test_multicommodity.py
index d38742a9..bbb2013d 100644
--- a/tests/test_multicommodity.py
+++ b/tests/test_multicommodity.py
@@ -324,8 +324,145 @@ def solver_options(self):
np.testing.assert_allclose(var_opex_hp_calc, var_opex_hp)
-if __name__ == "__main__":
+class TestGeothermalSourceElec(TestCase):
- test_cold_demand = TestMultiCommodityHeatPump()
- test_cold_demand.test_air_to_water_heat_pump_elec_min_elec()
- # test_cold_demand.test_heat_pump_elec_min_elec()
+ def test_geothermal_source(self):
+ """
+ This tests the electric behavior of the regular geothermal source asset.
+
+ It first does the standard checks, and then the equations that are added to the geothermal
+ asset, including the electricity power consumption.
+
+ """
+ import models.source_pipe_sink.src.double_pipe_heat as example
+ from models.source_pipe_sink.src.double_pipe_heat import SourcePipeSink
+
+ base_folder = Path(example.__file__).resolve().parent.parent
+
+ heat_problem = run_esdl_mesido_optimization(
+ SourcePipeSink,
+ base_folder=base_folder,
+ esdl_file_name="sourcesink_with_geo.esdl",
+ esdl_parser=ESDLFileParser,
+ profile_reader=ProfileReaderFromFile,
+ input_timeseries_file="timeseries_import.csv",
+ )
+ results = heat_problem.extract_results()
+ parameters = heat_problem.parameters(0)
+
+ # Standard checks.
+ demand_matching_test(heat_problem, results)
+ energy_conservation_test(heat_problem, results)
+ heat_to_discharge_test(heat_problem, results)
+ electric_power_conservation_test(heat_problem, results)
+
+ # Equations check
+ np.testing.assert_allclose(
+ parameters["GeothermalSource_a77b.cop"] * results["GeothermalSource_a77b.Power_elec"],
+ results["GeothermalSource_a77b.Heat_source"],
+ )
+
+ # Variable operational cost check.
+ np.testing.assert_allclose(
+ parameters["GeothermalSource_a77b.variable_operational_cost_coefficient"]
+ * sum(results["GeothermalSource_a77b.Heat_source"][1:])
+ / parameters["GeothermalSource_a77b.cop"],
+ results["GeothermalSource_a77b__variable_operational_cost"],
+ )
+
+ def test_geothermal_source_elec(self):
+ """
+ This tests checks the electric geothermal producer asset with an electricity port.
+
+ It does all the same checks as with the regular geothermal asset, plus tests on the
+ electricity in port.
+ """
+ import models.source_pipe_sink.src.double_pipe_heat as example
+ from models.source_pipe_sink.src.double_pipe_heat import SourcePipeSink
+
+ base_folder = Path(example.__file__).resolve().parent.parent
+
+ heat_problem = run_esdl_mesido_optimization(
+ SourcePipeSink,
+ base_folder=base_folder,
+ esdl_file_name="sourcesink_with_geo_elec.esdl",
+ esdl_parser=ESDLFileParser,
+ profile_reader=ProfileReaderFromFile,
+ input_timeseries_file="timeseries_import.csv",
+ )
+ results = heat_problem.extract_results()
+ parameters = heat_problem.parameters(0)
+
+ # Standard checks.
+ demand_matching_test(heat_problem, results)
+ energy_conservation_test(heat_problem, results)
+ heat_to_discharge_test(heat_problem, results)
+ electric_power_conservation_test(heat_problem, results)
+
+ # Equations check
+ np.testing.assert_allclose(
+ results["ElectricityProducer_4dde.ElectricityOut.Power"],
+ results["GeothermalSource_a77b.ElectricityIn.Power"],
+ )
+ np.testing.assert_allclose(
+ parameters["GeothermalSource_a77b.cop"] * results["GeothermalSource_a77b.Power_elec"],
+ results["GeothermalSource_a77b.Heat_source"],
+ )
+
+ # Test electricity port
+ np.testing.assert_allclose(
+ results["GeothermalSource_a77b.ElectricityIn.Power"],
+ results["GeothermalSource_a77b.Power_elec"],
+ )
+
+ # Variable operational cost check.
+ np.testing.assert_allclose(
+ parameters["GeothermalSource_a77b.variable_operational_cost_coefficient"]
+ * sum(results["GeothermalSource_a77b.Heat_source"][1:])
+ / parameters["GeothermalSource_a77b.cop"],
+ results["GeothermalSource_a77b__variable_operational_cost"],
+ )
+
+ def test_geothermal_source_elec_no_cop(self):
+ """
+ This tests checks the electric geothermal producer asset when no cop is provided.
+
+ It is the same test case as with the regular one, but in this case, the cop is not
+ provided and it assigned a value of 0.0, resulting in no power related costs.
+ """
+ import models.source_pipe_sink.src.double_pipe_heat as example
+ from models.source_pipe_sink.src.double_pipe_heat import SourcePipeSink
+
+ base_folder = Path(example.__file__).resolve().parent.parent
+
+ heat_problem = run_esdl_mesido_optimization(
+ SourcePipeSink,
+ base_folder=base_folder,
+ esdl_file_name="sourcesink_with_geo_elec_no_cop.esdl",
+ esdl_parser=ESDLFileParser,
+ profile_reader=ProfileReaderFromFile,
+ input_timeseries_file="timeseries_import.csv",
+ )
+ results = heat_problem.extract_results()
+
+ # Standard checks.
+ demand_matching_test(heat_problem, results)
+ energy_conservation_test(heat_problem, results)
+ heat_to_discharge_test(heat_problem, results)
+ electric_power_conservation_test(heat_problem, results)
+
+ # Equations check
+ np.testing.assert_allclose(
+ results["ElectricityProducer_4dde.ElectricityOut.Power"],
+ results["GeothermalSource_a77b.ElectricityIn.Power"],
+ )
+ np.testing.assert_allclose(results["ElectricityProducer_4dde.ElectricityOut.Power"], 0.0)
+
+ # Test electricity port
+ np.testing.assert_allclose(
+ results["GeothermalSource_a77b.ElectricityIn.Power"],
+ results["GeothermalSource_a77b.Power_elec"],
+ )
+
+ # Variable operational cost check.
+ np.testing.assert_allclose(0.0, results["GeothermalSource_a77b__variable_operational_cost"])
diff --git a/tests/utils_tests.py b/tests/utils_tests.py
index 3946b4b8..d50ba5b6 100644
--- a/tests/utils_tests.py
+++ b/tests/utils_tests.py
@@ -374,6 +374,7 @@ def electric_power_conservation_test(solution, results, atol=1e-2):
"elec_heat_source_elec",
"heat_pump_elec",
"air_water_heat_pump_elec",
+ "geothermal_source_elec",
]
)
producers = solution.energy_system_components_get(["electricity_source"])