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
1 change: 1 addition & 0 deletions changelog-entries/670.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New tutorial case: Partitioned Burgers' Equation in 1D solved with Finite Volumes and a Neural Network surrogate model !
143 changes: 143 additions & 0 deletions partitioned-burgers-1d/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
title: Partitioned Burgers' equation 1D
permalink: tutorials-partitioned-burgers-1d.html
keywords: Python, Neural Network, Surrogate, Burgers Equation, Finite Volume, CFD
summary: This tutorial demonstrates the partitioned solution of the 1D Burgers' equation using preCICE and a neural network surrogate solver.
---

{% note %}
Get the [case files of this tutorial](https://github.com/precice/tutorials/tree/master/partitioned-burgers-1d). Read how in the [tutorials introduction](https://precice.org/tutorials.html).
{% endnote %}

## Setup

We solve the 1D viscous Burgers' equation on the domain $[0,2]$:


$$
\frac{\partial u}{\partial t} = \nu \frac{\partial^2 u}{\partial x^2} - u \frac{\partial u}{\partial x},
$$

where $u(x,t)$ is the scalar velocity field and $\nu$ is the viscosity. In this tutorial by default $\nu$ is very small ($10^{-12}$), but can be changed in the solver.


The domain is partitioned into participants at $x=1$:

- **Dirichlet**: Solves the left half $[0,1]$ and receives Dirichlet boundary conditions at the interface.
- **Neumann**: Solves the right half $[1,2]$ and receives Neumann boundary conditions at the interface.

Both outer boundaries use zero-gradient conditions $\frac{\partial u}{\partial x} = 0$. The problem can be solved for different initial conditions of superimposed sine waves, which can be generated using the provided script `utils/generate_ic.py`.

<p align="center">
<img src="images/tutorials-partitioned-burgers-1d-full-domain-diagram.png" alt="Domain Diagram" width="500"/>
<br><em>Diagram of the partitioned domain with an example initial condition.</em>
</p>


## Configuration

preCICE configuration (image generated using the [precice-config-visualizer](https://precice.org/tooling-config-visualization.html)):

<p align="center">
<img src="images/tutorials-partitioned-burgers-1d-precice-config.png" alt="preCICE configuration visualization" width="600"/>
</p>

## Available Solvers

The conservative formulation of the Burgers' equation `solver-scipy` is implemented in a first-order finite volume code using Lax-Friedrichs fluxes and implicit Euler time stepping.

This tutorial includes two versions for the Neumann participant:
- A standard finite volume solver (`neumann-scipy`).
- A pre-trained neural network surrogate that approximates the solver (`neumann-surrogate`).

{% note %}
The surrogate participant requires PyTorch and related dependencies, which requires several gigabytes of disk space (~7Gb).
{% endnote %}

## Running the simulation
### Running the participants

To run the partitioned simulation, open two separate terminals and start each participant individually:

You can find the corresponding `run.sh` script for running the case in the folders corresponding to the participant you want to use:

```bash
cd dirichlet-scipy
./run.sh
```

and

```bash
cd neumann-scipy
./run.sh
```

or, to use the pretrained neural network surrogate participant:

```bash
cd neumann-surrogate
./run.sh
```

### Initial condition

The initial condition file `initial_condition.npz` is automatically generated by the run scripts if it does not exist.
You can also manually generate it using the script in `utils/`:

```bash
python3 utils/generate_ic.py --epoch <seed>
```

This script requires the Python libraries `numpy` and `matplotlib`.

---

### Helper scripts

There are helper scripts in the `utils/` directory that automate runs and visualization of both participants. They also accept an integer seed argument to specify the initial condition.

```bash
./utils/run-partitioned-scipy.sh
```

and

```bash
./utils/run-partitioned-surrogate.sh
```

### Monolithic solution (reference)

You can run the whole domain using the monolithic solver for comparison:

```bash
cd solver-scipy
./run.sh
```



## Visualization

After both participants (and/or monolithic simulation) have finished, you can run the visualization script.
`visualize_partitioned_domain.py` generates plots comparing the partitioned and monolithic solutions. You can specify which timestep to plot:

```bash
python3 visualize_partitioned_domain.py --neumann neumann-surrogate/surrogate.npz [timestep]
```

The script will produce the following output files in the `images/` directory:
- `full-domain-timestep-slice.png`: Solution $u$ at a selected timestep

<p align="left">
<img src="images/tutorials-partitioned-burgers-1d-full-domain-timestep-slice.png" alt="Full Domain Timestep Slice" width="400"/>
</p>

- `gradient-timestep-slice.png`: Gradient $du/dx$ at a selected timestep

- `full-domain-evolution.png`: Time evolution of the solution

<p align="left">
<img src="images/tutorials-partitioned-burgers-1d-full-domain-evolution.png" alt="Full Domain Evolution" width="400"/>
</p>
16 changes: 16 additions & 0 deletions partitioned-burgers-1d/clean-tutorial.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env sh
set -e -u

# shellcheck disable=SC1091
. ../tools/cleaning-tools.sh

clean_tutorial .
clean_precice_logs .
rm -fv ./*.log
rm -fv ./*.vtu

rm -f solver-scipy/full_domain.npz
rm -f dirichlet-scipy/dirichlet.npz
rm -f neumann-scipy/neumann.npz
rm -rf output/
rm -f initial_condition.npz
6 changes: 6 additions & 0 deletions partitioned-burgers-1d/dirichlet-scipy/clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e -u

rm -rf precice-profiling
rm -f dirichlet-scipy.log precice-Dirichlet-convergence.log precice-Dirichlet-iterations.log
rm -f dirichlet.npz
18 changes: 18 additions & 0 deletions partitioned-burgers-1d/dirichlet-scipy/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -e -u

python3 -m venv .venv
. .venv/bin/activate
pip install -r ../solver-scipy/requirements.txt

if [ ! -f "../initial_condition.npz" ]; then
echo "Generating initial condition..."
python3 ../utils/generate_ic.py
fi

. ../../tools/log.sh
exec > >(tee --append "$LOGFILE") 2>&1

python3 ../solver-scipy/solver.py Dirichlet

close_log
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions partitioned-burgers-1d/neumann-scipy/clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e -u

rm -rf precice-profiling
rm -f neumann-scipy.log precice-Neumann-convergence.log precice-Neumann-iterations.log
rm -f neumann.npz
18 changes: 18 additions & 0 deletions partitioned-burgers-1d/neumann-scipy/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -e -u

python3 -m venv .venv
. .venv/bin/activate
pip install -r ../solver-scipy/requirements.txt

if [ ! -f "../initial_condition.npz" ]; then
echo "Generating initial condition..."
python3 ../utils/generate_ic.py
fi

. ../../tools/log.sh
exec > >(tee --append "$LOGFILE") 2>&1

python3 ../solver-scipy/solver.py Neumann

close_log
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions partitioned-burgers-1d/neumann-surrogate/clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e -u

rm -rf precice-profiling
rm -f neumann-surrogate.log precice-Neumann-convergence.log precice-Neumann-iterations.log
rm -f surrogate.npz
Comment on lines +4 to +6
Copy link
Member

Choose a reason for hiding this comment

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

Compare how other tutorials do this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Renamed clean.sh to clean-tutorial.sh. But should it be symlinked to clean-tutorial-base.sh? I need these additional commands.

15 changes: 15 additions & 0 deletions partitioned-burgers-1d/neumann-surrogate/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import torch

# Model architecture
INPUT_SIZE = 128 + 2 # +2 for ghost cells
HIDDEN_SIZE = 64 # num filters
OUTPUT_SIZE = 128

assert INPUT_SIZE >= OUTPUT_SIZE, "Input size must be greater or equal to output size."
assert (INPUT_SIZE - OUTPUT_SIZE) % 2 == 0, "Input and output sizes must differ by an even number (for ghost cells)."

NUM_RES_BLOCKS = 4
KERNEL_SIZE = 5
ACTIVATION = torch.nn.ReLU

MODEL_NAME = "CNN_RES_UNROLL_7.pth"
110 changes: 110 additions & 0 deletions partitioned-burgers-1d/neumann-surrogate/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.utils import weight_norm

def pad_with_ghost_cells(input_seq, bc_left, bc_right):
return torch.cat([bc_left, input_seq, bc_right], dim=1)

class LinearExtrapolationPadding1D(nn.Module):
"""Applies 'same' padding using linear extrapolation."""
def __init__(self, kernel_size: int, dilation: int = 1):
super().__init__()
self.pad_total = dilation * (kernel_size - 1)
self.pad_beg = self.pad_total // 2
self.pad_end = self.pad_total - self.pad_beg

def forward(self, x):
# Don't pad if not necessary
if self.pad_total == 0:
return x

ghost_cell_left = x[:, :, :1]
ghost_cell_right = x[:, :, -1:]

# Calculate the gradient at each boundary
grad_left = x[:, :, 1:2] - ghost_cell_left
grad_right = ghost_cell_right - x[:, :, -2:-1]

# Higher order finite difference gradient approximation
# grad_left = ( -11 * ghost_cell_left + 18 * x[:, :, 1:2] - 9 * x[:, :, 2:3] + 2 * x[:, :, 3:4]) / 6
# grad_right = (11 * ghost_cell_right - 18 * x[:, :, -2:-1] + 9 * x[:, :, -3:-2] - 2 * x[:, :, -4:-3]) / 6

# 3. Extrapolated padding tensors
left_ramp = torch.arange(self.pad_beg, 0, -1, device=x.device, dtype=x.dtype).view(1, 1, -1)
left_padding = ghost_cell_left - left_ramp * grad_left

right_ramp = torch.arange(1, self.pad_end + 1, device=x.device, dtype=x.dtype).view(1, 1, -1)
right_padding = ghost_cell_right + right_ramp * grad_right

return torch.cat([left_padding, x, right_padding], dim=2)

class ResidualBlock1D(nn.Module):
"""A residual block that uses custom 'same' padding with linear extrapolation and weight normalization."""
def __init__(self, channels, kernel_size=3, activation=nn.ReLU):
super(ResidualBlock1D, self).__init__()
self.activation = activation()
# Apply weight normalization
self.conv1 = weight_norm(nn.Conv1d(channels, channels, kernel_size, padding='valid', bias=True))
self.ghost_padding1 = LinearExtrapolationPadding1D(kernel_size)
self.conv2 = weight_norm(nn.Conv1d(channels, channels, kernel_size, padding='valid', bias=True))
self.ghost_padding2 = LinearExtrapolationPadding1D(kernel_size)

def forward(self, x):
identity = x

out = self.ghost_padding1(x)
out = self.conv1(out)
out = self.activation(out)

out = self.ghost_padding2(out)
out = self.conv2(out)

return self.activation(out) + identity

class CNN_RES(nn.Module):
"""
A CNN with residual blocks for 1D data.
Expects a pre-padded input with ghost_cells//2 number ghost cells on each side.
Applies a custom linear extrapolation padding for inner layers.
"""
def __init__(self, hidden_channels, num_blocks=2, kernel_size=3, activation=nn.ReLU, ghost_cells=2):
super(CNN_RES, self).__init__()
self.activation = activation()
self.hidden_channels = hidden_channels
self.num_blocks = num_blocks
self.kernel_size = kernel_size
assert ghost_cells % 2 == 0, "ghost_cells must be even"
self.ghost_cells = ghost_cells

self.ghost_padding = LinearExtrapolationPadding1D(self.ghost_cells + self.kernel_size)

# Apply weight normalization to the input convolution
self.conv_in = weight_norm(nn.Conv1d(1, hidden_channels, kernel_size=1, bias=True))

layers = [ResidualBlock1D(hidden_channels, kernel_size, activation=activation) for _ in range(num_blocks)]
self.res_blocks = nn.Sequential(*layers)

self.conv_out = nn.Conv1d(hidden_channels, 1, kernel_size=1)

def forward(self, x):

if x.dim() == 2:
x = x.unsqueeze(1) # Add channel dim: (B, 1, L)

if not self.ghost_cells == 0:
x_padded = self.ghost_padding(x)

else:
x_padded = x

total_pad_each_side = self.ghost_padding.pad_beg + self.ghost_cells // 2

out = self.activation(self.conv_in(x_padded)) # no extra padding here
out = self.res_blocks(out)
out = self.conv_out(out) # no extra padding here

if not self.ghost_cells == 0:
out = out[:, :, total_pad_each_side:-total_pad_each_side] # remove ghost cells, return only internal domain

return out.squeeze(1)
5 changes: 5 additions & 0 deletions partitioned-burgers-1d/neumann-surrogate/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
numpy==2.3.5
pyprecice==3.3.1
scipy==1.16.3
matplotlib==3.10.8
torch==2.9.1
18 changes: 18 additions & 0 deletions partitioned-burgers-1d/neumann-surrogate/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -e -u

python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt

if [ ! -f "../initial_condition.npz" ]; then
echo "Generating initial condition..."
python3 ../utils/generate_ic.py
fi

. ../../tools/log.sh
exec > >(tee --append "$LOGFILE") 2>&1

python3 solver.py

close_log
Loading
Loading