Critical Heat Flux (CHF) Prediction

Inputs

  • D (m): Diameter of the test section (\(0.002 - 0.016~m\))

  • L (m): Heated length (\(0.07 - 15.0~m\))

  • P (kPa): Pressure (\(100 - 20,000~kPa\))

  • G (kg m-2s-1): Mass flux (\(17.7 - 7712.0~\frac{kg}{m^2\cdot s}\))

  • Tin (C): Inlet temperature length (\(9.0 - 353.62 ^\circ C\))

  • Xe (-): Outlet equilibrium quality (\(-0.445 - 0.986\))

Output

  • CHF (kW m-2): Critical heat flux (\(130.0 - 13345.0~\frac{kW}{m^2}\))

The CHF dataset was taken from the Nuclear Regulatory Commission CHF database. This dataset was generated from vertical water-cooled uniformly heated tubes, producing 24,579 samples from 59 different sources of measurements. The dataset was collected over experimental measurements spanning 60 years of CHF data collection methods such as visual identification, physical burnout, changes in the test section resistances, and the usage of thermocouples. The parameters collected consist of pressure (\(P\)), test section diameter (\(D\)), heated length (\(L\)), mass flux (\(G\)), inlet temperature (\(T_{in}\)), outlet equilibrium quality (\(X\)), and \(CHF\).

Negative quality \(X\) could represent a subcooled fluid. The database was limited in terms of diameter (2 \(<\) D \(<\) 25 mm), \(L/D\) ratio (\(L/D\) \(>\) 50 for X \(>\) 0, \(L/D\) \(>\) 25 for X \(<\) 0), pressure (100 \(\leq\) P \(\leq\) 21,000 kPa) and mass flux (0 \(\leq\) G \(<\) 8,000 kg/m2/s). The measured data was also not equally distributed over the whole span, and no data beyond \(D\) = 16 mm was found in the database. Our goal is to identify optimal ML models with pyMIASE that can predict CHF accurately given other geometrical parameters and boundary conditions (:math:`D, L, P, G, T_{in}, X`).

Important Note: This dataset is confidential, it is not open-sourced with pyMAISE and can be requested directly from the OECD Nuclear Energy Agency (NEA). In this notebook, we provide 2500 synthetic samples that mimic the real ones (split into 2000 train and 500 test samples). The synthetic samples were generated by adding random noise to the real experiments. The user only needs to replace the synthetic csv files below with the ones from NEA. More information about the benchmark can be found at this link: https://www.oecd-nea.org/jcms/pl_89619/benchmark-on-artificial-intelligence-and-machine-learning-for-scientific-computing-in-nuclear-engineering-phase-1-critical-heat-flux-exercise-specifications?details=true

image.png

[1]:
# Import other packages
import time
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
from scipy.stats import uniform, randint
from sklearn.preprocessing import MinMaxScaler

# pyMAISE specific imports
from pyMAISE.datasets import load_chf
from pyMAISE.preprocessing import correlation_matrix, scale_data, read_csv
import pyMAISE as mai

# Plot settings
matplotlib_settings = {
    "font.size": 12,
    "legend.fontsize": 12,
    "figure.figsize": (8, 8)
}
plt.rcParams.update(**matplotlib_settings)

Initialization and Preprocessing

We define a regression problem and load the data using pyMAISE.datasets.load_chf. Here, we set cuda_visible_devices="-1" to define a CPU-only job as this dataset is reasonably small.

[2]:
# Initialize pyMAISE settings
global_settings = mai.init(
    problem_type=mai.ProblemType.REGRESSION,   # Define a regression problem
    cuda_visible_devices="-1"                  # Use CPU only
)

# Load data
train_data, xtrain, ytrain, test_data, xtest, ytest = load_chf()

The CHF dataset has six inputs listed below with 2500 total samples, 2000 of which are for training.

