From 51397ff34d546a4ee3276530ee6da5ee80b50ea6 Mon Sep 17 00:00:00 2001 From: Magnus Ulimoen Date: Tue, 19 May 2020 20:05:31 +0200 Subject: [PATCH] shallow water equations solver --- Cargo.toml | 4 + README.md | 2 +- shallow_water/Cargo.toml | 10 + shallow_water/misc/.gitignore | 6 + shallow_water/misc/fluxes.py | 102 ++++++++ shallow_water/misc/report.tex | 155 ++++++++++++ shallow_water/src/characteristics.rs | 89 +++++++ shallow_water/src/lib.rs | 322 +++++++++++++++++++++++++ webfront/Cargo.toml | 3 + webfront/lib.rs | 60 +++++ webfront/pages/shallowWater/index.html | 15 ++ webfront/pages/shallowWater/main.js | 266 ++++++++++++++++++++ 12 files changed, 1033 insertions(+), 1 deletion(-) create mode 100644 shallow_water/Cargo.toml create mode 100644 shallow_water/misc/.gitignore create mode 100755 shallow_water/misc/fluxes.py create mode 100644 shallow_water/misc/report.tex create mode 100644 shallow_water/src/characteristics.rs create mode 100644 shallow_water/src/lib.rs create mode 100644 webfront/pages/shallowWater/index.html create mode 100644 webfront/pages/shallowWater/main.js diff --git a/Cargo.toml b/Cargo.toml index 18e79a0..d08d3d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,15 @@ members = [ "multigrid", "euler", "maxwell", + "shallow_water", ] [profile.bench] debug = true +[profile.release] +debug = true + [patch] [patch.crates-io] hdf5 = { git = "https://github.com/mulimoen/hdf5-rust.git", branch = "master" } diff --git a/README.md b/README.md index 65aa99f..238850b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # What is it? -This is a test at combining rust+WASM+WebGL+SBP. The prototype can be seen [here (Maxwell solver)](https://ulimoen.dev/physics/websbp/maxwell) and [here (Nonlinear Euler solver)](https://ulimoen.dev/physics/websbp/euler). +This is a test at combining rust+WASM+WebGL+SBP. The prototypes can be seen [here (Maxwell solver)](https://ulimoen.dev/physics/websbp/maxwell), [here (Nonlinear Euler solver)](https://ulimoen.dev/physics/websbp/euler), and [here (shallow water equations)](https://ulimoen.dev/physics/websbp/shallowWater). # Building Run `make_wasm.py` or `make_wasm.py -r` for the release version. diff --git a/shallow_water/Cargo.toml b/shallow_water/Cargo.toml new file mode 100644 index 0000000..618d3e5 --- /dev/null +++ b/shallow_water/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "shallow-water" +version = "0.1.0" +authors = ["Magnus Ulimoen "] +edition = "2018" + +[dependencies] +ndarray = "0.13.1" +sbp = { path = "../sbp" } +log = "0.4.8" diff --git a/shallow_water/misc/.gitignore b/shallow_water/misc/.gitignore new file mode 100644 index 0000000..75a859d --- /dev/null +++ b/shallow_water/misc/.gitignore @@ -0,0 +1,6 @@ +*.aux +*.fdb_latexmk +*.fls +*.log +*.pdf +*.synctex.gz diff --git a/shallow_water/misc/fluxes.py b/shallow_water/misc/fluxes.py new file mode 100755 index 0000000..456ee71 --- /dev/null +++ b/shallow_water/misc/fluxes.py @@ -0,0 +1,102 @@ +#! /usr/bin/env python3 +import sympy as sp +from sympy.utilities.codegen import codegen + +x, y, t = sp.symbols("x,y,t") +rho, g = sp.symbols("rho,g", positive=True, real=True) +# Ignoring rho, assumed to be constant +eta = sp.Symbol("eta", positive=True, real=True) +etau, etav = sp.symbols("etau,etav", real=True) + +u = etau / eta +v = etav / eta +q = sp.Matrix([eta, etau, etav]) + +E = sp.Matrix([eta * u, eta * u ** 2 + g * eta ** 2 / 2, eta * u * v]) +F = sp.Matrix([eta * v, eta * u * v, eta * v ** 2 + g * eta ** 2 / 2]) + +q = sp.Matrix([eta, etau, etav]) +A = E.jacobian(q) +sp.pprint(A) +B = F.jacobian(q) +sp.pprint(B) + +f = sp.symbols("f") +coriolis = sp.Matrix([0, -f * v * eta, +f * u * eta]) +sp.pprint(coriolis) +b = sp.symbols("b") +frictional = sp.Matrix([0, b * u * eta, b * v * eta]) +sp.pprint(frictional) + +print("A:") +# We can also diagonalise the transpose, +# which still gives the same (linearised) system. +# This results in a much nicer formulation for A+ and A- +A = A.T +S, D = A.diagonalize() +print("Diagonals:") +sp.pprint(D[0, 0]) +sp.pprint(D[1, 1]) +sp.pprint(D[2, 2]) +print("S:") +sp.pprint(S) +print("SI:") +sp.pprint(sp.simplify(S.inv())) + +m = abs(etau / eta) + abs(sp.sqrt(eta ** (3)) * sp.sqrt(g) / eta) + +L = D + sp.Matrix([[m, 0, 0], [0, m, 0], [0, 0, m]]) +Aplus = sp.simplify(S * L * S.inv()) / 2 +Aplus = Aplus.T +L = D - sp.Matrix([[m, 0, 0], [0, m, 0], [0, 0, m]]) +Aminus = sp.simplify(S * L * S.inv()) / 2 +Aminus = Aminus.T + +print("A plus:") +sp.pprint(Aplus) + +print("A minus:") +sp.pprint(Aminus) + +print("A S:") +sp.pprint(sp.simplify(Aminus - Aplus)) + +assert sp.simplify(S * D * S.inv() - A) == sp.Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + +print("B:") +S, D = B.diagonalize() +print("Diagonals:") +sp.pprint(D[0, 0]) +sp.pprint(D[1, 1]) +sp.pprint(D[2, 2]) +print("S:") +sp.pprint(S) +print("SI:") +sp.pprint(sp.simplify(S.inv())) + +assert sp.simplify(S * D * S.inv() - B) == sp.Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + +m = abs(etav / eta) + abs(sp.sqrt(eta ** (3)) * sp.sqrt(g) / eta) + +L = D + sp.Matrix([[m, 0, 0], [0, m, 0], [0, 0, m]]) +Bplus = sp.simplify(S * L * S.inv()) / 2 +L = D - sp.Matrix([[m, 0, 0], [0, m, 0], [0, 0, m]]) +Bminus = sp.simplify(S * L * S.inv()) / 2 + +print("B plus:") +sp.pprint(Bplus) + +print("B minus:") +sp.pprint(Bminus) + +print("B S:") +sp.pprint(sp.simplify(Bminus - Bplus)) + +assert sp.simplify((Bplus + Bminus) - B) == sp.Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + +breakpoint() +code = codegen( + [("Aplus", Aplus), ("Aminus", Aminus), ("Bplus", Bplus), ("Bminus", Bminus)], "rust" +) +with open("autogen.rs", "w") as f: + f.write(code[0][1]) diff --git a/shallow_water/misc/report.tex b/shallow_water/misc/report.tex new file mode 100644 index 0000000..62fb832 --- /dev/null +++ b/shallow_water/misc/report.tex @@ -0,0 +1,155 @@ +\documentclass[british]{scrartcl} +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} + +\usepackage{babel} +\usepackage{amsmath} +\usepackage{amsthm} +\usepackage{physics} +\usepackage{cleveref} +\usepackage{csquotes} + +\newtheorem{definition}{Definition} + +\title{Shallow Water Equations} +\author{Magnus Ulimoen} + +\newcommand{\ve}[1]{\mathbf{#1}} +\newcommand{\kron}{\otimes} + + +\begin{document} +\maketitle + +\section{Introduction} + +This is an attempt to solve the shallow water equations (SWE) using the SBP-SAT method. Equations and assumptions are laid out in this document. + + +\section{Formulation} + +The SWE takes the conservative form (assuming constant density) +\begin{gather} + \label{eq:conservative_formulation} + \pdv{\ve{q}}{t} + \pdv{\ve{E}}{x} + \pdv{\ve{F}}{y} = 0\\ + \ve{q} = \begin{bmatrix} \eta \\ \eta u \\ \eta v \end{bmatrix}, \hspace{1em} + \ve{E} = \begin{bmatrix} \eta u \\ \eta u^2 + \frac{1}{2} g \eta^2 \\ \eta u v\end{bmatrix}, \hspace{1em} + \ve{F} = \begin{bmatrix} \eta v \\ \eta u v \\ \eta v^2 + \frac{1}{2} g \eta^2 \end{bmatrix} +\end{gather} +where $\eta$ is height above surface, $u$ fluid motion along $x$, $v$ fluid motion along $y$, $g$ acceleration due to gravity. + +The equation is discretised in space on a regular cartesian grid, and the operators $\pdv{x}$ is approximated using the SBP operator $D_1$ (likewise for $\pdv{y}$). This operator obeys +\begin{definition} + An operator $D_1$ approximating $\pdv{x}$ on the form + \begin{gather*} + D_1 = H^{-1} \left( Q + \frac{1}{2} B \right) + \end{gather*} + where + \begin{gather*} + Q + Q^T = 0, \\ + B = -e_0 e_0^T + e_n e_n^T, \\ + H = H^T, \\ + x^T H x > 0\,\,\, \forall x \neq 0 + \end{gather*} + is an SBP operator +\end{definition} + +Applying this to a discretised version of \cref{eq:conservative_formulation}: +\begin{gather} + \pdv{\ve{q}}{t} + D_x \ve{E} + D_y \ve{F} = 0 +\end{gather} +where +\begin{gather} + D_x = I_3 \kron I_y \kron D_1\\ + D_y = I_3 \kron D_1 \kron I_x +\end{gather} +and $\ve{q}$ is a \enquote{flattening} of the vector (x, y, the three fields). This formulation can be discretised in time using some appropriate scheme (eg. Runge Kutta 4). + +\subsection{Energy stability} +To ensure stability, we must ensure no variable grows without bounds. First the continous case, taking the inner product left and right with q (and shifting stuff): +\begin{gather} + \left(q, \pdv{q}{t}\right) + \left(\pdv{q}{t}, q\right) + = - \left(q, \pdv{\ve{E}}{x} + \pdv{\ve{F}}{y}\right) + - \left(\pdv{\ve{E}}{x} + \pdv{\ve{F}}{y}, q\right) + = \frac{1}{2}\pdv{q^2}{t} +\end{gather} +Let us linearise this equation, with +\begin{gather} + A = \pdv{E}{q}, \hspace{1em} B = \pdv{F}{q} +\end{gather} +which gives us +\begin{align} + \nonumber \left(q, \pdv{q}{t}\right) &= - \left( q, A \pdv{q}{x} + B \pdv{q}{x} \right) \\ + \nonumber &= {\left(q_x, A q\right)} - {\left[ q^T A q \right]}_{x_0}^{x_n} + + {\left(q, B q_y \right)} - {\left[ q^T B q \right]}_{y_0}^{y_n} \\ + &= {\left(A^T q_x, q\right)} - {\left[ q^T A q \right]}_{x_0}^{x_n} + + {\left(B^T q, q_y \right)} - {\left[ q^T B q \right]}_{y_0}^{y_n} +\end{align} +and the following +\begin{align} + \nonumber \frac{1}{2}\pdv{q^2}{t} &= \left( q, \pdv{q}{t} \right) + \left( \pdv{q}{t} \right) \\ + &= (q_x, Aq) - (Aq_x, q) - \left[q^T (A + A^T) q\right] + (q_y, B q) - (B q_y, q) - [q^T(B + B^T)q] +\end{align} + +By symmetrising and changing variables ($\hat{q} = Sq$) we can find a suitable form which allows all the $(q, F) - (F, q)$ forms to disappear. It might not be fully symmetrisable in both $x,y$ simultaneously, but this can be skippped\ldots +Change of coordinates can be done within the integral, +\begin{gather} + q^T A q_x, \hspace{1em} \hat{q} = Sq, \hspace{1em} S^T \hat{q} = q \\ + q^T A q_x = {(S^T \hat{q})}^T A (S^T \hat{q}) = \hat{q}^T S S^T D S S^T \hat{q} = \hat{q}D\hat{q} +\end{gather} +This does not change anything within the integral, and shows the symmetrisation necessary to make the two forms cancel. The energy is therefore bounded by the boundary terms (which are omitted in the continous form). It is enough to bound +\begin{gather} + A^- \text{on the right} \\ + A^+ \text{on the left} \\ + B^- \text{on the top} \\ + B^+ \text{on the bottom} +\end{gather} + +\subsubsection{Discrete case} +In this section we will determine the penalty parameter $\tau$ for all directions. +\begin{gather} + \pdv{q}{t} = - D_x (Aq) - D_y (Aq) \\ +\end{gather} +\begin{align} + {\left(q, \pdv{q}{t}\right)}_H &= q^T (H_y \kron H_x) \pdv{q}{t} \\ + &= - q^T H D_x (Aq) - q^T H D_y (Aq) \\ + &= -q^T (H_y \kron H_x) (I_y \kron H^{-1} (Q + \frac{1}{2}B )) (I_y \kron A) q + q^T \\ + &- q^T (H_y \kron H_x) (H^{-1} (Q + \frac{1}{2}B ) \kron I_x) (B \kron I_x) q + q^T +\end{align} + +Let us just do this in one dimension, it is mostly transferable anyway\ldots +\begin{align} + q^T H \pdv{q}{t} &= -q^T H H^{-1} (Q + \frac{1}{2}B) A q \\ + &= - q^T (Q + \frac{1}{2} B ) A q +\end{align} +And the transpose +\begin{align} + {\left(\pdv{q}{t}\right)}^T H q = -{(Aq)}^T H D_1 q = -q^T A^T (Q + \frac{1}{2}B) q +\end{align} +Tranpose of this gives (can always transpose a scalar) +\begin{align} + q^T {(A^T (Q + \frac{1}{2}B))}^T q = q^T (Q^T + \frac{1}{2}B^T) A q +\end{align} +The sum of these two expressions +\begin{gather} + \frac{1}{2}\pdv{\norm{q}^2_H}{t} = q^T (Q + Q^T + \frac{1}{2}(B + B^T)) A q \\ + = q^T B A q = -q_0^T A q_0 + q_n^T A q_n +\end{gather} +We can thus control the energy rate by controlling $q_0^T A q_0$ and $q_n^T A q_n$ (that is, we set the boundaries). We do this by adding the following SAT\@. +\begin{gather} + SAT = \tau_0 H^{-1} e_0 e_0^T A_- (q - v) + \tau_n H^{-1} e_n e_n^T A_+ (q - v) +\end{gather} +Adding this to the energy form above (setting $v=0$ (data independence)) +\begin{gather} + E = -q_0^T A q_0 + \tau_0 q_0^T A_- q_0 + \tau_0 q_0^T A_-^T q_0 + + q_n^T A q_n + \ldots \\ + = -q_0^T (A - 2\tau_0 A_-) q_0 + \ldots +\end{gather} +This sets the following requirements +\begin{gather} + \tau_0 \ge \frac{1}{2} \\ + \tau_n \le -\frac{1}{2} +\end{gather} + + +\end{document} diff --git a/shallow_water/src/characteristics.rs b/shallow_water/src/characteristics.rs new file mode 100644 index 0000000..b8c2559 --- /dev/null +++ b/shallow_water/src/characteristics.rs @@ -0,0 +1,89 @@ +#![allow(non_snake_case)] +/* + * Code generated with sympy 1.7.dev + * + * See http://www.sympy.org/ for more information. + * + * This file is part of 'project' + */ +use super::Float; + +pub fn Aplus(eta: Float, etau: Float, etav: Float, g: Float) -> [[Float; 3]; 3] { + [ + [ + (eta.powf(3.0 / 2.0) * g.sqrt() + etau.abs()) / (2.0 * eta), + 1.0 / 2.0, + 0.0, + ], + [ + eta * g / 2.0 - etau.powi(2) / (2.0 * eta.powi(2)), + (eta.powf(3.0 / 2.0) * g.sqrt() + 2.0 * etau + etau.abs()) / (2.0 * eta), + 0.0, + ], + [ + -etau * etav / (2.0 * eta.powi(2)), + etav / (2.0 * eta), + (eta.powf(3.0 / 2.0) * g.sqrt() + etau + etau.abs()) / (2.0 * eta), + ], + ] +} + +pub fn Aminus(eta: Float, etau: Float, etav: Float, g: Float) -> [[Float; 3]; 3] { + [ + [ + -(eta.powf(3.0 / 2.0) * g.sqrt() + etau.abs()) / (2.0 * eta), + 1.0 / 2.0, + 0.0, + ], + [ + eta * g / 2.0 - etau.powi(2) / (2.0 * eta.powi(2)), + (-eta.powf(3.0 / 2.0) * g.sqrt() + 2.0 * etau - etau.abs()) / (2.0 * eta), + 0.0, + ], + [ + -etau * etav / (2.0 * eta.powi(2)), + etav / (2.0 * eta), + (-eta.powf(3.0 / 2.0) * g.sqrt() + etau - etau.abs()) / (2.0 * eta), + ], + ] +} + +pub fn Bplus(eta: Float, etau: Float, etav: Float, g: Float) -> [[Float; 3]; 3] { + [ + [ + (eta.powf(3.0 / 2.0) + etav.abs()) / (2.0 * eta), + 0.0, + 1.0 / 2.0, + ], + [ + -etau * etav / (2.0 * eta.powi(2)), + (eta.powf(3.0 / 2.0) * g.sqrt() + etav + etav.abs()) / (2.0 * eta), + etau / (2.0 * eta), + ], + [ + eta * g / 2.0 - etav.powi(2) / (2.0 * eta.powi(2)), + 0.0, + (eta.powf(3.0 / 2.0) * g.sqrt() + 2.0 * etav + etav.abs()) / (2.0 * eta), + ], + ] +} + +pub fn Bminus(eta: Float, etau: Float, etav: Float, g: Float) -> [[Float; 3]; 3] { + [ + [ + -(eta.powf(3.0 / 2.0) + etav.abs()) / (2.0 * eta), + 0.0, + 1.0 / 2.0, + ], + [ + -etau * etav / (2.0 * eta.powi(2)), + (-eta.powf(3.0 / 2.0) * g.sqrt() + etav - etav.abs()) / (2.0 * eta), + etau / (2.0 * eta), + ], + [ + eta * g / 2.0 - etav.powi(2) / (2.0 * eta.powi(2)), + 0.0, + (-eta.powf(3.0 / 2.0) * g.sqrt() + 2.0 * etav - etav.abs()) / (2.0 * eta), + ], + ] +} diff --git a/shallow_water/src/lib.rs b/shallow_water/src/lib.rs new file mode 100644 index 0000000..46f02f8 --- /dev/null +++ b/shallow_water/src/lib.rs @@ -0,0 +1,322 @@ +use ndarray::prelude::*; +use sbp::*; + +mod characteristics; +use characteristics::{Aminus, Aplus, Bminus, Bplus}; + +const G: Float = 1.0; + +#[derive(Clone, Debug)] +pub struct Field(Array3); + +impl<'a> Into> for &'a Field { + fn into(self) -> ArrayView3<'a, Float> { + self.0.view() + } +} + +impl<'a> Into> for &'a mut Field { + fn into(self) -> ArrayViewMut3<'a, Float> { + self.0.view_mut() + } +} + +impl Field { + fn new(ny: usize, nx: usize) -> Self { + Self(Array3::zeros((3, ny, nx))) + } + fn eta(&self) -> ArrayView2 { + self.0.slice(s![0, .., ..]) + } + fn etau(&self) -> ArrayView2 { + self.0.slice(s![1, .., ..]) + } + fn etav(&self) -> ArrayView2 { + self.0.slice(s![2, .., ..]) + } + /* + fn eta_mut(&mut self) -> ArrayViewMut2 { + self.0.slice_mut(s![0, .., ..]) + } + fn etau_mut(&mut self) -> ArrayViewMut2 { + self.0.slice_mut(s![1, .., ..]) + } + fn etav_mut(&mut self) -> ArrayViewMut2 { + self.0.slice_mut(s![2, .., ..]) + } + */ + fn components_mut( + &mut self, + ) -> ( + ArrayViewMut2, + ArrayViewMut2, + ArrayViewMut2, + ) { + self.0 + .multi_slice_mut((s![0, .., ..], s![1, .., ..], s![2, .., ..])) + } +} + +pub struct System { + fnow: Field, + fnext: Field, + x: (Float, Float, usize), + y: (Float, Float, usize), + op: Box, + k: [Field; 4], +} + +impl System { + pub fn new(x: (Float, Float, usize), y: (Float, Float, usize)) -> Self { + let field = Field::new(y.2, x.2); + Self { + fnow: field.clone(), + fnext: field.clone(), + x, + y, + op: Box::new(operators::Upwind9), + k: [field.clone(), field.clone(), field.clone(), field], + } + } + pub fn nx(&self) -> usize { + self.x.2 + } + pub fn ny(&self) -> usize { + self.y.2 + } + pub fn eta(&self) -> ArrayView2 { + self.fnow.eta() + } + pub fn etau(&self) -> ArrayView2 { + self.fnow.etau() + } + pub fn etav(&self) -> ArrayView2 { + self.fnow.etav() + } + pub fn components_mut( + &mut self, + ) -> ( + ArrayViewMut2, + ArrayViewMut2, + ArrayViewMut2, + ) { + self.fnow.components_mut() + } + + fn max_dt(&self) -> Float { + 0.1 * ((self.x.1 - self.x.0) / self.x.2 as Float) + .min((self.y.1 - self.y.0) / self.y.2 as Float) + } + + pub fn advance(&mut self) { + let max_dt = self.max_dt(); + let op = &self.op; + let rhs = move |next: &mut Field, now: &Field, _t: Float| { + let (mut next_eta, mut next_etau, mut next_etav) = next.components_mut(); + next_eta.fill(0.0); + next_etau.fill(0.0); + next_etav.fill(0.0); + + let nx = next_eta.shape()[1]; + let ny = next_eta.shape()[0]; + + if false { + let eta = now.eta(); + for j in 0..ny { + for i in 0..nx { + if eta[(j, i)] <= 0.0 { + panic!("{} {}", j, i); + } + } + } + } + + let mut temp = Array2::::zeros((ny, nx)); + + // E flux + let mut temp_dx = temp.clone(); + azip!((dest in &mut temp, etau in now.etau()) { + *dest = *etau; + }); + op.diffxi(temp.view(), temp_dx.view_mut()); + next_eta.scaled_add(-1.0, &temp_dx); + + azip!((dest in &mut temp, eta in now.eta(), etau in now.etau()) { + *dest = etau.powi(2)/eta + G*eta.powi(2)/2.0 + }); + op.diffxi(temp.view(), temp_dx.view_mut()); + next_etau.scaled_add(-1.0, &temp_dx); + + azip!((dest in &mut temp, eta in now.eta(), etau in now.etau(), etav in now.etav()) { + *dest = etau*etav/eta + }); + op.diffxi(temp.view(), temp_dx.view_mut()); + next_etav.scaled_add(-1.0, &temp_dx); + + // F flux + let mut temp_dy = temp_dx; + azip!((dest in &mut temp, etav in now.etav()) { + *dest = *etav; + }); + op.diffeta(temp.view(), temp_dy.view_mut()); + next_eta.scaled_add(-1.0, &temp_dy); + + azip!((dest in &mut temp, eta in now.eta(), etau in now.etau(), etav in now.etav()) { + *dest = etau*etav/eta; + }); + op.diffeta(temp.view(), temp_dy.view_mut()); + next_etau.scaled_add(-1.0, &temp_dy); + + azip!((dest in &mut temp, eta in now.eta(), etav in now.etav()) { + *dest = etav.powi(2)/eta + G*eta.powi(2)/2.0 + }); + op.diffeta(temp.view(), temp_dy.view_mut()); + next_etav.scaled_add(-1.0, &temp_dy); + + // Upwind dissipation + if false { + let mut temp_dx = temp_dy; + azip!((dest in &mut temp, eta in now.eta(), etau in now.etau()) { + *dest = -(eta.powf(3.0/2.0)*G.sqrt() + etau.abs())/eta + }); + op.dissxi(temp.view(), temp_dx.view_mut()); + azip!((dest in &mut next_eta, eta in now.eta(), diss in &temp_dx) { + *dest -= eta*diss; + }); + azip!((dest in &mut next_etau, etau in now.etau(), diss in &temp_dx) { + *dest -= etau*diss; + }); + azip!((dest in &mut next_etav, etav in now.etav(), diss in &temp_dx) { + *dest -= etav*diss; + }); + + let mut temp_dy = temp_dx; + azip!((dest in &mut temp, eta in now.eta(), etav in now.etav()) { + *dest = -(eta.powf(3.0/2.0)*G.sqrt() + etav.abs())/eta + }); + op.disseta(temp.view(), temp_dy.view_mut()); + azip!((dest in &mut next_eta, eta in now.eta(), diss in &temp_dy) { + *dest -= eta*diss; + }); + azip!((dest in &mut next_etau, etau in now.etau(), diss in &temp_dy) { + *dest -= etau*diss; + }); + azip!((dest in &mut next_etav, etav in now.etav(), diss in &temp_dy) { + *dest -= etav*diss; + }); + } + + // SAT boundaries + #[derive(Debug)] + enum Direction { + North, + South, + East, + West, + } + for dir in &[ + Direction::North, + Direction::South, + Direction::East, + Direction::West, + ] { + let mut dest; + let this; + let other; + match dir { + Direction::North => { + dest = next.0.slice_mut(s![.., ny - 1, ..]); + this = now.0.slice(s![.., ny - 1, ..]); + other = now.0.slice(s![.., 0_usize, ..]); + } + Direction::South => { + dest = next.0.slice_mut(s![.., 0_usize, ..]); + this = now.0.slice(s![.., 0_usize, ..]); + other = now.0.slice(s![.., ny - 1, ..]); + } + Direction::East => { + dest = next.0.slice_mut(s![.., .., nx - 1]); + this = now.0.slice(s![.., .., nx - 1]); + other = now.0.slice(s![.., .., 0_usize]); + } + Direction::West => { + dest = next.0.slice_mut(s![.., .., 0_usize]); + this = now.0.slice(s![.., .., 0_usize]); + other = now.0.slice(s![.., .., nx - 1]); + } + } + for ((mut dest, this), other) in dest + .axis_iter_mut(Axis(1)) + .zip(this.axis_iter(Axis(1))) + .zip(other.axis_iter(Axis(1))) + { + let tau = match dir { + Direction::North => 1.0, + Direction::South => -1.0, + Direction::East => 1.0, + Direction::West => -1.0, + }; + let hinv = match dir { + Direction::North | Direction::South => { + if op.is_h2eta() { + (ny - 2) as Float / op.heta()[0] + } else { + (ny - 1) as Float / op.heta()[0] + } + } + Direction::East | Direction::West => { + if op.is_h2xi() { + (nx - 2) as Float / op.hxi()[0] + } else { + (nx - 1) as Float / op.hxi()[0] + } + } + }; + + let v = (this[0], this[1], this[2]); + let g = (other[0], other[1], other[2]); + let mat = match dir { + Direction::West => Aplus(v.0, v.1, v.2, G), + Direction::East => Aminus(v.0, v.1, v.2, G), + Direction::North => Bminus(v.0, v.1, v.2, G), + Direction::South => Bplus(v.0, v.1, v.2, G), + }; + + let q = [v.0 - g.0, v.1 - g.1, v.2 - g.2]; + + let mut res = [0.0; 3]; + for j in 0..3 { + for i in 0..3 { + res[j] += mat[j][i] * q[i]; + } + } + + for i in 0..3 { + dest[i] += tau * hinv * res[i]; + } + } + } + log::trace!("Iteration complete"); + }; + integrate::integrate::( + rhs, + &self.fnow, + &mut self.fnext, + &mut 0.0, + max_dt, + &mut self.k[..], + ); + + std::mem::swap(&mut self.fnow, &mut self.fnext) + } +} + +#[test] +fn test_advance() { + let mut sys = System::new((0.0, 1.0, 50), (0.0, 1.0, 50)); + sys.fnow.components_mut().0.fill(1.0); + for _ in 0..10 { + sys.advance(); + } + println!("{:?}", sys.fnow); +} diff --git a/webfront/Cargo.toml b/webfront/Cargo.toml index ddd02d7..7aadeda 100644 --- a/webfront/Cargo.toml +++ b/webfront/Cargo.toml @@ -16,3 +16,6 @@ sbp = { path = "../sbp", features = ["f32"] } ndarray = "0.13.0" euler = { path = "../euler" } maxwell = { path = "../maxwell" } +shallow-water = { path = "../shallow_water" } +console_log = "0.2.0" +log = "0.4.8" diff --git a/webfront/lib.rs b/webfront/lib.rs index 71ef3d4..867de1d 100644 --- a/webfront/lib.rs +++ b/webfront/lib.rs @@ -13,6 +13,14 @@ pub fn set_panic_hook() { console_error_panic_hook::set_once(); } +#[wasm_bindgen] +pub fn set_console_logger() { + use std::sync::Once; + static LOG_INIT: Once = Once::new(); + + LOG_INIT.call_once(|| console_log::init_with_level(log::Level::Trace).unwrap()); +} + #[wasm_bindgen] pub struct MaxwellUniverse(maxwell::System); @@ -119,3 +127,55 @@ fn start_and_advance_upwind_euler() { universe.advance_upwind(0.01); } } + +#[wasm_bindgen] +pub struct ShallowWaterUniverse(shallow_water::System); + +#[wasm_bindgen] +impl ShallowWaterUniverse { + #[wasm_bindgen(constructor)] + pub fn new(height: usize, width: usize) -> Self { + let x = (0.0, 1.0, width); + let y = (0.0, 1.0, height); + Self(shallow_water::System::new(x, y)) + } + + pub fn init(&mut self, x0: f32, y0: f32) { + let nx = self.0.nx(); + let ny = self.0.ny(); + let x = ndarray::Array1::linspace(0.0, 1.0, nx); + let y = ndarray::Array1::linspace(0.0, 1.0, ny); + + let (mut eta, mut etau, mut etav) = self.0.components_mut(); + + let sigma = 0.1; + + for j in 0..ny { + for i in 0..nx { + let r = f32::hypot(x[i] - x0, y[j] - y0); + + let f = 1.0 / (sigma * (2.0 * sbp::consts::PI).sqrt()) + * f32::exp(-0.5 * (r / sigma).powi(2)); + eta[(j, i)] = 1.0 - 0.1 * f; + etau[(j, i)] = eta[(j, i)] * 0.0; + etav[(j, i)] = eta[(j, i)] * 0.0; + } + } + } + + pub fn advance(&mut self) { + self.0.advance() + } + + pub fn get_eta_ptr(&self) -> *const u8 { + self.0.eta().as_ptr() as *const u8 + } + + pub fn get_etau_ptr(&self) -> *const u8 { + self.0.etau().as_ptr() as *const u8 + } + + pub fn get_etav_ptr(&self) -> *const u8 { + self.0.etav().as_ptr() as *const u8 + } +} diff --git a/webfront/pages/shallowWater/index.html b/webfront/pages/shallowWater/index.html new file mode 100644 index 0000000..80bc6d5 --- /dev/null +++ b/webfront/pages/shallowWater/index.html @@ -0,0 +1,15 @@ + + + + + + + + Shallow Water solver (ΣBP) + + + + + + + diff --git a/webfront/pages/shallowWater/main.js b/webfront/pages/shallowWater/main.js new file mode 100644 index 0000000..865d6de --- /dev/null +++ b/webfront/pages/shallowWater/main.js @@ -0,0 +1,266 @@ +import { ShallowWaterUniverse, default as init, set_panic_hook as setPanicHook, set_console_logger as setConsoleLogger } from "../sbp_web.js"; + +/** + * Initialises and runs the Maxwell solver, + * plotting the solution to a canvas using webgl + */ +(async function run() { + const wasm = await init("../sbp_web_bg.wasm"); + setPanicHook(); + setConsoleLogger(); + + const canvas = document.getElementById("glCanvas"); + + const gl = canvas.getContext("webgl"); + if (gl === null) { + console.error("Unable to initialise WebGL"); + return; + } + + const vsSource = String.raw` + #version 100 + attribute mediump float aX; + attribute mediump float aY; + attribute mediump float aField; + + uniform vec4 uBbox; + + varying mediump float vField; + + void main() { + vField = aField; + mediump float x = (aX - uBbox.x)*uBbox.y; + mediump float y = (aY - uBbox.z)*uBbox.w; + gl_Position = vec4(2.0*x - 1.0, 2.0*y - 1.0, 1.0, 1.0); + } + `; + + const fsSource = String.raw` + #version 100 + varying mediump float vField; + + uniform int uChosenField; + + void main() { + mediump float r = 0.0; + mediump float g = 0.0; + mediump float b = 0.0; + + if (uChosenField == 0) { + r = 0.5*vField; + } else if (uChosenField == 1) { + g = (vField + 1.0)/2.0; + } else { + b = vField + 0.5; + } + gl_FragColor = vec4(r, g, b, 1.0); + } + `; + + const vsShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vsShader, vsSource); + gl.compileShader(vsShader); + if (!gl.getShaderParameter(vsShader, gl.COMPILE_STATUS)) { + console.error(`Could not compile shader: ${gl.getShaderInfoLog(vsShader)}`); + return; + } + + const fsShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fsShader, fsSource); + gl.compileShader(fsShader); + if (!gl.getShaderParameter(fsShader, gl.COMPILE_STATUS)) { + console.error(`Could not compile shader: ${gl.getShaderInfoLog(fsShader)}`); + return; + } + + const shaderProgram = gl.createProgram(); + gl.attachShader(shaderProgram, vsShader); + gl.attachShader(shaderProgram, fsShader); + gl.linkProgram(shaderProgram); + gl.validateProgram(shaderProgram); + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { + console.error(`Unable to link shader program: ${gl.getProgramInfoLog(shaderProgram)}`); + return; + } + gl.useProgram(shaderProgram); + + // A nice pink to show missing values + gl.clearColor(1.0, 0.753, 0.796, 1.0); + gl.clearDepth(1.0); + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + gl.disable(gl.CULL_FACE); + + console.info("Successfully set OpenGL state"); + + + const width = 70; + const height = 70; + + + const x = new Float32Array(width * height); + const y = new Float32Array(width * height); + for (let j = 0; j < height; j += 1) { + for (let i = 0; i < width; i += 1) { + const n = width*j + i; + x[n] = i / (width - 1.0); + y[n] = j / (height - 1.0); + } + } + + const universe = new ShallowWaterUniverse(height, width); + + + // Transfer x, y to cpu, prepare fBuffer + const xBuffer = gl.createBuffer(); + const yBuffer = gl.createBuffer(); + const fBuffer = gl.createBuffer(); + { + const numcomp = 1; + const type = gl.FLOAT; + const normalise = false; + const stride = 0; + const offset = 0; + + let loc = gl.getAttribLocation(shaderProgram, "aX"); + gl.bindBuffer(gl.ARRAY_BUFFER, xBuffer); + gl.vertexAttribPointer(loc, numcomp, type, normalise, stride, offset); + gl.enableVertexAttribArray(loc); + gl.bufferData(gl.ARRAY_BUFFER, x, gl.STATIC_DRAW); + + loc = gl.getAttribLocation(shaderProgram, "aY"); + gl.bindBuffer(gl.ARRAY_BUFFER, yBuffer); + gl.vertexAttribPointer(loc, numcomp, type, normalise, stride, offset); + gl.enableVertexAttribArray(loc); + gl.bufferData(gl.ARRAY_BUFFER, y, gl.STATIC_DRAW); + + loc = gl.getAttribLocation(shaderProgram, "aField"); + gl.bindBuffer(gl.ARRAY_BUFFER, fBuffer); + gl.vertexAttribPointer(loc, numcomp, type, normalise, stride, offset); + gl.enableVertexAttribArray(loc); + } + + // Create triangles covering the domain + const positions = new Int16Array((width-1)*(height-1)*2*3); + for (let j = 0; j < height - 1; j += 1) { + for (let i = 0; i < width - 1; i += 1) { + const n = 2*3*((width-1)*j + i); + positions[n+0] = width*j + i; + positions[n+1] = width*j + i+1; + positions[n+2] = width*(j+1) + i; + positions[n+3] = width*j + i+1; + positions[n+4] = width*(j+1) + i; + positions[n+5] = width*(j+1) + i+1; + } + } + + const indexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, positions, gl.STATIC_DRAW); + + const bbox = [+Number(Infinity), -Number(Infinity), +Number(Infinity), -Number(Infinity)]; + for (let i = 0; i < width*height; i += 1) { + bbox[0] = Math.min(bbox[0], x[i]); + bbox[1] = Math.max(bbox[1], x[i]); + bbox[2] = Math.min(bbox[2], y[i]); + bbox[3] = Math.max(bbox[3], y[i]); + } + + { + const loc = gl.getUniformLocation(shaderProgram, "uBbox"); + gl.uniform4f(loc, bbox[0], 1.0/(bbox[1] - bbox[0]), bbox[2], 1.0/(bbox[3] - bbox[2])); + } + + const TIMEFACTOR = 10000.0; + const MAX_DT = 1.0/Math.max(width, height); + + let t = 0; + let firstDraw = true; + let warnTime = -1; + const chosenField = { + "uLocation": gl.getUniformLocation(shaderProgram, "uChosenField"), + "value": 2, + "cycle": function cycle() { + if (this.value === 0) { + this.value = 1; + } else if (this.value === 1) { + this.value = 2; + } else { + this.value = 0; + } + gl.uniform1i(this.uLocation, this.value); + } + }; + chosenField.cycle(); + + universe.init(0.5, 0.5); + + /** + * Integrates and draws the next iteration + * @param {Time} timeOfDraw Time of drawing + */ + function drawMe(timeOfDraw) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + let dt = (timeOfDraw - t) / TIMEFACTOR; + t = timeOfDraw; + if (firstDraw || dt <= 0.0) { + firstDraw = false; + dt = MAX_DT; + } else { + if (dt >= MAX_DT) { + warnTime += 1; + if (warnTime !== -2) { + console.warn("Can not keep up with framerate"); + } + dt = MAX_DT; + } + } + + let fieldPtr; + if (chosenField.value === 0) { + fieldPtr = universe.get_eta_ptr(); + } else if (chosenField.value === 1) { + fieldPtr = universe.get_etau_ptr(); + } else { + fieldPtr = universe.get_etav_ptr(); + } + const field = new Float32Array(wasm.memory.buffer, fieldPtr, width*height); + gl.bufferData(gl.ARRAY_BUFFER, field, gl.DYNAMIC_DRAW); + + { + const offset = 0; + const type = gl.UNSIGNED_SHORT; + const vertexCount = positions.length; + gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + } + + universe.advance(); + + window.requestAnimationFrame(drawMe); + } + + // https://stackoverflow.com/questions/4288253/html5-canvas-100-width-height-of-viewport#8486324 + function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + } + + window.addEventListener("keyup", event => { + if (event.key === "c") { + chosenField.cycle(); + } + }, {"passive": true}); + window.addEventListener("resize", resizeCanvas, false); + window.addEventListener("click", event => { + // Must adjust for bbox and transformations for x/y + const mousex = event.clientX / window.innerWidth; + const mousey = event.clientY / window.innerHeight; + universe.init(mousex, 1.0 - mousey); + }, {"passive": true}); + + resizeCanvas(); + window.requestAnimationFrame(drawMe); +}());