|
1 | | -## 📝 Goal of the tutorial: |
2 | | -This tutorial aims to document the performance trade-offs of using PINNs and high-fidelity traditional numerical solvers from the SciML ecosystem. |
| 1 | +# Trusting PINNs: Validating a Neural Solver against a standard numerical solver |
3 | 2 |
|
4 | | -## 🧪 Scope of Comparison: |
| 3 | +## Dependencies |
| 4 | +These packages are required to run the following tutorial: |
5 | 5 |
|
6 | | -The comparison will be using a multi parameter PDE like the Burgers' or Heat Equation, where a physical parameter is varied. |
| 6 | +```julia |
| 7 | +using Pkg |
| 8 | +Pkg.add(["NeuralPDE", "MethodOfLines", "OrdinaryDiffEq", "ModelingToolkit", "Lux", "Optimization", "OptimizationOptimJL", "OptimizationOptimisers", "DomainSets", "Plots", "Statistics"]) |
| 9 | +``` |
7 | 10 |
|
8 | | -* Approach using PINNs : Train a single, continuous PINN model as a function of space, time, and the varied parameter. |
9 | | -* Traditional Numerical Approach: Use an optimized solver to generate solutions across the same parameter range. |
| 11 | +## 1D Viscous Burgers' Equation: |
10 | 12 |
|
11 | | -**Status: Work In Progress (WIP)** |
| 13 | +```math |
| 14 | +\frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} - \nu \frac{\partial^2 u}{\partial x^2} = 0 |
| 15 | +``` |
| 16 | + |
| 17 | +Where we set $\nu = 0.02$ as the viscosity coefficient. The problem is defined on the domain $t \in [0, 1]$ and $x \in [-1, 1]$ with: |
| 18 | +* **Initial Condition:** $u(0, x) = -\sin(\pi x)$ |
| 19 | +* **Boundary Conditions:** $u(t, -1) = u(t, 1) = 0$ |
| 20 | + |
| 21 | +Implementing this using`ModelingToolkit.jl`: |
| 22 | + |
| 23 | +```@example burgers_comp |
| 24 | +using NeuralPDE, MethodOfLines, OrdinaryDiffEq, ModelingToolkit |
| 25 | +using Lux, Optimization, OptimizationOptimJL, OptimizationOptimisers |
| 26 | +using DomainSets, Plots, Random, Statistics |
| 27 | +
|
| 28 | +# Setting seed to ensure reproducibility: |
| 29 | +rng = Random.default_rng() |
| 30 | +Random.seed!(rng, 42) |
| 31 | +
|
| 32 | +# Statistics Tracker Training: |
| 33 | +mutable struct TrainingStats |
| 34 | + losses::Vector{Float64} |
| 35 | + best_loss::Float64 |
| 36 | +end |
| 37 | +
|
| 38 | +@parameters t x |
| 39 | +@variables u(..) |
| 40 | +Dt = Differential(t) |
| 41 | +Dx = Differential(x) |
| 42 | +Dxx = Differential(x)^2 |
| 43 | +
|
| 44 | +# Setting values of constants: |
| 45 | +ν = 0.02 |
| 46 | +t_max = 1.0 |
| 47 | +x_min = -1.0 |
| 48 | +x_max = 1.0 |
| 49 | +
|
| 50 | +# Defining the 1D Viscous Burgers' Equation: |
| 51 | +eq = Dt(u(t,x)) + u(t,x)*Dx(u(t,x)) - ν*Dxx(u(t,x)) ~ 0 |
| 52 | +
|
| 53 | +# Setting Boundary Conditions: |
| 54 | +bcs = [u(0.0, x) ~ -sin(π * x), |
| 55 | + u(t, x_min) ~ 0.0, |
| 56 | + u(t, x_max) ~ 0.0] |
| 57 | +domains = [t ∈ Interval(0.0, t_max), x ∈ Interval(x_min, x_max)] |
| 58 | +
|
| 59 | +# Defining the PDE System: |
| 60 | +@named pde_system = PDESystem(eq, bcs, domains, [t, x], [u(t, x)]) |
| 61 | +``` |
| 62 | + |
| 63 | +## Generating the ground truth: (MethodOfLines) |
| 64 | +We discretize the spatial domain using a finite difference scheme and solve the resulting ODE system with a high-order solver (`Tsit5`). This is done to generate a ground truth solution to compare the results of the neural network to. |
| 65 | + |
| 66 | +```@example burgers_comp |
| 67 | +
|
| 68 | +dx_mol = 0.01 |
| 69 | +discretization_mol = MOLFiniteDifference([x => dx_mol], t) |
| 70 | +prob_mol = discretize(pde_system, discretization_mol) |
| 71 | +@time sol_mol = solve(prob_mol, Tsit5(), saveat=0.01) |
| 72 | +``` |
| 73 | + |
| 74 | +## Training a PINN: |
| 75 | +We use a grid training strategy to train a deep neural network. The best loss achieved is stored. |
| 76 | + |
| 77 | +```@example burgers_comp |
| 78 | +# Defining the architecture of the Neural Network: |
| 79 | +chain = Lux.Chain( |
| 80 | + Dense(2, 20, Lux.tanh), |
| 81 | + Dense(20, 30, Lux.tanh), |
| 82 | + Dense(30, 30, Lux.tanh), |
| 83 | + Dense(30, 20, Lux.tanh), |
| 84 | + Dense(20, 20, Lux.tanh), |
| 85 | + Dense(20, 1) |
| 86 | +) |
| 87 | +strategy = GridTraining(0.05) |
| 88 | +discretization_pinn = PhysicsInformedNN(chain, strategy) |
| 89 | +prob_pinn = discretize(pde_system, discretization_pinn) |
| 90 | +stats = TrainingStats([], Inf) |
| 91 | +callback = function (p, l) |
| 92 | + push!(stats.losses, l) |
| 93 | + if l < stats.best_loss |
| 94 | + stats.best_loss = l |
| 95 | + end |
| 96 | + |
| 97 | + # In case we get NaN anywhere: |
| 98 | + if isnan(l) |
| 99 | + println("!!! WARNING: Loss became NaN. Aborting training.") |
| 100 | + return true |
| 101 | + end |
| 102 | +
|
| 103 | + if length(stats.losses) % 500 == 0 |
| 104 | + println("Iter: $(length(stats.losses)) | Current: $l | Best: $(stats.best_loss)") |
| 105 | + end |
| 106 | + return false |
| 107 | +end |
| 108 | +``` |
| 109 | + |
| 110 | +## Training strategy |
| 111 | +The training of the neural network consists of 2 stages: |
| 112 | +1. **Adam**: Rapidly descends the loss landscape to find a rough solution (2000 iterations). |
| 113 | +2. **BFGS**: A second-order optimizer used to fine-tune the solution to high precision (1000 iterations). |
| 114 | + |
| 115 | +```@example burgers_comp |
| 116 | +# Use of Adam optimizer: |
| 117 | +res_adam = Optimization.solve(prob_pinn, OptimizationOptimisers.Adam(0.01); callback=callback, maxiters=2000) |
| 118 | +# Use of the BFGS Optimizer: |
| 119 | +prob_bfgs = remake(prob_pinn, u0=res_adam.u) |
| 120 | +@time res_pinn = Optimization.solve(prob_bfgs, BFGS(); callback=callback, maxiters=1000) |
| 121 | +``` |
| 122 | + |
| 123 | +## Results Analysis: |
| 124 | +Generating an animation to visually compare the prediction from the PINN against the ground truth generated by the numerical solver and calculating the global MSE: |
| 125 | +```@example burgers_comp |
| 126 | +ts_mol = sol_mol[t] |
| 127 | +xs_mol = sol_mol[x] |
| 128 | +u_truth_matrix = sol_mol[u(t, x)] |
| 129 | +phi = discretization_pinn.phi |
| 130 | +u_predict_matrix = [first(phi([t_val, x_val], res_pinn.u)) for t_val in ts_mol, x_val in xs_mol] |
| 131 | +mse_global = mean(abs2, u_truth_matrix .- u_predict_matrix) |
| 132 | +println("FINAL GLOBAL MSE: $mse_global") |
| 133 | +anim = @animate for (i, t_val) in enumerate(ts_mol) |
| 134 | + truth_slice = u_truth_matrix[i, :] |
| 135 | + pred_slice = u_predict_matrix[i, :] |
| 136 | + |
| 137 | + p1 = plot(xs_mol, truth_slice, label="MOL Ground Truth", |
| 138 | + lw=3, color=:blue, ylims=(-1.2, 1.2), legend=:topright, |
| 139 | + title="Burgers' (MOL) vs PINN (t = $(round(t_val, digits=2)))") |
| 140 | + plot!(p1, xs_mol, pred_slice, label="PINN Prediction", |
| 141 | + ls=:dash, lw=3, color=:orange) |
| 142 | + |
| 143 | + err_slice = abs.(truth_slice .- pred_slice) |
| 144 | + p2 = plot(xs_mol, err_slice, label="Abs Error", |
| 145 | + color=:red, fill=(0, 0.2, :red), ylims=(0, 0.2), |
| 146 | + xlabel="x", ylabel="Error") |
| 147 | + |
| 148 | + plot(p1, p2, layout=(2, 1)) |
| 149 | +end |
| 150 | +
|
| 151 | +# Saving the gif animation: |
| 152 | +mkpath("assets") |
| 153 | +gif(anim, "assets/burgers_mol_pinn_evolution.gif", fps=15) |
| 154 | +``` |
| 155 | + |
| 156 | + |
| 157 | +## Performance Summary |
| 158 | +While MethodOfLines.jl approach is significantly faster than for this equation, we have demonstrated the ability of a PINN to learn the solution without any mesh generation. This may be useful for solving PDEs in higher dimensions or inverse problems. |
0 commit comments