[3]:
xtrain
[3]:
<xarray.DataArray (index: 2000, variable: 6)>
array([[1.01591633e-02, 2.98755621e+00, 1.99910352e+04, 5.20754066e+02,
        2.60238993e+02, 3.36618827e-01],
       [9.61641659e-03, 5.48464942e+00, 7.07735164e+03, 2.74115374e+03,
        2.31174290e+02, 4.12976136e-01],
       [7.58601972e-03, 7.66842472e-01, 1.42533415e+04, 4.03670025e+03,
        2.92868636e+02, 2.48984031e-02],
       ...,
       [8.05556451e-03, 1.03945144e+00, 6.74277013e+03, 2.03124398e+03,
        1.31730956e+02, 1.67113569e-01],
       [1.14979333e-02, 1.78439632e+00, 6.83238523e+03, 1.94423864e+03,
        2.66335292e+02, 3.57005646e-01],
       [7.48060708e-03, 2.97318942e+00, 1.39846734e+04, 4.10568657e+03,
        3.22802524e+02, 2.77580848e-01]])
Coordinates:
  * index     (index) int64 0 1 2 3 4 5 6 ... 1993 1994 1995 1996 1997 1998 1999
  * variable  (variable) object 'D (m)' 'L (m)' 'P (kPa)' ... 'Tin (C)' 'Xe (-)'

We have one output, CHF.

[4]:
ytrain
[4]:
<xarray.DataArray (index: 2000, variable: 1)>
array([[ 377.05549841],
       [ 956.92913687],
       [3091.19358014],
       ...,
       [3725.37959666],
       [2066.25518823],
       [ 969.10507166]])
Coordinates:
  * index     (index) int64 0 1 2 3 4 5 6 ... 1993 1994 1995 1996 1997 1998 1999
  * variable  (variable) object 'CHF (kW m-2)'

Below we have the correlation matrix for this dataset.

[5]:
correlation_matrix(xr.concat([train_data, test_data], dim="samples"))
plt.show()
../_images/benchmarks_chf_9_0.png

We can see a slightly negative correlation between \(L\) and \(X\) with \(CHF\), while \(G\) has a slightly positive correlation.

We then min-max scale all data before hyperparameter tuning.

[6]:
# Min-max scale data
xtrain, xtest, xscaler = scale_data(xtrain, xtest, scaler=MinMaxScaler())
ytrain, ytest, yscaler = scale_data(ytrain, ytest, scaler=MinMaxScaler())

Model Initialization

We will examine the performance of 7 regression models:

  • linear: Linear,

  • lasso: lasso,

  • support vector machine SVM,

  • decision tree DT,

  • random forest RF,

  • k-nearest neighbors KN,

  • dense feedforward neural networks FNN.

For hyperparameter tuning, we initialize all classical models as scikit-learn defaults. For the FNN we define input and output layers with possible dropout layers. These layers include hyperparameter tuning of their number of nodes, use of sublayers, and the rate of dropout. The dense hidden layers include tuning of their depth.

[7]:
model_settings = {
    "models": ["Linear", "Lasso", "SVM", "DT", "RF", "KN", "FNN"],
    "FNN": {
        "structural_params": {
            "Dense_hidden": {
                "num_layers": mai.Int(min_value=0, max_value=4),
                "units": mai.Int(min_value=25, max_value=300),
                "activation": "relu",
                "kernel_initializer": "normal",
                "sublayer": mai.Choice(["Dropout_hidden", "None"]),
                "Dropout_hidden": {
                "rate": mai.Float(min_value=0.4, max_value=0.6),
                },
            },
            "Dense_output": {
                "units": ytrain.shape[-1],
                "activation": "linear",
                "kernel_initializer": "normal",
            },
        },
        "optimizer": "Adam",
        "Adam": {
            "learning_rate": mai.Float(min_value=1e-5, max_value=0.001),
        },
        "compile_params": {
            "loss": "mean_absolute_error",
            "metrics": ["mean_absolute_error"],
        },
        "fitting_params": {
            "batch_size": mai.Choice([16, 32, 64]),
            "epochs": 30,
            "validation_split": 0.15,
        },
    },
}
tuner = mai.Tuner(xtrain, ytrain, model_settings=model_settings)

Hyperparameter Tuning

