Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
10 changes: 10 additions & 0 deletions qmat/playgrounds/martin/diff_eqs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

from qmat.playgrounds.martin.diff_eqs.de_solver import DESolver
from qmat.playgrounds.martin.diff_eqs.burgers import Burgers
from qmat.playgrounds.martin.diff_eqs.dahlquist2 import Dahlquist2

__all__ = [
"DESolver",
"Burgers",
"Dahlquist2",
]
159 changes: 159 additions & 0 deletions qmat/playgrounds/martin/diff_eqs/burgers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import numpy as np
from qmat.playgrounds.martin.diff_eqs.de_solver import DESolver


class Burgers(DESolver):
"""
Class to handle the 1D viscous Burgers' equation.
"""

def __init__(self, N: int, nu: float, domain_size: float = 2.0 * np.pi):
# Resolution
self._N: int = N

# Viscosity
self._nu: float = nu

# Domain size
self._domain_size: float = domain_size

# Prepare spectral differentiation values
self._d_dx_ = 1j * np.fft.fftfreq(self._N, d=1.0 / self._N) * 2.0 * np.pi / self._domain_size

def _d_dx(self, u: np.ndarray) -> np.ndarray:
"""Compute the spatial derivative of `u` using spectral methods.

Parameters
----------
u : np.ndarray
Array of shape (N,) representing the solution at the current time step.

Returns
-------
du_dx : np.ndarray
Array of shape (N,) representing the spatial derivative of `u`.
"""
u_hat = np.fft.fft(u)
du_dx_hat = u_hat * self._d_dx_
du_dx = np.fft.ifft(du_dx_hat).real
return du_dx

def initial_u0(self, mode: str) -> np.ndarray:
"""Compute some initial conditions for the 1D viscous Burgers' equation."""

if mode == "sine":
x = np.linspace(0, self._domain_size, self._N, endpoint=False)
u0 = np.sin(x)

elif mode == "hat":
x = np.linspace(0, self._domain_size, self._N, endpoint=False)
u0 = np.where((x >= np.pi / 2) & (x <= 3 * np.pi / 2), 1.0, 0.0)

elif mode == "random":
np.random.seed(42)
u0 = np.random.rand(self._N)

else:
raise ValueError(f"Unknown initial condition mode: {mode}")

return u0

def evalF(self, u: np.ndarray, t: float) -> np.ndarray:
"""
Evaluate the right-hand side of the 1D viscous Burgers' equation.

Parameters
----------
u : np.ndarray
Array of shape (N,) representing the solution at the current time step.

Returns
-------
f : np.ndarray
Array of shape (N,) representing the right-hand side evaluated at `u`.
"""
# Compute spatial derivatives using spectral methods
u_hat = np.fft.fft(u)
du_dx_hat = self._d_dx_ * u_hat
d2u_dx2_hat = (self._d_dx_**2) * u_hat

du_dx = np.fft.ifft(du_dx_hat).real
d2u_dx2 = np.fft.ifft(d2u_dx2_hat).real

f = -u * du_dx + self._nu * d2u_dx2
return f

def u_solution(self, u0: np.ndarray, t: float) -> np.ndarray:
"""
Compute the analytical solution of the 1D viscous Burgers' equation at time `t`.

See
https://gitlab.inria.fr/sweet/sweet/-/blob/6f20b19f246bf6fcc7ace1b69567326d1da78635/src/programs/_pde_burgers/time/Burgers_Cart2D_TS_ln_cole_hopf.cpp

Parameters
----------
u0 : np.ndarray
Array of shape (N,) representing the initial condition.
t : float
Time at which to evaluate the analytical solution.

Returns
-------
u_analytical : np.ndarray
Array of shape (N,) representing the analytical solution at time `t`.
"""

if self._nu < 0.05:
print("Viscosity is very small which can lead to errors in analytical solution!")

u0_hat = np.fft.fft(u0)

# Divide by d/dx operator in spectral space
tmp = np.zeros_like(u0_hat, dtype=complex)
tmp[1:] = u0_hat[1:] / self._d_dx_[1:]

# Back to physical space
phi = np.fft.ifft(tmp).real

# Apply exp(...)
phi = np.exp(-phi / (2 * self._nu))

phi_hat = np.fft.fft(phi)

# Solve directly the heat equation in spectral space with exponential integration
phi_hat = phi_hat * np.exp(self._nu * self._d_dx_**2 * t)

phi = np.fft.ifft(phi_hat)
phi = np.log(phi)

phi_hat = np.fft.fft(phi)

u1_hat = -2.0 * self._nu * phi_hat * self._d_dx_
return np.fft.ifft(u1_hat).real

def test(self):
"""
Run test for currently set up Burgers instance.
"""
x = np.linspace(0, self._domain_size, self._N, endpoint=False)
w = 2.0 * np.pi / self._domain_size
u0 = np.sin(x * w)

u1_analytical = np.cos(x * w) * w
u1_num = self._d_dx(u0)

error: float = np.max(np.abs(u1_num - u1_analytical))

if error > 1e-10:
raise AssertionError(f"Test failed: error {error} too large for domain size {self._domain_size}.")

def run_tests(self):
"""
Run basic tests to verify the correctness of the implementation.

This doesn't change the current instance, but will create new instances.
"""

for domain_size in [2.0 * np.pi, 1.0, 9.0]:
burgers = Burgers(N=128, nu=0.01, domain_size=domain_size)
burgers.test()
55 changes: 55 additions & 0 deletions qmat/playgrounds/martin/diff_eqs/dahlquist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import numpy as np
from qmat.playgrounds.martin.diff_eqs.de_solver import DESolver


