Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions tests/models/wko/input/timeseries_high_cold_demand.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DateTime,HeatingDemand_9b90,CoolingDemand_15e8
01-01-2019 00:00, 50000., 150000.
01-01-2019 01:00, 100000., 150000.
01-01-2019 02:00, 1200000., 150000.
01-01-2019 03:00, 2000000., 2000000.
01-01-2019 04:00, 150000., 1500000.
01-01-2019 05:00, 150000., 1000000.
01-01-2019 06:00, 150000., 500000.

12 changes: 10 additions & 2 deletions tests/models/wko/model/airco.esdl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<esdl:EnergySystem xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:esdl="http://www.tno.nl/esdl" id="03386224-685c-4f3b-bcc0-3c9dc63110de_with_return_network_with_return_network" description="" esdlVersion="v2401" name="airco_voc" version="5">
<esdl:EnergySystem xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:esdl="http://www.tno.nl/esdl" id="03386224-685c-4f3b-bcc0-3c9dc63110de_with_return_network_with_return_network" description="" esdlVersion="v2401" name="airco" version="8">
<energySystemInformation xsi:type="esdl:EnergySystemInformation" id="ad1a8ca8-b21b-4ad3-aa3e-756cf321bd4d">
<carriers xsi:type="esdl:Carriers" id="553fa302-8878-4fa9-b0ca-6ee6123bf2c9">
<carrier xsi:type="esdl:HeatCommodity" name="LT" supplyTemperature="25.0" id="13db0822-98da-4ca8-9d48-868653ae06af"/>
Expand Down Expand Up @@ -271,10 +271,18 @@
<point xsi:type="esdl:Point" lon="4.418658434678616" lat="52.079938869403605" CRS="WGS84"/>
</geometry>
</asset>
<asset xsi:type="esdl:Airco" name="Airco_23d6" power="1000000.0" id="23d6ef31-45e6-46e0-b669-edc306e2ca9c">
<asset xsi:type="esdl:Airco" name="Airco_23d6" power="1000000.0" id="23d6ef31-45e6-46e0-b669-edc306e2ca9c" state="OPTIONAL">
<port xsi:type="esdl:InPort" name="In" id="733e8687-330f-46db-8879-9020b7b80a67" connectedTo="751dd976-7e12-4363-aea2-f2e068e306ac" carrier="13db0822-98da-4ca8-9d48-868653ae06af"/>
<port xsi:type="esdl:OutPort" name="Out" id="30531768-8713-4d8c-9c5c-29a89e239eb9" connectedTo="a564025e-1ec7-4091-8e1f-6711a8ab8eb1" carrier="13db0822-98da-4ca8-9d48-868653ae06af_ret"/>
<geometry xsi:type="esdl:Point" lon="4.419829845428468" lat="52.07849717267527" CRS="WGS84"/>
<costInformation xsi:type="esdl:CostInformation" id="245e6b56-c37e-4c55-9f7e-52d4603d4277">
<variableOperationalCosts xsi:type="esdl:SingleValue" id="a44957ec-6a01-4145-90f9-c1aac617b2ad" value="100.0">
<profileQuantityAndUnit xsi:type="esdl:QuantityAndUnitType" physicalQuantity="COST" description="Cost in EUR/kWh" id="ffdb790d-f225-48b9-b4e7-f588612b418f" unit="EURO" perMultiplier="KILO" perUnit="WATTHOUR"/>
</variableOperationalCosts>
<investmentCosts xsi:type="esdl:SingleValue" id="1ff41860-e95a-441f-83e9-b3b52b07bc11" value="10000.0">
<profileQuantityAndUnit xsi:type="esdl:QuantityAndUnitType" physicalQuantity="COST" description="Cost in EUR/MW" id="7c08a501-8656-435a-aafb-24136ab4ef4c" unit="EURO" perMultiplier="MEGA" perUnit="WATT"/>
</investmentCosts>
</costInformation>
</asset>
<asset xsi:type="esdl:Pipe" innerDiameter="0.3127" name="Pipe_1e91" length="129.8" outerDiameter="0.45" id="1e91bc33-c10f-42e3-b49a-058fad456ec3" diameter="DN300">
<costInformation xsi:type="esdl:CostInformation" id="684fb7e9-00c8-42a9-a708-06bb6b329153">
Expand Down
75 changes: 72 additions & 3 deletions tests/models/wko/src/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class MinimizeSourcesHeatGoal(Goal):
over the full horizon and not per time-step.
"""

priority = 3
priority = 2

order = 1

Expand Down Expand Up @@ -113,6 +113,48 @@ def function(
return optimization_problem.state(f"{self.source}.Heat_source")


class MinimizeInvestmentCost(Goal):
"""
Goal that minimizes the investment cost for the asset.