The hyperparameter tuning spaces are defined below. We use random search for the classical models as their training is quick, and random search can cover ample parameter space. Bayesian search is utilized for the FNNs as their training is more computationally expensive. Both search methods use 5-fold cross-validation to mitigate bias from the training data. We train 200 classical models from random search and 50 iterations of Bayesian search for neural networks.

[8]:
random_search_spaces = {
    "Lasso": {
        "alpha": uniform(loc=0.0001, scale=0.0099), # 0.0001 - 0.01
    },
    "SVM": {
        "kernel": ["linear", "poly", "rbf", "sigmoid"],
        "degree": randint(low=1, high=5),
        "gamma": ["scale", "auto"],
    },
    "DT": {
        "max_depth": randint(low=5, high=50), # 5 - 50
        "max_features": [None, "sqrt", "log2", 2, 4, 6],
        "min_samples_split": randint(low=2, high=20), # 2 - 20
        "min_samples_leaf": randint(low=1, high=20), # 1 - 20
    },
    "RF": {
        "n_estimators": randint(low=50, high=200), # 50 - 200
        "criterion": ["squared_error", "absolute_error", "poisson"],
        "min_samples_split": randint(low=2, high=20), # 2 - 20
        "min_samples_leaf": randint(low=1, high=20), # 1 - 20
        "max_features": [None, "sqrt", "log2", 2, 4, 6],
    },
    "KN": {
        "n_neighbors": randint(low=1, high=20), # 1 - 20
        "weights": ["uniform", "distance"],
        "leaf_size": randint(low=1, high=30), # 1 - 30
        "p": randint(low=1, high=10), # 1 - 10
    },
}

start = time.time()
random_search_configs = tuner.random_search(
    param_spaces=random_search_spaces,
    n_iter=200,
    n_jobs=6,
    cv=5,
)

bayesian_search_configs = tuner.nn_bayesian_search(
    objective="r2_score",
    max_trials=50,
    cv=5,
)
print("Hyperparameter tuning took " + str((time.time() - start) / 60) + " minutes to process.")
Hyperparameter tuning took 23.15457481543223 minutes to process.

After the training, we can see training results for each iteration using the pyMAISE.Tuner.convergence_plot function. For the neural networks, we have the following:

[9]:
ax = tuner.convergence_plot(model_types="FNN")
ax.set_ylim([0, 1])
plt.show()
../_images/benchmarks_chf_17_0.png

The Bayesian search explores the search space but finds several suitable hyperparameter configurations.

Model Postprocessing

With the top pyMAISE.Settings.num_configs_saved, we can pass these parameter configurations to the pyMAISE.PostProcessor for model comparison and analysis. For the FNNs, we define the "epochs" parameter as 200 for better performance.

[10]:
postprocessor = mai.PostProcessor(
    data=(xtrain, xtest, ytrain, ytest),
    model_configs=[random_search_configs, bayesian_search_configs],
    new_model_settings={
        "FNN": {"fitting_params": {"epochs": 200}},
    },
    yscaler=yscaler,
)

Below we computed the performance metrics based on the performance of each model on the CHF dataset.

[11]:
postprocessor.metrics().drop("Parameter Configurations", axis=1)
[11]:
Model Types Train R2 Train MAE Train MAPE Train RMSE Train RMSPE Test R2 Test MAE Test MAPE Test RMSE Test RMSPE
26 FNN 0.990508 94.516480 6.649731 141.619037 10.694737 0.984016 112.056938 7.155676 168.414268 9.765656
29 FNN 0.981713 130.973074 11.190134 196.569625 19.033209 0.980012 133.205079 10.444191 188.331572 16.682770
28 FNN 0.981788 123.758863 9.232552 196.167621 15.062968 0.978672 129.940655 8.839379 194.544377 12.666816
30 FNN 0.980160 137.554895 10.049621 204.747434 17.117261 0.976495 146.234950 9.795921 204.231296 13.868115
27 FNN 0.983183 112.064923 7.608649 188.505708 12.652984 0.976474 125.963180 7.825601 204.320630 11.469703
16 RF 0.993368 65.585293 4.510185 118.381224 7.187412 0.972953 153.625124 10.616529 219.079658 14.802359
20 RF 0.987236 81.077412 5.414823 164.227892 8.852843 0.969859 159.234441 10.801584 231.269415 14.995095
17 RF 0.986148 91.061165 6.335240 171.081482 10.739503 0.968772 161.034753 11.308920 235.404268 15.945121
19 RF 0.984688 94.123667 6.631856 179.871080 11.169824 0.961154 174.426685 12.140031 262.551185 17.195610
18 RF 0.984847 96.270814 6.930750 178.937088 12.111184 0.959509 177.228360 12.392103 268.052022 17.778772
21 KN 1.000000 0.000000 0.000000 0.000000 0.000000 0.933615 215.336825 13.840165 343.222384 20.584267
22 KN 1.000000 0.000000 0.000000 0.000000 0.000000 0.933615 215.336825 13.840165 343.222384 20.584267
24 KN 0.967781 154.904106 9.857600 260.917217 14.309083 0.931029 223.432334 14.515832 349.843983 22.164982
25 KN 0.967781 154.904106 9.857600 260.917217 14.309083 0.931029 223.432334 14.515832 349.843983 22.164982
14 DT 0.989358 82.580507 5.293493 149.951876 7.964006 0.923636 254.440601 17.471253 368.117166 25.283433
13 DT 0.973614 130.161606 8.147538 236.122261 11.623378 0.922344 261.998800 17.658795 371.217626 24.205510
11 DT 0.974340 117.200649 7.370938 232.849637 10.797160 0.922214 261.855286 17.658778 371.529430 24.188682
23 KN 1.000000 0.000000 0.000000 0.000000 0.000000 0.921545 227.629803 14.753816 373.122665 21.699285
12 DT 0.960467 156.372291 9.907215 289.020105 14.289249 0.918241 267.786020 18.265848 380.899293 24.892493
15 DT 0.966189 145.913486 9.179704 267.284736 13.291821 0.917124 269.810126 18.446146 383.491269 25.067672
10 SVM 0.806954 542.860286 60.626177 638.673390 103.960390 0.821172 473.896169 45.830185 563.324304 72.300791
9 SVM 0.806954 542.860286 60.626177 638.673390 103.960390 0.821172 473.896169 45.830185 563.324304 72.300791
8 SVM 0.806954 542.860286 60.626177 638.673390 103.960390 0.821172 473.896169 45.830185 563.324304 72.300791
7 SVM 0.806954 542.860286 60.626177 638.673390 103.960390 0.821172 473.896169 45.830185 563.324304 72.300791
6 SVM 0.806954 542.860286 60.626177 638.673390 103.960390 0.821172 473.896169 45.830185 563.324304 72.300791
0 Linear 0.700590 567.152874 57.689799 795.392588 122.844808 0.702795 512.110104 44.459503 726.221750 74.579545
1 Lasso 0.700212 564.042244 56.414526 795.893993 118.768912 0.702055 508.729649 43.538637 727.125794 72.654205
2 Lasso 0.700203 563.995461 56.395515 795.905770 118.712196 0.702041 508.686986 43.525811 727.142169 72.628259
3 Lasso 0.699871 562.646548 55.829363 796.347214 117.001227 0.701573 507.432404 43.145331 727.713187 71.855605
4 Lasso 0.699842 562.554779 55.789611 796.384942 116.880006 0.701535 507.347537 43.119137 727.759704 71.801809
5 Lasso 0.699836 562.536258 55.781555 796.392753 116.855255 0.701527 507.330195 43.113784 727.769300 71.790839

This data set is not linear, with a poor performance from linear and lasso regression. The remaining four models in order are FNN > RF > KN > DT > SVM with all five models providing reliable performance. KN overfits with a perfect training \(R^2\).

Using the pyMAISE.PostProcessor.print_model function, we can see all the top performing (based on test \(R^2\)) models’ hyperparameters.

[12]:
for model in ["Lasso", "SVM", "DT", "RF", "KN", "FNN"]:
    postprocessor.print_model(model_type=model)
    print()
Model Type: Lasso
  alpha: 0.00017553613590585732

