-
-
Notifications
You must be signed in to change notification settings - Fork 144
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
854ad44
292a672
b193d05
6c99c35
3c66728
bb434d9
c06bede
83218df
602a2b0
51d1b2f
ea81634
e692a53
bbf7f20
9a8dc51
84cafd5
a476880
1c69601
ad6b1ee
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 @@ | ||
| - Added new case: 1D partitioned Burgers' equation with one finite volume and one NN surrogate participant. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| --- | ||
| 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/develop/partitioned-burgers-1d), as continuously rendered here, or see the [latest released version](https://github.com/precice/tutorials/tree/master/partitioned-burgers-1d) (if there is already one). 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`. | ||
|
|
||
|  | ||
| Diagram of the partitioned domain with an example initial condition. | ||
|
|
||
| ## Configuration | ||
|
|
||
| preCICE configuration (image generated using the [precice-config-visualizer](https://precice.org/tooling-config-visualization.html)): | ||
|
|
||
|  | ||
|
|
||
| ## Available solvers | ||
|
|
||
| Currently, the SciPy solver (`solver-scipy`) can be used for both sides, the NN surrogate solver (`neumann-surrogate`) only for the Neumann side. | ||
|
|
||
| - SciPy. Simple finite volume solver using Lax-Friedrichs fluxes and implicit Euler time stepping. | ||
| - Surrogate. Pre-trained neural network surrogate model. | ||
|
|
||
| The conservative formulation of the Burgers' equation is implemented in the SciPy solver. The surrogate is a neural network trained to predict the next time step solution based on the current solution. The surrogate model was trained on solutions obtained with the SciPy solver. | ||
|
|
||
| {% note %} | ||
| The surrogate participant requires PyTorch and related dependencies. By default, the CPU version is installed. If you wish to use the GPU version, see the comment in `neumann-surrogate/requirements.txt`. The GPU version requires several gigabytes of disk space (~6.2Gb). | ||
| {% 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 | ||
| ``` | ||
|
|
||
| ## Post-processing | ||
|
|
||
| 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. Call from the root of the tutorial: | ||
|
|
||
| ```bash | ||
| python3 utils/visualize_partitioned_domain.py --neumann neumann-surrogate/surrogate.npz [timestep] | ||
| ``` | ||
|
|
||
| The script will produce the following output files in the `output/` directory: | ||
|
|
||
| - `full-domain-timestep-slice.png`: Solution $u$ at the selected timestep | ||
|
|
||
|  | ||
|
|
||
| - `gradient-timestep-slice.png`: Gradient $du/dx$ at the selected timestep | ||
|
|
||
| - `full-domain-evolution.png`: Time evolution of the solution | ||
|
|
||
|  |
|
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. Should be a symbolic link to ../tools/clean-tutorial-base.sh.
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. I changed the ../tools/cleaning-tools.sh to skip the But I cannot symlink clean-tutorial.sh to ../tools/clean-tutorial-base.sh, because I need additional commands to remove the clean-tutorial.sh #!/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
# Clean up root directory
rm -f initial_condition.npz
rm -rf output/
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. Understood. Problem is that your case produces files at the root level.
Is there any easy option to split these and move into the subfolders?
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. Both sides need the
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| #!/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 | ||
|
|
||
| # Clean up root directory | ||
| rm -f initial_condition.npz | ||
| rm -rf output/ |
vidulejs marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #!/usr/bin/env sh | ||
| set -e -u | ||
|
|
||
| # shellcheck disable=SC1091 | ||
| . ../../tools/cleaning-tools.sh | ||
|
|
||
| clean_precice_logs . | ||
| clean_case_logs . | ||
| rm -f dirichlet.npz |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #!/usr/bin/env bash | ||
| set -e -u | ||
|
|
||
| if [ ! -v PRECICE_TUTORIALS_NO_VENV ] | ||
| then | ||
| python3 -m venv .venv | ||
| . .venv/bin/activate | ||
| pip install -r ../solver-scipy/requirements.txt && pip freeze > pip-installed-packages.log | ||
| fi | ||
|
|
||
| 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 |
vidulejs marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #!/usr/bin/env sh | ||
| set -e -u | ||
|
|
||
| # shellcheck disable=SC1091 | ||
| . ../../tools/cleaning-tools.sh | ||
|
|
||
| clean_precice_logs . | ||
| clean_case_logs . | ||
| rm -f neumann.npz |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #!/usr/bin/env bash | ||
| set -e -u | ||
|
|
||
| if [ ! -v PRECICE_TUTORIALS_NO_VENV ] | ||
| then | ||
| python3 -m venv .venv | ||
| . .venv/bin/activate | ||
| pip install -r ../solver-scipy/requirements.txt && pip freeze > pip-installed-packages.log | ||
| fi | ||
|
|
||
| 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,9 @@ | ||
| #!/usr/bin/env sh | ||
| set -e -u | ||
|
|
||
| # shellcheck disable=SC1091 | ||
| . ../../tools/cleaning-tools.sh | ||
|
|
||
| clean_precice_logs . | ||
| clean_case_logs . | ||
| rm -f surrogate.npz |
| 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,113 @@ | ||
| 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] | ||
|
|
||
| # 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,9 @@ | ||
| # For GPU version, uncomment the following line and comment out the CPU one: | ||
| # --extra-index-url https://download.pytorch.org/whl/cu118 | ||
| --extra-index-url https://download.pytorch.org/whl/cpu | ||
|
|
||
| numpy==2.3.5 | ||
| pyprecice~=3.0 | ||
| scipy==1.16.3 | ||
| matplotlib==3.10.8 | ||
| torch==2.9.1 | ||
uekerman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #!/usr/bin/env bash | ||
| set -e -u | ||
|
|
||
| if [ ! -v PRECICE_TUTORIALS_NO_VENV ] | ||
| then | ||
| python3 -m venv .venv | ||
| . .venv/bin/activate | ||
| pip install -r requirements.txt && pip freeze > pip-installed-packages.log | ||
| fi | ||
|
|
||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add references section with your thesis.