Optimization
carbatpy supports optimization workflows for steady-state cycle models (Heat pump, ORC,
Carnot battery) based on the current comp infrastructure.
Two optimization approaches are supported: a single-objective and a multi-objective optimization.
Single-objective optimization can be performed using scipy.optimize as well as a differential evolution algorithm provided by the pymoo framework.
Multi-objective optimization is currently available only for the Carnot Battery as a whole, using the NSGA-II algorithm from the pymoo framework.
Typical optimization variables
Examples of objective variables are:
costs: by default the capital expenditures (CAPEX)
performance: by default the Round-trip efficiency (RTE)
Examples of optimization variables are:
working fluid mixture composition (molar fractions)
pressure levels (e.g.
p_high,p_low)component parameters (e.g. superheat/minimum approach temperatures)
storage boundary conditions (e.g. cold tank temperature level)
Optimization using scipy
Fixed parameters vs. optimization variables
In the current workflow, most cycle parameters are read from a YAML configuration file.
The optimization scripts then provide a partial configuration (conf_m) and
corresponding bounds (bounds_m) for the variables that shall be optimized.
Examples (SciPy single-objective)
Heat pump optimization (opti_hp_comp.py)
Download:
opti_hp_comp.py
YAML configuration used by this example:
io-hp-data.yaml
This example performs a baseline run of the heat pump model, and then optimizes selected
variables (e.g. mixture fractions, pressure levels, cold storage temperature) using
scipy.optimize via helper functions in cb.opti_cycle_comp_helpers.
# -*- coding: utf-8 -*-
"""
Example for Optimization of a heat pump (heat_pump_comp) using scipy.optimize
Composition and high pressure (condenser) of the working fluid, and the low
temperature of the double tank cold storage are optimization variables. All
other parameters are fixed in a yaml file (io-cycle-data.yaml).
Created on Mon Aug 4 13:12:24 2025
@author: atakan
Universität Duisburg-Essen, Germany
In the framework of the Priority Programme: "Carnot Batteries: Inverse Design from
Markets to Molecules" (SPP 2403)
https://www.uni-due.de/spp2403/
https://git.uni-due.de/spp-2403/residuals_weather_storage
"""
import numpy as np
import pandas as pd
import carbatpy as cb
import datetime
from pathlib import Path
import shutil
import yaml
import pickle
# Konfiguration und Konstanten (diese können außerhalb des main-Blocks stehen)
current_date = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-")
OPTI = True
HOW_OPT = "dif_evol" # "local" #
STORE_FILENAME = (
cb.CB_DEFAULTS["General"]["RES_DIR"] + "\\" + current_date + "hp_opt_results"
)
DIR = cb.CB_DEFAULTS["General"]["CB_DATA"]
file_config = DIR + "\\io-hp-data.yaml"
# Ordner erstellen und YAML schreiben
dir_res = Path(STORE_FILENAME)
dir_res.mkdir(exist_ok=True)
datei = dir_res / "config_bound.yaml"
res_dat = dir_res / "opt_res.yaml"
file_res = dir_res / (current_date + "hp_opti_res.csv")
# optimization variables, must be present in the file above
conf_m = {
"cold_storage": {"temp_low": 270.0},
"compressor": {"dt_superheat": 5},
"working_fluid": {"p_high": 1.65e6, "p_low": 1.67e5, "fractions": [0.80, 0.2]},
}
# bounds of the optimization variables
bounds_m = {
"cold_storage": {"temp_low": [265, 277]},
"compressor": {"dt_superheat": [3, 20]},
"working_fluid": {
"p_high": [5e5, 2.1e6],
"p_low": [1e5, 4e5],
"fractions": [[0.7, 1.0], [0.0, 0.4]],
},
}
# Run heat pump without optimization, with the configuration conf_m:
if __name__ == "__main__":
res_m = cb.hp_comp.heat_pump(
file_config, config=conf_m, verbose=True, plotting=True
)
if any(ns.value != 0 for ns in res_m["warnings"].values()):
print(f"Check Warnings, at least one deviates from 0!\n {res_m['warnings']}")
if OPTI:
# for optimization:
opt_res, paths = cb.opti_cycle_comp_helpers.optimize_wf_heat_pump(
file_config,
conf_m,
bounds_m,
optimize_global=HOW_OPT,
workers=1,
maxiter=50,
verbose=True,
)
print(opt_res)
with open(dir_res / "paths.pkl", "wb") as pf:
pickle.dump(paths, pf)
co_n = cb.opti_cycle_comp_helpers.insert_optim_data(conf_m, opt_res.x, paths)
res_ = cb.hp_comp.heat_pump(file_config, config=co_n, plotting=True)
col_names = cb.opti_cycle_comp_helpers.extract_cycle_column_names_from_config(
res_["output"]["config"], paths
) + ["COP"]
file_fig = dir_res / (current_date + "_hp_opti_res.png")
res_["figure"].savefig(file_fig)
if HOW_OPT == "dif_evol":
res_combi = np.column_stack(
[opt_res.population, opt_res.population_energies]
)
df = pd.DataFrame(res_combi, columns=col_names)
df.to_csv(file_res, sep=",", index=False)
res_dict = {
"x": (opt_res.x).tolist(),
"fun": float(opt_res.fun),
"success": opt_res.success,
"message": opt_res.message,
"results": dict(zip(col_names, (opt_res.x).tolist())),
}
with res_dat.open("w") as f:
yaml.dump(res_dict, f, default_flow_style=False, sort_keys=False)
ORC optimization (opti_orc_comp.py)
Download:
opti_orc_comp.py
YAML configuration used by this example:
io-orc-data.yaml
# -*- coding: utf-8 -*-
"""
Example for Optimization of a heat pump (heat_pump_comp) using scipy.optimize
Composition and high pressure (condenser) of the working fluid, and the low
temperature of the double tank cold storage are optimization variables. All
other parameters are fixed in a yaml file (io-cycle-data.yaml).
Created on Mon Aug 4 13:12:24 2025
@author: atakan
Universität Duisburg-Essen, Germany
In the framework of the Priority Programme: "Carnot Batteries: Inverse Design from
Markets to Molecules" (SPP 2403)
https://www.uni-due.de/spp2403/
https://git.uni-due.de/spp-2403/residuals_weather_storage
"""
import numpy as np
import pandas as pd
import carbatpy as cb
OPTI = True
HOW_OPT = "dif_evol"
STORE_FILENAME = None
COP = 3.05
Q_DOT_HIGH = 3000.
WITH_COMPOSITION = True
dir_name_out = cb.CB_DEFAULTS["General"]["CB_DATA"]+"\\io-orc-data.yaml"
# optimization variables, must be present in the file above
if WITH_COMPOSITION:
conf_m = {"cold_storage": {"temp_low": 254.},
"working_fluid": {"p_high": .49e6,
'fractions': [.74, .0, 0.26, 0.0000]}}
# bounds of the optimization variables
bounds_m = {"cold_storage": {"temp_low": [255, 277]},
"working_fluid":
{"p_high": [4e5, 0.6e6],
'fractions': [[0.0, .85], [0.0, .005], [0, 0.5], [0, 0.5]]},
}
else:
conf_m = {"cold_storage": {"temp_low": 254.},
"working_fluid": {"p_high": .58e6,
'p_low': 3.5e5}}
# bounds of the optimization variables
bounds_m = {"cold_storage": {"temp_low": [250, 277]},
"working_fluid":
{"p_high": [4.1e5, 1.1e6],
'p_low': [3.0e5, 3.9e5]},
}
# Run heat pump without optimization, with the configuration conf_m:
res_m = cb.orc_comp.orc(
dir_name_out, COP, Q_DOT_HIGH, config=conf_m, verbose=True, plotting=True)
if any(ns.value != 0 for ns in res_m['warnings'].values()):
print(f"Check Warnings, at least one deviates from 0!\n {res_m['warnings']}")
if __name__ == '__main__':
if OPTI:
# for optimization:
print('\nOptimization is running ...\n')
opt_res, paths = cb.opti_cycle_comp_helpers.optimize_orc(
dir_name_out, COP, Q_DOT_HIGH,
conf_m, bounds_m,
optimize_global=HOW_OPT,
verbose=True,
maxiter =1)
print(opt_res)
if HOW_OPT == "dif_evol": # or: "dif_evol", "bas_hop"
df = pd.DataFrame(opt_res.population)
df["eta-weighted"] = opt_res.population_energies
p_l = []
c6 = []
p_ratio = []
etas = []
for o_val in opt_res.population:
try:
conf_o = cb.opti_cycle_comp_helpers.insert_optim_data(
conf_m, o_val, paths)
# conf_o = {"working_fluid": {"p_high": o_val[0], 'fractions': [
# *o_val[1:], 1 - np.sum(o_val[1:])]}}
res_o = cb.orc_comp.orc(
dir_name_out, COP,
Q_DOT_HIGH,
config=conf_o,
verbose=True,
plotting=True)
p_l_opt = res_o['output']['start']['p_low']
p_h_opt = conf_o["working_fluid"]["p_high"]
p_l.append(p_l_opt)
c6.append(1-np.sum(o_val[1:]))
p_ratio.append(p_h_opt / p_l_opt)
etas.append(res_o["eta_th"])
except Exception as e:
print("Error in ORC-Opti:", type(e), e)
p_ratio.append(-10)
etas.append(-10)
#df["hexane"] = c6 # name for this input file
#df["p_low"] = p_l
df["p_ratio"] = p_ratio
df['eta_th'] = res_o["eta_th"]
if STORE_FILENAME is not None:
df.to_csv(
STORE_FILENAME, # should be '.csv'
index=False)
else:
o_val = opt_res.x
conf_o = cb.opti_cycle_comp_helpers.insert_optim_data(
conf_m, o_val, paths)
res_o = cb.orc_comp.orc(
dir_name_out, COP, Q_DOT_HIGH, config=conf_o, verbose=True, plotting=True)
print(f"eta_th-Optimized by {HOW_OPT}: {res_o['eta']:.2f}")
Carnot battery optimization (opti_cb_comp.py)
Download:
opti_cb_comp.py
YAML configurations used by this example (charge/discharge models):
io-hp-data.yaml
io-orc-data.yaml
# -*- coding: utf-8 -*-
"""
Example for Optimization of a Carnot battery, with a heat pump (heat_pump_comp)
for charging and an ORC for discharging using scipy.optimize
Composition and high pressure (condenser) of the working fluid, and the low
temperature of the double tank cold storage are optimization variables for the heat pump. All
other parameters are fixed in a yaml file (io-cycle-data.yaml). For the ORC, the same
composition is used and only the two working fluid pressure levels are optimized.
Created on Mon Aug 4 13:12:24 2025
@author: atakan
Universität Duisburg-Essen, Germany
In the framework of the Priority Programme: "Carnot Batteries: Inverse Design from
Markets to Molecules" (SPP 2403)
https://www.uni-due.de/spp2403/
https://git.uni-due.de/spp-2403/residuals_weather_storage
"""
import numpy as np
import pandas as pd
import carbatpy as cb
import datetime
from pathlib import Path
import shutil
import yaml
import pickle
# Konfiguration und Konstanten (diese können außerhalb des main-Blocks stehen)
OPTI = True
HOW_OPT = "bas_hop" #'shgo' # "dif_evol" # "local" #
current_date = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-")
STORE_FILENAME = cb.CB_DEFAULTS["General"]["RES_DIR"] +"\\"+ current_date + "cb_opt_result"
POWER_C = 2000. # compressr power of heat pump
DIR = cb.CB_DEFAULTS["General"]["CB_DATA"]
# Ordner erstellen und YAML schreiben
dir_res = Path(STORE_FILENAME)
dir_res.mkdir(exist_ok=True)
datei = dir_res / 'config_bound.yaml'
res_dat = dir_res / 'opt_res.yaml'
file_res = dir_res / (current_date + "cb_opti_res.csv")
dir_names_both = {"hp": DIR+"\\io-hp-data.yaml",
"orc": DIR+"\\io-orc-data.yaml"}
for io_file in dir_names_both.values():
shutil.copy2(io_file, dir_res)
# optimization variables, must be present in the file above
conf_hp = {"cold_storage": {"temp_low": 265.},
"working_fluid": {"p_high": 1.45e6,
"p_low": 1.8e5,
'fractions': [.438, .4310, 0.131, 0.0000],
}
}
conf_orc = {"working_fluid": {"p_high": 4.71e5,
"p_low": 2.21e5,
}
}
# bounds of the optimization variables
bounds_hp = {"cold_storage": {"temp_low": [265, 277]},
"working_fluid":
{"p_high": [8.e5, 01.9e6],
"p_low":[1e5, 2.9e5],
'fractions': [[0.3, .85], [0.0, .7], [0.0, 0.3], [0, 0.25]]},
}
bounds_orc = {"working_fluid":
{"p_high": [4e5, 0.82e6],
"p_low":[2.10e5, 3.9e5],},
}
configs_m ={"hp" : conf_hp,
"orc": conf_orc}
bounds_m = {"hp" : bounds_hp,
"orc" : bounds_orc}
with datei.open('w') as f:
yaml.dump({"configs":configs_m,
"bounds":bounds_m,
'power_compressor': POWER_C}, f, default_flow_style=False,
sort_keys=False)
# WICHTIG: Der ausführbare Code muss in diesem Block stehen!
if __name__ == "__main__":
if OPTI:
# for optimization:
print("Carnot battery optimization is running. It may take a while!")
opt_res, paths = cb.opti_cycle_comp_helpers.optimize_cb(
dir_names_both, POWER_C, configs_m, bounds_m,
optimize_global=HOW_OPT,
workers=1,
maxiter=1,
#opt_opt = -.03,
)
print(opt_res)
with open(dir_res / "paths.pkl", "wb") as pf:
pickle.dump(paths, pf)
co_n = cb.opti_cycle_comp_helpers.extract_cb_conf_from_x(opt_res.x,
configs_m,
paths)
rte, res_ = cb.cb_comp.cb_calc(dir_names_both, POWER_C, config=co_n, plotting=True)
col_names = cb.opti_cycle_comp_helpers.extract_column_names_from_config(res_["hp"]['output']["config"], paths) + ["rte"]
for key in res_.keys(): # save plots
file_fig = dir_res / (current_date + key+"_cb_opti_res.png")
res_[key]["figure"].savefig(file_fig)
if HOW_OPT== 'dif_evol':
res_combi = np.column_stack([opt_res.population, opt_res.population_energies])
df = pd.DataFrame(res_combi, columns=col_names)
df.to_csv(file_res, sep=",", index=False)
else:
res_dict ={'x': (opt_res.x).tolist(),
'fun':float(opt_res.fun),
'success':opt_res.success,
'message':opt_res.message,
'results':dict(zip(col_names, (opt_res.x).tolist()))}
with res_dat.open('w') as f:
yaml.dump(res_dict, f, default_flow_style=False,
sort_keys=False)
Outputs / Results
The optimization examples typically write:
an output folder under the configured results directory (timestamped)
a YAML file with the best point (
x), objective value (fun), and a mapping to named variablesoptional CSV data (e.g. population + energies for differential evolution)
optional figures from the best design point
Optimization using pymoo
The pymoo-based optimization provides a unified interface for both single-objective and multi-objective optimization of implemented thermodynamic cycles (Heat pump, ORC, Carnot battery). It is build on the pymoo framework and includes cycle-specific problem definitions as well as customizable parallelization parameters.
Architecture
The optimization is structured into two layers:
1. Core helpers (helpers_optimization.py)
Located in src/carbatpy/optimizations/helpers_optimizations.py, this module contains all building blocks shared across optimization algorithms.
opti_func Wraps the simulation of a Heat pump, ORC, or Carnot battery.
It takes a vector of decision variables, maps them to the corresponding cycle parameters,
runs the steady-state model, and returns the objective value(s) together with any constraint violations.
OptiProblem is a pymoo-compatible problem class.
It is responsible for the evaluation logic by calling opti_func, constraint handling and deciding the number of objective values.
The class supports both single-objective (n_obj=1) and multi-objective (n_obj=2) formulations.
CombinedTermination is a termination class that combines multiple stopping criteria.
Supported criteria include the maximum number of generation and the objective variable convergence.
2. Algorithm scripts
The individual scripts for the algorithms (opti_de and opti_NSGA2) then use OptiProblem and CombinedTermination for the calculation in optimize.
This function can be called by the user to perform any optimization.
The optimize function accepts a range of parameters that can be grouped into three categories:
carbatpy specific:
Parameter |
Description |
Relevant mode |
|---|---|---|
|
Optimization mode: |
all |
|
Configuration dictionary or path to a YAML configuration file |
all |
|
Lower and upper bounds for all decision variables |
all |
|
Whether the same working fluid is used across HP and ORC |
|
|
Heat loss fraction of the system |
|
|
Coefficient of performance |
|
|
Heat input/output rate |
|
parallelization specific:
Parameter |
Description |
Default |
|---|---|---|
|
Number of worker processes. Auto-detected via |
|
|
Maximum number of tasks a single worker process executes before being replaced |
|
pymoo specific:
Parameter |
Description |
Default |
|---|---|---|
|
Population size |
|
|
Number of iterations for Latin Hypercube Sampling (LHS) initialization |
|
|
Maximum number of generations |
|
|
Number of generations over which convergence is checked |
|
|
Function value tolerance for the convergence termination criterion |
|
|
Print optimization progress to stdout |
|
|
Number of inequality constraints passed to |
|
|
Return the least infeasible solution if no feasible solution is found |
|
pymoo specific (DE):
Parameter |
Description |
Default |
|---|---|---|
|
DE mutation/crossover strategy, e.g. |
|
|
Crossover probability |
|
|
Dithering strategy for the scaling factor F: |
|
|
Apply jitter to the scaling factor F |
|
pymoo specific (NSGA-II):
Parameter |
Description |
Default |
|---|---|---|
|
Number of offspring per generation. Uses pymoo default if |
|
|
Crowding distance function for survival selection, e.g. |
|
|
Remove duplicate individuals from the population |
|
|
Save the per-generation optimization history to the result object |
|
Further information on the functions and classes are provided in the API Reference and the following examples.
Single-objective
The single-objective optimization uses the differential evolution algorithm from pymoo.
For further information, see the Pymoo DE documentation.
Problem formulation
Objective
By default, the RTE, COP and the thermal efficiency are maximized (internally passed as a negative value since pymoo minimizes by default).
Constraints
The following constraints are enforced by default:
Carnot Battery: pressure in the heat pump ≥ 1 bar; RTE ≥ 0.05
Heat Pump: pressure ≥ 1 bar; COP ≥ 0.05
ORC: pressure ≥ 1 bar; Thermal efficiency ≥ 0.05
Decision variables
Decision variables are defined by the user through lower and upper bounds in the form of a dictionary corresponding to the parameter definition in the config.
Example for a Carnot battery
Multi-objective
The multi-objective optimization uses the NSGA-II (Non-dominated Sorting Genetic Algorithm II) from pymoo. For further information, see the Pymoo NSGA2 documentation. Multi-objective optimization is currently available for the Carnot battery model only.
Problem formulation
Objectives
By default, the following two objective variables are optimized:
RTE
CAPEX
The result is a Pareto-front — a set of non-dominated solutions representing the trade-off between the two objectives.
Constraints
The same default constraints as in the single-objective case apply (minimum pressures, minimum RTE).
Decision variables
Decision variables are defined by the user through lower and upper bounds in the form of a dictionary corresponding to the parameter definition in the config
and including the thermodynamic cycles hp and orc as the first keys.
Example for a Carnot battery
Outputs / Results
The optimization function returns the pymoo results object. For further information on attributes see the Pymoo Results documentation.