Model Type: SVM
  degree: 4
  gamma: scale
  kernel: rbf

Model Type: DT
  max_depth: 26
  max_features: 6
  min_samples_leaf: 1
  min_samples_split: 7

Model Type: RF
  criterion: squared_error
  max_features: None
  min_samples_leaf: 1
  min_samples_split: 2
  n_estimators: 174

Model Type: KN
  leaf_size: 24
  n_neighbors: 5
  p: 1
  weights: distance

Model Type: FNN
  Structural Hyperparameters
    Layer: Dense_hidden_0
      units: 231
      sublayer: None
    Layer: Dense_hidden_1
      units: 138
      sublayer: None
    Layer: Dense_hidden_2
      units: 267
      sublayer: Dropout_hidden
    Layer: Dense_hidden_2_sublayer_Dropout_hidden_0
      rate: 0.4995897609454529
    Layer: Dense_output_0
  Compile/Fitting Hyperparameters
    Adam_learning_rate: 0.0009311391232267503
    batch_size: 64
Model: "FNN"
_________________________________________________________________
 Layer (type)                Output Shape              Param #
=================================================================
 Dense_hidden_0 (Dense)      (None, 231)               1617

 Dense_hidden_1 (Dense)      (None, 138)               32016

 Dense_hidden_2 (Dense)      (None, 267)               37113

 Dense_hidden_2_sublayer_Dr  (None, 267)               0
 opout_hidden_0 (Dropout)

 Dense_output_0 (Dense)      (None, 1)                 268

=================================================================
Total params: 71014 (277.40 KB)
Trainable params: 71014 (277.40 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

Below is the network plot for the FNN.

[13]:
postprocessor.nn_network_plot(
    to_file="./supporting/chf_network.png",
    show_shapes=True,
    show_layer_names=True,
    expand_nested=True,
    show_layer_activations=True,
)
[13]:
../_images/benchmarks_chf_25_0.png

We can use the pyMAISE.PostProcessor.diagonal_validation_plot and pyMAISE.PostProcessor.validation_plot functions to visualize the performance of these models. Diagonal validation plots show the actual versus predicted outcome. A well-performing model follows \(y=x\) on this plot. You can choose what outputs to display on the diagonal validation plot; however, since we have a single output (CHF), a single plot will be generated. Both functions use the top-performing model on test \(R^2\) by default.

[14]:
models = np.array([["Linear", "Lasso"], ["DT", "KN"], ["RF", "SVM"]])
fig = plt.figure(figsize=(15, 25))
gs = fig.add_gridspec(4, 2)
for i in range(models.shape[0]):
    for j in range(models.shape[1]):
        ax = fig.add_subplot(gs[i, j])
        ax = postprocessor.diagonal_validation_plot(model_type=models[i, j])
        ax.set_title(models[i, j])
ax = fig.add_subplot(gs[3, :])
ax = postprocessor.diagonal_validation_plot(model_type="FNN")
ax.set_title("FNN")
plt.show()
../_images/benchmarks_chf_27_0.png

All models display a close spread to \(y=x\); however, given the more extensive spread, linear models’ lack of performance is apparent.

Validation plots show the absolute error of the predicted output relative to the actual outputs of the testing data set.

[15]:
fig = plt.figure(figsize=(15, 25))
gs = fig.add_gridspec(4, 2)
for i in range(models.shape[0]):
    for j in range(models.shape[1]):
        ax = fig.add_subplot(gs[i, j])
        ax = postprocessor.validation_plot(model_type=models[i, j])
        ax.set_title(models[i, j])
ax = fig.add_subplot(gs[3, :])
ax = postprocessor.validation_plot(model_type="FNN")
ax.set_title("FNN")
plt.show()
../_images/benchmarks_chf_29_0.png

Finally, we can see if the FNN is overfit based on its learning curve.

[16]:
postprocessor.nn_learning_plot()
plt.show()
../_images/benchmarks_chf_31_0.png

The FNN is not overfitting, as the validation curve closely follows the training curve.

Finally, using the best FNN model, we can generate new CHF predictions for new experimental setups that do not exist in our data set.

pyMAISElogo.png