The objective is computed as the asset's investment cost coefficient
multiplied by its maximum installed size.
"""

priority = 3
order = 1

def __init__(self, source):
"""
The constructor of the goal.

Parameters
----------
source : string of the source name that is going to be minimized
"""

self.source = source
self.target_max = 0.0
self.function_range = (0.0, 2.0 * 1e6)
self.function_nominal = 1e6

def function(self, optimization_problem, ensemble_member):
"""
Compute the investment cost as objective.

Parameters
----------
optimization_problem : The optimization class containing the variables'.
ensemble_member : the ensemble member.

Returns
-------
Investment cost objective value.
"""

return optimization_problem.extra_variable(f"{self.source}__investment_cost")


class _GoalsAndOptions:
"""
A goals class that we often use if we specify multiple problem classes.
Expand Down Expand Up @@ -143,7 +185,7 @@ def path_goals(self):
return goals


class HeatProblem(
class HeatColdProblem(
_GoalsAndOptions,
TechnoEconomicMixin,
LinearizedOrderGoalProgrammingMixin,
Expand Down Expand Up @@ -225,9 +267,36 @@ def energy_system_options(self):
return options


class HeatColdProblemSizing(HeatColdProblem):
"""
Heat–cold optimization problem variant that includes asset sizing.

This class extends the base HeatColdProblem by adding an investment
cost minimization goal for selected assets.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def goals(self):
Comment thread
KobusVanRooyen marked this conversation as resolved.
"""
Add goal to minimize the investment cost of selected asset

Returns
-------
Extended goals list.
"""

goals = super().goals().copy()
for source in self.energy_system_components["airco"]:
goals.append(MinimizeInvestmentCost(source=source))

return goals


if __name__ == "__main__":
elect = run_optimization_problem(
HeatProblem,
HeatColdProblem,
esdl_file_name="airco.esdl",
esdl_parser=ESDLFileParser,
profile_reader=ProfileReaderFromFile,
Expand Down
57 changes: 41 additions & 16 deletions tests/test_cold_demand.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_insufficient_capacity(self):

"""
import models.wko.src.example as example
from models.wko.src.example import HeatProblem
from models.wko.src.example import HeatColdProblem

logger, logs_list = create_log_list_scaling("mesido")

Expand All @@ -50,7 +50,7 @@ def test_insufficient_capacity(self):
unittest.mock.patch("mesido.potential_errors.POTENTIAL_ERRORS", PotentialErrors()),
):
_ = run_esdl_mesido_optimization(
HeatProblem,
HeatColdProblem,
base_folder=base_folder,
esdl_file_name="LT_wko_error_check.esdl",
esdl_parser=ESDLFileParser,
Expand Down Expand Up @@ -83,12 +83,12 @@ def test_cold_demand(self):

"""
import models.wko.src.example as example
from models.wko.src.example import HeatProblem
from models.wko.src.example import HeatColdProblem

base_folder = Path(example.__file__).resolve().parent.parent

heat_problem = run_esdl_mesido_optimization(
HeatProblem,
HeatColdProblem,
base_folder=base_folder,
esdl_file_name="LT_wko.esdl",
esdl_parser=ESDLFileParser,
Expand All @@ -106,44 +106,69 @@ def test_airco(self):
This test is to check the basic physics for a network which includes an airco. In this
case we have a network with an air-water hp, a low temperature ates and both hot and cold
demand. In this case the demands are matched and the low temperature ates is utilized.
Only airco is optional. Airco sizing is done by minimization of airco investment cost

Checks:
1. demand is matched
2. energy conservation in the network
3. heat to discharge
4. variable operational and investment cost calculations
5. airco sizing
Comment thread
KobusVanRooyen marked this conversation as resolved.

"""
import models.wko.src.example as example
from models.wko.src.example import HeatProblem
from models.wko.src.example import HeatColdProblemSizing

base_folder = Path(example.__file__).resolve().parent.parent

heat_problem = run_esdl_mesido_optimization(
HeatProblem,
HeatColdProblemSizing,
base_folder=base_folder,
esdl_file_name="airco.esdl",
esdl_parser=ESDLFileParser,
profile_reader=ProfileReaderFromFile,
input_timeseries_file="timeseries.csv",
input_timeseries_file="timeseries_high_cold_demand.csv",
)
results = heat_problem.extract_results()
parameters = heat_problem.parameters(0)
name_to_id_map = heat_problem.esdl_asset_name_to_id_map

hp_id = name_to_id_map["HeatPump_b97e"]
ac_id = name_to_id_map["Airco_23d6"]

demand_matching_test(heat_problem, results)
energy_conservation_test(heat_problem, results)
heat_to_discharge_test(heat_problem, results)

# Check how variable operation cost is calculated
# Check variable operation cost calculation
np.testing.assert_array_less(1e3, results[f"{hp_id}__variable_operational_cost"])
np.testing.assert_allclose(
parameters[f"{hp_id}.variable_operational_cost_coefficient"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the heat_source var_opx is now 0, and initially this value was tested and it was not 0. Please reovle this so that the var_opex is tested again and > 0

Copy link
Copy Markdown
Collaborator Author

@tolga-akan tolga-akan Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now i made changes in demand value in csv and cost coefficient of airco to ensure both heatpump and airco assets are used. Hence that both assets have non zero var-opex

* sum(results[f"{hp_id}.Heat_source"][1:])
/ parameters[f"{hp_id}.cop"],
results[f"{hp_id}__variable_operational_cost"],
)

np.testing.assert_array_less(1e3, results[f"{ac_id}__variable_operational_cost"])
np.testing.assert_allclose(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the variable operational cost = 0 due to an unintentional change by someone? Will this test still pass or fail? How can we update the test to cater for this?

Copy link
Copy Markdown
Collaborator Author

@tolga-akan tolga-akan Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable Operational cost has no influence in the problem. Problem objectives are DemandMatcing and MinimizeInvestmentCost. When I make var-opex coefficint zero, test still passes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please resolve as discussed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put additional check to ensure variable operational cost > 1e3

parameters[f"{ac_id}.variable_operational_cost_coefficient"]
* sum(results[f"{ac_id}.Heat_airco"][1:]),
results[f"{ac_id}__variable_operational_cost"],
)

# Check investment cost calculation
np.testing.assert_array_less(1e3, results[f"{ac_id}__investment_cost"])
np.testing.assert_allclose(
parameters[f"{ac_id}.investment_cost_coefficient"] * results[f"{ac_id}__max_size"],
results[f"{ac_id}__investment_cost"],
)

# Check airco sizing
np.testing.assert_allclose(
max(results[f"{ac_id}.Heat_airco"]),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the size = 0 due to an unintentional change by someone? Will this test still pass or fail? How can we update the test to cater for this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this problem do not consider variable operation cost in the objective. Because apart from demand matching objective we have MinimizeInvestmentCost. There we minize the size of asset. Hence, making var-opex coefficint zero will not fail the tests.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please resolve as discussed

Copy link
Copy Markdown
Collaborator Author

@tolga-akan tolga-akan Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put additional check to ensure investment cost > 1e3. Hence, asset size > 0

results[f"{ac_id}__max_size"],
)

def test_wko(self):
"""
This test is to check the basic physics for a network which includes cold demand. In this
Expand All @@ -168,13 +193,13 @@ def test_wko(self):
- pipe heat losses excluded: excpect no heat losses or gains
"""
import models.wko.src.example as example
from models.wko.src.example import HeatProblem
from models.wko.src.example import HeatColdProblem

base_folder = Path(example.__file__).resolve().parent.parent

# ------------------------------------------------------------------------------------------
# Pipe heat losses inlcuded
class HeatingCoolingProblem(HeatProblem):
class HeatingCoolingProblem(HeatColdProblem):

def energy_system_options(self):
options = super().energy_system_options()
Expand Down Expand Up @@ -300,11 +325,11 @@ def test_heat_cold_demand_peak_overlap(self):
both peaks are on the same day.
"""
import models.wko.src.example as example
from models.wko.src.example import HeatProblem
from models.wko.src.example import HeatColdProblem

base_folder = Path(example.__file__).resolve().parent.parent

class DiscretizationProblem(HeatProblem):
class DiscretizationProblem(HeatColdProblem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.day_steps = 1
Expand Down Expand Up @@ -373,11 +398,11 @@ def test_heat_cold_demand_peak_back_to_back(self):
both peaks are on consecutive days.
"""
import models.wko.src.example as example
from models.wko.src.example import HeatProblem
from models.wko.src.example import HeatColdProblem

base_folder = Path(example.__file__).resolve().parent.parent

class DiscretizationProblem(HeatProblem):
class DiscretizationProblem(HeatColdProblem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.day_steps = 1
Expand Down Expand Up @@ -446,11 +471,11 @@ def test_heat_cold_peak_before(self):
the cold peak happens before the heat one.
"""
import models.wko.src.example as example
from models.wko.src.example import HeatProblem
from models.wko.src.example import HeatColdProblem

base_folder = Path(example.__file__).resolve().parent.parent

class DiscretizationProblem(HeatProblem):
class DiscretizationProblem(HeatColdProblem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.day_steps = 1
Expand Down
Loading