|
| 1 | +import numpy as np |
| 2 | +from qmat.playgrounds.martin.diff_eqs.de_solver import DESolver |
| 3 | + |
| 4 | + |
| 5 | +class Burgers(DESolver): |
| 6 | + """ |
| 7 | + Class to handle the 1D viscous Burgers' equation. |
| 8 | + """ |
| 9 | + |
| 10 | + def __init__(self, N: int, nu: float, domain_size: float = 2.0 * np.pi): |
| 11 | + # Resolution |
| 12 | + self._N: int = N |
| 13 | + |
| 14 | + # Viscosity |
| 15 | + self._nu: float = nu |
| 16 | + |
| 17 | + # Domain size |
| 18 | + self._domain_size: float = domain_size |
| 19 | + |
| 20 | + # Prepare spectral differentiation values |
| 21 | + self._d_dx_ = 1j * np.fft.fftfreq(self._N, d=1.0 / self._N) * 2.0 * np.pi / self._domain_size |
| 22 | + |
| 23 | + def _d_dx(self, u: np.ndarray) -> np.ndarray: |
| 24 | + """Compute the spatial derivative of `u` using spectral methods. |
| 25 | +
|
| 26 | + Parameters |
| 27 | + ---------- |
| 28 | + u : np.ndarray |
| 29 | + Array of shape (N,) representing the solution at the current time step. |
| 30 | +
|
| 31 | + Returns |
| 32 | + ------- |
| 33 | + du_dx : np.ndarray |
| 34 | + Array of shape (N,) representing the spatial derivative of `u`. |
| 35 | + """ |
| 36 | + u_hat = np.fft.fft(u) |
| 37 | + du_dx_hat = u_hat * self._d_dx_ |
| 38 | + du_dx = np.fft.ifft(du_dx_hat).real |
| 39 | + return du_dx |
| 40 | + |
| 41 | + def initial_u0(self, mode: str) -> np.ndarray: |
| 42 | + """Compute some initial conditions for the 1D viscous Burgers' equation.""" |
| 43 | + |
| 44 | + if mode == "sine": |
| 45 | + x = np.linspace(0, self._domain_size, self._N, endpoint=False) |
| 46 | + u0 = np.sin(x) |
| 47 | + |
| 48 | + elif mode == "hat": |
| 49 | + x = np.linspace(0, self._domain_size, self._N, endpoint=False) |
| 50 | + u0 = np.where((x >= np.pi / 2) & (x <= 3 * np.pi / 2), 1.0, 0.0) |
| 51 | + |
| 52 | + elif mode == "random": |
| 53 | + np.random.seed(42) |
| 54 | + u0 = np.random.rand(self._N) |
| 55 | + |
| 56 | + else: |
| 57 | + raise ValueError(f"Unknown initial condition mode: {mode}") |
| 58 | + |
| 59 | + return u0 |
| 60 | + |
| 61 | + def evalF(self, u: np.ndarray, t: float) -> np.ndarray: |
| 62 | + """ |
| 63 | + Evaluate the right-hand side of the 1D viscous Burgers' equation. |
| 64 | +
|
| 65 | + Parameters |
| 66 | + ---------- |
| 67 | + u : np.ndarray |
| 68 | + Array of shape (N,) representing the solution at the current time step. |
| 69 | +
|
| 70 | + Returns |
| 71 | + ------- |
| 72 | + f : np.ndarray |
| 73 | + Array of shape (N,) representing the right-hand side evaluated at `u`. |
| 74 | + """ |
| 75 | + # Compute spatial derivatives using spectral methods |
| 76 | + u_hat = np.fft.fft(u) |
| 77 | + du_dx_hat = self._d_dx_ * u_hat |
| 78 | + d2u_dx2_hat = (self._d_dx_**2) * u_hat |
| 79 | + |
| 80 | + du_dx = np.fft.ifft(du_dx_hat).real |
| 81 | + d2u_dx2 = np.fft.ifft(d2u_dx2_hat).real |
| 82 | + |
| 83 | + f = -u * du_dx + self._nu * d2u_dx2 |
| 84 | + return f |
| 85 | + |
| 86 | + def int_f(self, u0: np.ndarray, dt: float, t: float = 0.0) -> np.ndarray: |
| 87 | + """ |
| 88 | + Compute the analytical solution of the 1D viscous Burgers' equation at time `t`. |
| 89 | +
|
| 90 | + See |
| 91 | + https://gitlab.inria.fr/sweet/sweet/-/blob/6f20b19f246bf6fcc7ace1b69567326d1da78635/src/programs/_pde_burgers/time/Burgers_Cart2D_TS_ln_cole_hopf.cpp |
| 92 | +
|
| 93 | + Parameters |
| 94 | + ---------- |
| 95 | + u0 : np.ndarray |
| 96 | + Array of shape (N,) representing the initial condition. |
| 97 | + t : float |
| 98 | + Time at which to evaluate the analytical solution. |
| 99 | +
|
| 100 | + Returns |
| 101 | + ------- |
| 102 | + u_analytical : np.ndarray |
| 103 | + Array of shape (N,) representing the analytical solution at time `t`. |
| 104 | + """ |
| 105 | + |
| 106 | + if self._nu < 0.05: |
| 107 | + print("Viscosity is very small which can lead to errors in analytical solution!") |
| 108 | + |
| 109 | + u0_hat = np.fft.fft(u0) |
| 110 | + |
| 111 | + # Divide by d/dx operator in spectral space |
| 112 | + tmp = np.zeros_like(u0_hat, dtype=complex) |
| 113 | + tmp[1:] = u0_hat[1:] / self._d_dx_[1:] |
| 114 | + |
| 115 | + # Back to physical space |
| 116 | + phi = np.fft.ifft(tmp).real |
| 117 | + |
| 118 | + # Apply exp(...) |
| 119 | + phi = np.exp(-phi / (2 * self._nu)) |
| 120 | + |
| 121 | + phi_hat = np.fft.fft(phi) |
| 122 | + |
| 123 | + # Solve directly the heat equation in spectral space with exponential integration |
| 124 | + phi_hat = phi_hat * np.exp(self._nu * self._d_dx_**2 * (t+dt)) |
| 125 | + |
| 126 | + phi = np.fft.ifft(phi_hat) |
| 127 | + phi = np.log(phi) |
| 128 | + |
| 129 | + phi_hat = np.fft.fft(phi) |
| 130 | + |
| 131 | + u1_hat = -2.0 * self._nu * phi_hat * self._d_dx_ |
| 132 | + return np.fft.ifft(u1_hat).real |
| 133 | + |
| 134 | + def test(self): |
| 135 | + """ |
| 136 | + Run test for currently set up Burgers instance. |
| 137 | + """ |
| 138 | + x = np.linspace(0, self._domain_size, self._N, endpoint=False) |
| 139 | + w = 2.0 * np.pi / self._domain_size |
| 140 | + u0 = np.sin(x * w) |
| 141 | + |
| 142 | + u1_analytical = np.cos(x * w) * w |
| 143 | + u1_num = self._d_dx(u0) |
| 144 | + |
| 145 | + error: float = np.max(np.abs(u1_num - u1_analytical)) |
| 146 | + |
| 147 | + if error > 1e-10: |
| 148 | + raise AssertionError(f"Test failed: error {error} too large for domain size {self._domain_size}.") |
| 149 | + |
| 150 | + def run_tests(self): |
| 151 | + """ |
| 152 | + Run basic tests to verify the correctness of the implementation. |
| 153 | +
|
| 154 | + This doesn't change the current instance, but will create new instances. |
| 155 | + """ |
| 156 | + |
| 157 | + for domain_size in [2.0 * np.pi, 1.0, 9.0]: |
| 158 | + burgers = Burgers(N=128, nu=0.01, domain_size=domain_size) |
| 159 | + burgers.test() |
0 commit comments