class Dahlquist(DESolver):
"""
Standard Dahlquist test equation with two eigenvalues.
Optionally, some external frequency forcing can be added which is
configurable through `ext_scalar`.

u(t) = exp(t*(lam1+lam2))*u(0) + s*sin(t)

d/dt u(t) = (lam1 + lam2) * (u(t) - s*sin(t)) + s*cos(t)
"""

def __init__(self, lam1: complex, lam2: complex, ext_scalar: float = 0.0):
# Lambda 1
self.lam1: float = lam1

# Lambda 2
self.lam2: float = lam2

# Scaling of external sin(t) frequency. Set to 0 to deactivate.
self.ext_scalar = ext_scalar

def initial_u0(self, mode: str = None) -> np.ndarray:
return np.array([1.0 + 0.0j], dtype=np.complex128)

def evalF(self, u: np.ndarray, t: float) -> np.ndarray:
lam = self.lam1 + self.lam2
s = self.ext_scalar
retval = lam * (u - s * np.sin(t)) + s * np.cos(t)

assert retval.shape == u.shape
return retval

def fSolve(self, u: np.ndarray, dt: float, t: float) -> np.ndarray:
t1 = t + dt
lam = self.lam1 + self.lam2
s = self.ext_scalar

rhs = u - s * dt * (lam * np.sin(t1) - np.cos(t1))
retval = rhs / (1.0 - dt * lam)

assert retval.shape == u.shape
return retval

def u_solution(self, u0: np.ndarray, t: float) -> np.ndarray:
lam = self.lam1 + self.lam2
s = self.ext_scalar
assert isinstance(t, float)
retval = np.exp(t * lam) * u0 + s * np.sin(t)

assert retval.shape == u0.shape
return retval
43 changes: 43 additions & 0 deletions qmat/playgrounds/martin/diff_eqs/dahlquist2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import numpy as np
from qmat.playgrounds.martin.diff_eqs.de_solver import DESolver


class Dahlquist2(DESolver):
"""
Modified Dahlquist test equation with superposition of two
frequencies in the solution u(t).

u(t) = 0.5*(exp(lam1*t) + exp(lam2*t)) * u(0)
"""

def __init__(self, lam1: complex, lam2: complex, s: float = 0.6):
"""
:param lam1: First eigenvalue (complex)
:param lam2: Second eigenvalue (complex)
:param s: Weighting between the two exponentials in the solution
"""
self.lam1: complex = lam1
self.lam2: complex = lam2
# Weighting between the two exponentials in the solution
# to avoid division by 0
self.s: float = s

assert 0 <= self.s <= 1, "s must be in [0,1]"

def initial_u0(self, mode: str = None) -> np.ndarray:
return np.array([1.0 + 0.0j], dtype=np.complex128)

def evalF(self, u: np.ndarray, t: float) -> np.ndarray:
retval = (
(self.lam1 * self.s * np.exp(t * self.lam1) + self.lam2 * (1.0 - self.s) * np.exp(t * self.lam2))
/ (self.s * np.exp(t * self.lam1) + (1.0 - self.s) * np.exp(t * self.lam2))
* u
)
assert retval.shape == u.shape
return retval

def u_solution(self, u0: np.ndarray, t: float) -> np.ndarray:
assert isinstance(t, float)
retval = (self.s * np.exp(t * self.lam1) + (1.0 - self.s) * np.exp(t * self.lam2)) * u0
assert retval.shape == u0.shape
return retval
85 changes: 85 additions & 0 deletions qmat/playgrounds/martin/diff_eqs/de_solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from abc import ABC, abstractmethod
import numpy as np


class DESolver(ABC):

@abstractmethod
def evalF(self, u: np.ndarray, t: float) -> np.ndarray:
"""Evaluate the right-hand side of the 1D viscous Burgers' equation.

Parameters
----------
u : np.ndarray
Array of shape (N,) representing the solution at the current time step.
t : float
Current timestamp.
Returns
-------
f : np.ndarray
Array of shape (N,) representing the right-hand side evaluated at `u`.
"""
pass

# This is optional since not every DE might have a solver for backward Euler
# @abstractmethod
def fSolve(self, rhs: np.ndarray, dt: float, t: float) -> np.ndarray:
"""Solve the right-hand side of an equation implicitly.

# Solving this equation implicitly...
u_t = f(u, t)

# ... means to u_new
u_new - dt * F(u_new, t + dt) = rhs

Parameters
----------
rhs : np.ndarray
Right hand as given above.
t : float
Current timestamp.
Future one will be computed as `t + dt`
dt : float
Time step size.

Returns
-------
u_new : np.ndarray
Array of shape (N,) representing the solution at the next time step.
"""
pass

@abstractmethod
def initial_u0(self, mode: str) -> np.ndarray:
"""Compute some initial conditions for the 1D viscous Burgers' equation.

Parameters
----------
mode : str
The type of initial condition to generate.

Returns
-------
u0 : np.ndarray
Array of shape (N,) representing the initial condition.
"""
pass

@abstractmethod
def u_solution(self, u0: np.ndarray, t: float) -> np.ndarray:
"""
Compute the (analytical) solution at time `t`.

Parameters
----------
u0 : np.ndarray
Array of shape (N,) representing the initial condition.
t : float
Time at which to evaluate the solution.

Returns
-------
u_analytical : np.ndarray
Solution at time `t`.
"""
pass
Loading