This demo example shows ...
In [ ]:
%pylab inline
Se også notebook for den eksplisitte løsningen. Vi ønsker nå å løse varmelikningen implisitt,
$\frac{\delta u}{\delta t} = \kappa \frac{\delta^2u}{\delta x^2}$.
Diskretisert implisitt med hensyn på tid får vi
$\frac{1}{\Delta t}\left(u_i^{k} - u_i^{k-1}\right) = \frac{\kappa}{\Delta x^2} \left(u^k_{i-1} - 2u_i^k + u_{i+1}^k \right)$
og vår stensil
$u_i^{k-1} = -su^k_{i-1} +(1+2s)u_i^k - su_{i+1}^k , \quad s=\frac{\kappa\Delta t}{\Delta x^2}$.
Om vi lar ("flux-koeffisienter") $F_i = (L_i, R_i)$ være s.a. flux fra celle $i$ til celle $i+1$ er gitt ved $L_i\cdot u_i + R_i\cdot u_{i+1}$, kan vi skrive dette som
$-s\cdot F^n_{i-1} + u_i^n + s\cdot F^n_i = u_i^{n-1}$,
som illustrerer sammenhengen mellom matrisa for likningssystemet og fluksene samt verdiene $u_i^n$ for løsningen.
In [ ]:
import numpy as np
import time
import matplotlib.pyplot as plt
from IPython.display import clear_output
# Grid spacing
dx = 0.5
# Heat diffusion constant
kappa = 0.3
# Maximum time step size (constrained by CFL)
dt = dx*dx/(2*kappa)
# Hmm... That was the dt from the explicit case... Setting it a bit larger now:
dt = 5.0;
# Number of grid cells
n = 6
# Boundary conditions
r0 = 0.5
r1 = 1.5
s = kappa * dt / (dx*dx)
# Coefficients for fluxes, one flux for each face, i.e., n+1 of these, and two coefficients for each flux,
# in essence a delta for face indices, resulting in cell indices... ugh.
f = np.zeros( (n+1, 2) )
for i in range(0, n+1):
f[i] = np.array( [1, -1] )
f[0] = np.array( [nan, nan] ) # should not be used
f[n] = np.array( [nan, nan] ) # should not be used
In [ ]:
# Plot or not
pl = False
if (pl):
fig, ax = plt.subplots()
In [ ]:
# Initial temperatures
u = np.linspace(0.0, 0.0, n)
u[0] = r0
u[n-1] = r1
A = np.zeros( (n, n) )
# Assemble the matrix one cell at a time. (This loops over interior cells, for indices 1, ..., n-2 inclusive.)
for i in range(1, n-1):
# The equation system is: -s(u_{i-1}^n - u_i^n) + u_i^n +s(u_i^n - u_{i+1}^n) = u_i^{n-1}, for i=0, ..., n-1.
# With f[i] = [1, -1], we get the following.
A[i, i-1:i+1] += -s*f[i]
A[i, i ] += 1
A[i, i:i+2] += s*f[i+1]
# Apply boundary conditions, # u_i^n = u_i^n-1, for i=0 and i=n-1.
A[0, 0] = 1
A[n-1, n-1] = 1
In [ ]:
if (not(pl)):
print "A =\n", A
print -1, "u =", u
for k in range(0, 20):
# Perform one timestep
# Solve
v = np.linalg.solve(A, u);
# Swap, for the next round
u, v = v, u
if (pl):
# Plot the solution
x = np.linspace(0.0, n-1, n) + 0.5
plot(x, u, 'r.-', label='$u^{k+1}$')
pylab.ylim([r0-0.5,r1+0.5])
#time.sleep(0.2)
clear_output()
display(fig)
ax.cla()
title("Time = " + str(dt*k))
legend()
else:
print k, "u =", u
if (pl):
plt.close()
Ok, what can we imagine a DSL-program for this would look like?
In [ ]:
%%file 1D_heat_implicit.txt
# The grid details should be input to the DSL program
grid.type = RegularCartesian1D
grid.x0 = 0.0
grid.x1 = 1.0
grid.n = 6
# Physics that requires specification
physics.diffusion_constant = 0.3 # Heat diffusion constant. Or should this rather be an input parameter to the generated simulator?
boundary.condition_type = Dirichlet
boundary.val0 = 0.5
boundary.val1 = 1.5
initial.value = 0 # Or should this rather be an input parameter to the generated simulator?
initial.boundary = BoundaryConditions # Overriding the initial value specified above, for the boundaries
# What kind of simulator should be generated?
simulator.type = Implicit # Explicit or Implicit
simulator.basis = Constant # Temperatures averaged over cells
dt = MaxStableTimeStep # "Special" value for a variable ("Local variables" in the DSL program...)
dx = grid.getSpacing() # (x1-x0)/n for 1D grid
s = physics.diffusion_constant * dt / (dx*dx) # Think "Python name" more than "variable". The stencil-specification below makes use of this 's'...
simulator.stencil = (-s, 1+2s, -s) # -s(u_{i-1}^n - u_i^n) + u_i^n +s(u_i^n - u_{i+1}^n) = u_i^{n-1},
# Hmm. Does it make more sense not to specify this, but instead let this be generated from "flux considerations"?
# If the extremas consist of either telling the generator "solve for heat" or actually feeding it detailed C++-code, where in between should we define our sweet spot?!
simulator.generate("generated_1D_heat_implicit.cpp")
And what should the generator produce? Maybe something like the following. First a small routine for the EB3: (And no, I don't remember what EB3 stands for... EBBB = Equelle Basic Building Blocks?)
In [ ]:
%%file EB3_sample_linSystemSolver.cpp
#include <iostream>
#include <cmath>
#include <utility>
#include <cstring>
// Solving the n x n system a x = b, upper triangular matrix ends up in uppertri,
// b_scratch must have length n, solution ends up in x, returns true if non-singular,
// false otherwise. Matrices in row-major order.
bool GaussElimIfNonSingular(const int n,
const double * const a, double * const uppertri,
const double * const b, double * const b_scratch,
double * const x)
{
memcpy(uppertri, a, n*n*sizeof(double));
memcpy(b_scratch, b, n*sizeof(double));
memset(x, 0, n*sizeof(double));
for (int j=0; j<n; j++) {
int maxrow=j;
for (int i=j+1; i<n; i++)
if (fabs(uppertri[i*n+j]) > fabs(uppertri[maxrow*n+j]))
maxrow = i;
for (int k=j; k<n; k++)
std::swap(uppertri[j*n+k], uppertri[maxrow*n+k]);
std::swap(b_scratch[j], b_scratch[maxrow]);
if (fabs(uppertri[j*n+j]) < 1e-12) // Singular?
return false;
for (int i=j+1; i<n; i++) {
const double tmp = uppertri[i*n+j] / uppertri[j*n+j];
b_scratch[i] -= b_scratch[j] * tmp;
for (int k=n-1; k>=j; k--)
uppertri[i*n+k] -= uppertri[j*n+k] * tmp;
}
}
x[n-1] = b_scratch[n-1] / uppertri[(n-1)*n+(n-1)];
for (int i=n-2; i>=0; i--) {
double tmp = uppertri[i*n+i+1] * x[i+1];
for (int k=i+2; k<n; k++)
tmp += uppertri[i*n+k] * x[k];
x[i] = (b_scratch[i] - tmp) / uppertri[i*n+i];
}
return true;
}
In [ ]:
!g++ -Wall -O2 -c -o EB3_sample_linSystemSolver.o EB3_sample_linSystemSolver.cpp
And then the "generated" simulator:
In [ ]:
%%file generated_1D_heat_implicit.cpp
// E3B-headers to be included here
bool GaussElimIfNonSingular(const int n,
const double * const a, double * const uppertri,
const double * const b, double * const b_scratch,
double * const x);
double u[6]; // Length from "grid.n = 6", type and dimension from "grid.type = RegularCartesian1D"
double A[6][6]; // From "simulator.type = Implicit", "grid.n = 6" and "simulator.basis = Constant"
void solution_init(void)
{
for (int i=0; i<6; i++)
u[i] = 0.0; // From "initial.value = 0"
u[0] = 0.5; // From "boundary.val0 = 0.5"
u[5] = 1.5; // From "boundary.val1 = 1.5"
}
void system_init(const double s)
{
const int stencil_length = 3;
const double stencil[stencil_length] = {-s, 1.0+2.0*s, -s}; // From "simulator.stencil = (-s, 1+2s, -s)"
for (int i=0; i<6; i++) {
for (int j=0; j<6; j++)
A[i][j] = 0.0;
// A[i][i] = 1.0; // got this in the stencil already
if ( (i>0) && (i<5) )
for (int j=0; j<3; j++)
A[i][i-stencil_length/2+j] += stencil[j];
}
A[0][0] = 1.0; // From the "boundary.*" entries:
A[5][5] = 1.0;
}
void init(void)
{
// const double dx = 1.0/6.0; // From "dx = grid.getSpacing()"
const double dx = 0.5; // Overriding with the number from the iPython notebook, so that we can compare the results!
// const double dt = dx*dx/(2*0.3); // From "dt = MaxStableTimeStep"
// Hmm... This was the max dt for the *explicit* stencil...
const double dt = 5.0; // Trying a larger value (Note: Use the same as for the iPython notebook, so that we can compare results...)
system_init( 0.3 * dt / (dx*dx) ); // From "physics.diffusion_constant = 0.3" and "s = physics.diffusion_constant * dt / (dx*dx)"
solution_init();
}
void performOneTimeStep(void)
{
double v[6], Ascratch[36], uScratch[6];
const bool nonSingular = GaussElimIfNonSingular(6, (double *)A, Ascratch, u, uScratch, v);
for (int i=0; i<6; i++)
u[i] = v[i];
}
In [ ]:
!g++ -c -o generated_1D_heat_implicit.o generated_1D_heat_implicit.cpp
Let us test this in a small app
In [ ]:
%%file simtest.cpp
#include <iostream>
// Contained in the "generated" C++-code:
extern double u[6];
void init(void);
void performOneTimeStep(void);
int main(int argc, char *argv[])
{
init();
for (int i=0; i<20; i++) {
std::cout << i << ": \t";
for (int j=0; j<6; j++)
std::cout << u[j] << " ";
std::cout << std::endl;
performOneTimeStep();
}
return 0;
}
In [ ]:
!g++ -o simtest simtest.cpp generated_1D_heat_implicit.o EB3_sample_linSystemSolver.o
In [ ]:
!simtest
Hmm... A bit strange that this converges faster than the iPython version?! Better linear solver? iPython using float instead of double internally?
In [ ]: