# pip install packages that are not in Pyodide
%pip install ipympl==0.9.3
%pip install seaborn==0.12.2
%pip install pyodide
Requirement already satisfied: ipympl==0.9.3 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (0.9.3)
Requirement already satisfied: ipython<9 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipympl==0.9.3) (8.37.0)
Requirement already satisfied: numpy in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipympl==0.9.3) (2.2.6)
Requirement already satisfied: ipython-genutils in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipympl==0.9.3) (0.2.0)
Requirement already satisfied: pillow in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipympl==0.9.3) (11.2.1)
Requirement already satisfied: traitlets<6 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipympl==0.9.3) (5.14.3)
Requirement already satisfied: ipywidgets<9,>=7.6.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipympl==0.9.3) (8.1.7)
Requirement already satisfied: matplotlib<4,>=3.4.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipympl==0.9.3) (3.10.3)
Requirement already satisfied: decorator in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipython<9->ipympl==0.9.3) (5.2.1)
Requirement already satisfied: jedi>=0.16 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipython<9->ipympl==0.9.3) (0.19.2)
Requirement already satisfied: matplotlib-inline in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipython<9->ipympl==0.9.3) (0.1.7)
Requirement already satisfied: pexpect>4.3 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipython<9->ipympl==0.9.3) (4.9.0)
Requirement already satisfied: prompt_toolkit<3.1.0,>=3.0.41 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipython<9->ipympl==0.9.3) (3.0.51)
Requirement already satisfied: pygments>=2.4.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipython<9->ipympl==0.9.3) (2.19.2)
Requirement already satisfied: stack_data in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipython<9->ipympl==0.9.3) (0.6.3)
Requirement already satisfied: comm>=0.1.3 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipywidgets<9,>=7.6.0->ipympl==0.9.3) (0.2.2)
Requirement already satisfied: widgetsnbextension~=4.0.14 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipywidgets<9,>=7.6.0->ipympl==0.9.3) (4.0.14)
Requirement already satisfied: jupyterlab_widgets~=3.0.15 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from ipywidgets<9,>=7.6.0->ipympl==0.9.3) (3.0.15)
Requirement already satisfied: contourpy>=1.0.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib<4,>=3.4.0->ipympl==0.9.3) (1.3.2)
Requirement already satisfied: cycler>=0.10 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib<4,>=3.4.0->ipympl==0.9.3) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib<4,>=3.4.0->ipympl==0.9.3) (4.59.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib<4,>=3.4.0->ipympl==0.9.3) (1.4.8)
Requirement already satisfied: packaging>=20.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib<4,>=3.4.0->ipympl==0.9.3) (25.0)
Requirement already satisfied: pyparsing>=2.3.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib<4,>=3.4.0->ipympl==0.9.3) (3.2.3)
Requirement already satisfied: python-dateutil>=2.7 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib<4,>=3.4.0->ipympl==0.9.3) (2.9.0.post0)
Requirement already satisfied: wcwidth in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from prompt_toolkit<3.1.0,>=3.0.41->ipython<9->ipympl==0.9.3) (0.2.13)
Requirement already satisfied: parso<0.9.0,>=0.8.4 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from jedi>=0.16->ipython<9->ipympl==0.9.3) (0.8.4)
Requirement already satisfied: ptyprocess>=0.5 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from pexpect>4.3->ipython<9->ipympl==0.9.3) (0.7.0)
Requirement already satisfied: six>=1.5 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from python-dateutil>=2.7->matplotlib<4,>=3.4.0->ipympl==0.9.3) (1.17.0)
Requirement already satisfied: executing>=1.2.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from stack_data->ipython<9->ipympl==0.9.3) (2.2.0)
Requirement already satisfied: asttokens>=2.1.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from stack_data->ipython<9->ipympl==0.9.3) (3.0.0)
Requirement already satisfied: pure_eval in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from stack_data->ipython<9->ipympl==0.9.3) (0.2.3)
Note: you may need to restart the kernel to use updated packages.
Requirement already satisfied: seaborn==0.12.2 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (0.12.2)
Requirement already satisfied: numpy!=1.24.0,>=1.17 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from seaborn==0.12.2) (2.2.6)
Requirement already satisfied: pandas>=0.25 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from seaborn==0.12.2) (2.3.0)
Requirement already satisfied: matplotlib!=3.6.1,>=3.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from seaborn==0.12.2) (3.10.3)
Requirement already satisfied: contourpy>=1.0.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (1.3.2)
Requirement already satisfied: cycler>=0.10 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (4.59.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (1.4.8)
Requirement already satisfied: packaging>=20.0 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (25.0)
Requirement already satisfied: pillow>=8 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (11.2.1)
Requirement already satisfied: pyparsing>=2.3.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (3.2.3)
Requirement already satisfied: python-dateutil>=2.7 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from pandas>=0.25->seaborn==0.12.2) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from pandas>=0.25->seaborn==0.12.2) (2025.2)
Requirement already satisfied: six>=1.5 in /Users/isabelslingerland/tudelft-conda/lib/python3.12/site-packages (from python-dateutil>=2.7->matplotlib!=3.6.1,>=3.1->seaborn==0.12.2) (1.17.0)
Note: you may need to restart the kernel to use updated packages.
Collecting pyodide
Using cached pyodide-0.0.2.tar.gz (19 kB)
Preparing metadata (setup.py) ... ?25lerror
error: subprocess-exited-with-error
× python setup.py egg_info did not run successfully.
│ exit code: 1
╰─> [7 lines of output]
Traceback (most recent call last):
File "<string>", line 2, in <module>
File "<pip-setuptools-caller>", line 35, in <module>
File "/private/var/folders/9f/qj38cd3d5v37nszw4dsn87nr0000gn/T/pip-install-4jlkdv5h/pyodide_5067822562264f268c58bab21d7e5463/setup.py", line 7, in <module>
raise ValueError(
ValueError: Pyodide is a Python distribution that runs in the browser or Node.js. It cannot be installed from PyPi.
See https://github.com/pyodide/pyodide for how to use Pyodide.
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
?25herror: metadata-generation-failed
× Encountered error while generating package metadata.
╰─> See above for output.
note: This is an issue with the package mentioned above, not pip.
hint: See above for details.
Note: you may need to restart the kernel to use updated packages.
import time
import pickle
import numpy as np
import os
import matplotlib.pyplot as plt
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from ipywidgets import VBox, HBox, IntSlider, Dropdown, interactive_output
from mude_tools import magicplotter
import warnings
from sklearn.exceptions import ConvergenceWarning
import seaborn as sns
from ipywidgets import interact, IntSlider, Dropdown
from cycler import cycler
%matplotlib widget
# Set the color scheme
sns.set_theme()
colors = [
"#0076C2",
"#EC6842",
"#A50034",
"#009B77",
"#FFB81C",
"#E03C31",
"#6CC24A",
"#EF60A3",
"#0C2340",
"#00B8C8",
"#6F1D77",
]
plt.rcParams["axes.prop_cycle"] = cycler(color=colors)
import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
MLP Application: Predicting Pressure in a Water Network#
In this example, we demonstrate how a Multilayer Perceptron (MLP) can be used to estimate nodal pressures in a water distribution network, as illustrated in the simplified schematic below:
Fig. 6.34 Simplified scheme of a branched water distribution system#
Pressure estimation is critical for ensuring stable and efficient operation of water networks. Traditionally, this is achieved using physically-based hydrodynamic models, which simulate fluid behavior across the system. However, such models can be computationally expensive, especially when used in tasks like optimization or real-time control in large networks.
To address this limitation, we use a data-driven alternative: a neural network trained on simulation results from the original physical model. This allows us to approximate the mapping from pipe diameters to nodal pressures, achieving high accuracy with significantly reduced computational cost.
As a case study, we use data from the BakRyan water distribution system as seen in the figure below, which consists of 58 pipes and 35 nodes.
Fig. 6.35 Numerical results for the BakRyan water distribution system#
Mathematical Model#
We can express the relationship between pipe diameters and nodal pressures using a function \(\phi\) parameterized by the neural network weights \(\mathbf{W}\):
Where:
\(\mathbf{y}\): output data (nodal pressures, units: mwc)
\(\phi\): represents the Neural Network
\(\mathbf{x}\): input data (pipe diameters, units: m)
\(\mathbf{W}\): parameters of the MLP (unitless)
Having pairs of input-output data (diameters \(\mathbf{x}\) and pressures \(\mathbf{y}\)) the goal is to find the parameters \(\mathbf{W}\) that best fit the data.
The neural network we train is a fully connected MLP, as sketched below.
Fig. 6.36 Artificial neural network representation, with a zoomed-in view of how a single neuron works. For this notebook, we have 58 input features (pipe diameters) and 35 outputs (nodal pressures).#
Load data#
we load the data and below you can check the dimensions of the features (the pipe diameters) and the targets (the nodal pressures).
# import os
# from urllib.request import urlretrieve
# def findfile(fname):
# if not os.path.isfile(fname):
# print(f"Downloading {fname}...")
# urlretrieve('http://files.mude.citg.tudelft.nl/'+fname, fname)
# findfile('targets_BAK.csv')
# findfile('features_BAK.csv')
from pyodide.http import open_url
import numpy as np
features = np.loadtxt(open_url("https://files.mude.citg.tudelft.nl/features_BAK.csv"), delimiter=",")
targets = np.loadtxt(open_url("https://files.mude.citg.tudelft.nl/targets_BAK.csv"), delimiter=",")
Downloading targets_BAK.csv...
Downloading features_BAK.csv...
print('Dimensions of features (X):', features.shape)
print('Dimensions of targets (t):', targets.shape)
Dimensions of features (X): (10000, 58)
Dimensions of targets (t): (10000, 35)
Training an MLP with scikit-learn#
To train a neural network in Python, we can use scikit-learn’s MLPRegressor class. This interface allows us to define and train a fully connected multilayer perceptron with just a few lines of code.
We only need to specify the number of hidden layers and neurons, the activation function, and the number of training epochs.
from sklearn.neural_network import MLPRegressor
model = MLPRegressor(
hidden_layer_sizes=(50, 50), # Two hidden layers with 50 neurons each
activation='relu', # Activation function ('identity', 'logistic', 'tanh', 'relu')
solver='sgd', # Optimizer
max_iter=100, # Number of epochs
early_stopping=True, # Stop if validation score does not improve
validation_fraction=0.1, # 10% of training data used for validation
random_state=42
)
Once the model is defined, we call .fit() to train it:
model.fit(X_train, t_train)
This single call handles all internal operations: shuffling the data, batching, performing gradient descent, and updating the weights at each epoch.
During training, scikit-learn records the training loss at each epoch, which we can access via model.loss_curve_.
If you enable early stopping by setting early_stopping=True, the model will automatically split off a portion of the training data for validation (controlled by validation_fraction, default is 0.1). In this case, you can access model.validation_scores_ to get the \(R^2\) score on the validation set at each epoch. Note that \(R^2\) is a performance metric (higher is better) and not a loss metric. It measures how well the model explains the variance in the data.
These training and validation metrics can be plotted to analyze model performance and detect overfitting. When early_stopping=True, training will terminate automatically if the validation score stops improving for a specified number of iterations (n_iter_no_change), helping prevent overfitting. You can experiment with hyperparameters yourself and observe how they affect the training and validation plots below.
Be aware that the required computations can take a few moments to run.
def prepare_data(features, targets, test_size=0.20):
"""
Prepares and normalizes the data for training.
"""
scaler_X = MinMaxScaler().fit(features)
scaler_t = MinMaxScaler().fit(targets)
X_train_n = scaler_X.transform(features)
t_train_n = scaler_t.transform(targets)
return X_train_n, t_train_n
warnings.filterwarnings("ignore", category=ConvergenceWarning)
def interactive_train_plot(features, targets):
def run(num_layers=2, neurons=50, activation="tanh", n_epochs=30):
X_train, t_train = prepare_data(features, targets)
hidden = tuple([neurons] * num_layers)
model = MLPRegressor(
hidden_layer_sizes=hidden,
activation=activation,
solver="sgd",
learning_rate_init=1e-3,
batch_size=64,
max_iter=n_epochs,
early_stopping=True,
validation_fraction=0.1,
random_state=42,
)
model.fit(X_train, t_train)
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
fig.canvas.toolbar_visible = False
# Training Loss
if hasattr(model, "loss_curve_"):
axes[0].plot(model.loss_curve_, color='blue', linewidth=2, label="Training Loss")
axes[0].set_yscale("log")
axes[0].set_title("Training Loss Curve")
axes[0].set_xlabel("Epochs")
axes[0].set_ylabel("Loss (MSE)")
axes[0].grid(True, linestyle="--", alpha=0.5)
axes[0].legend()
# Validation R^2 Score
val_scores = getattr(model, "validation_scores_", None)
if val_scores is not None and len(val_scores) > 0:
axes[1].plot(val_scores, color='orange', linewidth=2, label="Validation $R^2$")
axes[1].set_title("Validation Score Curve")
axes[1].set_xlabel("Epochs")
axes[1].set_ylabel("$R^2$ Score")
axes[1].grid(True, linestyle="--", alpha=0.5)
axes[1].legend()
fig.suptitle("MLP Training Progress", fontsize=16, fontweight="bold")
fig.tight_layout(rect=[0, 0, 1, 0.95]) # play with subtitle space
fig.subplots_adjust(wspace=0.25) # reduce horizontal space between subplots
plt.show()
# Create widgets
num_layers_widget = IntSlider(min=1, max=10, step=1, value=2, description="Layers")
neurons_widget = IntSlider(min=10, max=100, step=5, value=50, description="Neurons")
n_epochs_widget = IntSlider(min=10, max=200, step=1, value=50, description="Epochs")
activation_widget = Dropdown(
options=["identity", "logistic", "tanh", "relu"],
value="tanh",
description="Activation"
)
controls = VBox([num_layers_widget, neurons_widget, n_epochs_widget, activation_widget])
output = interactive_output(run, {
"num_layers": num_layers_widget,
"neurons": neurons_widget,
"activation": activation_widget,
"n_epochs": n_epochs_widget,
})
display(VBox([output, controls]))
interactive_train_plot(features, targets)