-
-
Notifications
You must be signed in to change notification settings - Fork 128
New tutorial: Partitioned Burgers eq. 1D #670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
4149918
1dabcb0
6c8660c
82accc2
2c425e4
5ab08f6
c5844c1
8b78407
2357008
5d66f9f
8213367
6546901
20cab48
fd63b1d
9beb26b
1e8b1ea
11b91fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 ! |
| 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> | ||
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Compare how other tutorials do this.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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" |
| 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) |
| 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 |
| 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 |
Uh oh!
There was an error while loading. Please reload this page.