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 variables

  • optional 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

mode

Optimization mode: 'hp', 'orc', or 'cb'

all

config

Configuration dictionary or path to a YAML configuration file

all

boundaries

Lower and upper bounds for all decision variables

all

same_fluid

Whether the same working fluid is used across HP and ORC

'cb'

heat_losses

Heat loss fraction of the system

'cb'

COP

Coefficient of performance

'orc'

q_dot

Heat input/output rate

'orc'

parallelization specific:

Parameter

Description

Default

n_processes

Number of worker processes. Auto-detected via default_n_processes() if None

None

maxtasksperchild

Maximum number of tasks a single worker process executes before being replaced

20

pymoo specific:

Parameter

Description

Default

pop_size

Population size

50

sampling_iterations

Number of iterations for Latin Hypercube Sampling (LHS) initialization

50

n_gen

Maximum number of generations

100

period

Number of generations over which convergence is checked

20

ftol

Function value tolerance for the convergence termination criterion

1e-4

verbose

Print optimization progress to stdout

True

n_ieq_constr

Number of inequality constraints passed to OptiProblem

3

return_least_infeasible

Return the least infeasible solution if no feasible solution is found

False

pymoo specific (DE):

Parameter

Description

Default

de_variant

DE mutation/crossover strategy, e.g. 'DE/rand/1/bin'

'DE/rand/1/bin'

CR

Crossover probability

0.3

dither

Dithering strategy for the scaling factor F: 'vector', 'scalar', or None

'vector'

jitter

Apply jitter to the scaling factor F

False

pymoo specific (NSGA-II):

Parameter

Description

Default

n_offspring

Number of offspring per generation. Uses pymoo default if None

None

crowding_func

Crowding distance function for survival selection, e.g. 'pcd'

'pcd'

eliminate_duplicates

Remove duplicate individuals from the population

True

save_history

Save the per-generation optimization history to the result object

False

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.