Heat equation inversion

The heat equation is another classic example of an inverse problem. The following example shows how you can estimate time dependent temperature distribution on a 1d domain using several point measurements when heating is localized to a few fixed points. This is done with the help of sparse matrices and regularized least squares. Regularization of the second derivative used used to ensure smoothness of the reconstructed heating.

A generalization of this to 2d and 3d (and with advection added to the model) has applications for example in oceanography, where the inverse problem is to estimate the methane release from localized sources in the sea floor.

 
#!/usr/bin/env python
#
# Inverse problems with the 1d heat equation
# (c) 2017 Juha Vierinen
#
import numpy as n
import matplotlib.pyplot as plt
from scipy.sparse import coo_matrix
import scipy.sparse.linalg as s
import lsqlin

def create_theory_matrix(n_t=200,n_x=200,
dt=2.0,dx=1.0,C=0.5,
alpha=0,
alpha_q=0,
alpha_u=0,
q_true=None,q_x=100, # measured heating at location x
u_meas=None,u_x=100): # meuasred u at location x
#
# n_t = number of points in time
# n_x = number of points in space
# dx = spatial grid spaceing
# dt = time spacing
# alpha = L_2 regularization
# parameters for u and
# n_t parameters for heat source q at known location (center of domain)
#
print(q_x)
n_par=n_t*n_x + n_t*len(q_x)
qi0=n_t*n_x

# diffusion coefficient (assumed to be known)
C = n.zeros([n_t,n_x])
C[:,:]=0.5

# number of unknowns
n_col=n_par

row_idx=0

d=[]
rows=[]
cols=[]
data=[]
row_idx=0

q_x=n.array(q_x)

# heat equation part du/dx - C d^2u/dx^2 - q = 0
alpha_theory=1.0
for i in range(n_t-1):
for j in range(1,n_x-1):
# du/dt
rows.append(row_idx)
cols.append(i*n_x+j)
data.append(alpha_theory*-1.0/dt)

rows.append(row_idx)
cols.append((i+1)*n_x+j)
data.append(alpha_theory*1.0/dt)

# -C*d^2u/dx^2
rows.append(row_idx)
cols.append(i*n_x+j-1)
data.append(alpha_theory*-C[i,j]*1.0/(dx**2.0))

rows.append(row_idx)
cols.append(i*n_x+j)
data.append(alpha_theory*C[i,j]*2.0/(dx**2.0))

rows.append(row_idx)
cols.append(i*n_x+j+1)
data.append(alpha_theory*-C[i,j]*1.0/(dx**2.0))

# specific location at center where heating occurs
if j in q_x:
q_idx=n.where(j==q_x)[0][0]
rows.append(row_idx)
cols.append(qi0+q_idx*n_t+i)
data.append(alpha_theory*-1.0)

d.append(0.0)

row_idx+=1

# initial boundary condition of 0
ut=n.repeat(0,n_x)
ux=n.arange(n_x)
uv=n.zeros(n_x)
for mi in range(len(ut)):
i=ut[mi]
j=ux[mi]
rows.append(row_idx)
cols.append(i*n_x+j)
data.append(1.0)
d.append(uv[mi])
row_idx+=1

# regularization of heat field
if alpha > 0:
for i in range(n_t-2):
for j in range(n_x):
rows.append(row_idx)
cols.append(i*n_x+j)
data.append(1.0*alpha)

rows.append(row_idx)
cols.append((i+1)*n_x+j)
data.append(-2.0*alpha)

rows.append(row_idx)
cols.append((i+2)*n_x+j)
data.append(1.0*alpha)

d.append(0.0)
row_idx+=1

# regularization of heat sources
if alpha_q > 0:
for j in range(len(q_x)):
for i in range(n_t-2):
rows.append(row_idx)
cols.append(qi0+j*n_t+i)
data.append(1.0*alpha_q)

rows.append(row_idx)
cols.append(qi0+j*n_t+i+1)
data.append(-2.0*alpha_q)

rows.append(row_idx)
cols.append(qi0+j*n_t+i+2)
data.append(1.0*alpha)

d.append(0.0)
row_idx+=1

# heating boundar conditions q=0 at t=0 and q=0 at t=n_t
for j in range(len(q_x)):
rows.append(row_idx)
cols.append(qi0+j*n_t)
data.append(1.0)
d.append(0.0)
row_idx+=1
rows.append(row_idx)
cols.append(qi0+j*n_t+n_t-1)
data.append(1.0)
d.append(0.0)
row_idx+=1

# heating measurements (used for forward model)
if q_true != None:
for j in range(len(q_x)):
for i in range(n_t):
rows.append(row_idx)
cols.append(qi0+j*n_t+i)
data.append(1.0)
d.append(q_true[j][i])
row_idx+=1

if u_meas != None:
for j in range(len(u_x)):
for i in range(n_t):
rows.append(row_idx)
cols.append(i*n_x+u_x[j]) # at time i, location u_x
data.append(alpha_u)
d.append(u_meas[j][i]*alpha_u)
row_idx+=1

d=n.array(d)
n_row=row_idx
n_col=n_par

# create a sparse theory matrix
G=coo_matrix((data, (rows, cols)), shape=(n_row, n_col), dtype=n.float32)
return(G,d,qi0)

n_t=100 # number of time steps
n_x=200 # number of grid points
# true heating (gaussian shape)
q_true0=n.exp(-0.5*(n.linspace(-5,5,num=n_t)+1.0)**2.0/0.1)
q_true0=q_true0/n.max(q_true0)
q_true1=n.exp(-0.5*(n.linspace(-5,5,num=n_t)-1.0)**2.0/0.1)
q_true1=q_true1/n.max(q_true1)
q_true=[q_true0,q_true1]
q_x=[82,140]     # heat source locations
u_x=[50,90,150] # temperature measurement location

C=0.2   # diffusion coefficient

# solve heat equation, no regularization needed, because the problem is fully determined
# as we provide heating q.
G,d,qi0=create_theory_matrix(n_t=n_t,n_x=n_x,q_true=q_true,q_x=q_x,alpha=1e-6,alpha_q=0,C=C)
print(G.shape)
print(d.shape)
m0=s.lsqr(G,d)[0]
m0u=m0[0:qi0]
q_hat0=m0[qi0+n.arange(n_t)]
q_hat1=m0[qi0+n_t+n.arange(n_t)]
m0u.shape=(n_t,n_x)

u_meas_0=m0u[:,u_x[0]]+n.random.randn(n_t)*0.02
u_meas_1=m0u[:,u_x[1]]+n.random.randn(n_t)*0.02
u_meas_2=m0u[:,u_x[2]]+n.random.randn(n_t)*0.02
u_meas=[u_meas_0,u_meas_1,u_meas_2]

# now create a theory matrix with q unknown
# in order to estimate q from a measurement of u from only one location
#
# 1.0/alpha_u is "measurement error standard deviation"
# the larger alpha_u, the more weight we put on measurements and
# less weight on the solution satisfying the heat equation
q_x=[42,82,140]   # heat source locations
G,d,qi0=create_theory_matrix(n_t=n_t,
n_x=n_x,
u_meas=u_meas,
q_x=q_x,
u_x=u_x,
alpha=0.01,alpha_q=0.01,
alpha_u=0.1,C=C)
print(G.shape)
print(d.shape)

# sparse iterative least squares
# it would be advantageous to add a positivity constraint to u and q, but maybe later
m0=s.lsqr(G,d)[0]

m0u_hat=m0[0:qi0]
m0u_hat.shape=(n_t,n_x)
q_hat0=m0[qi0+n.arange(n_t)]
q_hat1=m0[qi0+n_t+n.arange(n_t)]
q_hat2=m0[qi0+2*n_t+n.arange(n_t)]

#
# Plot stuff
#
plt.subplot(231)
plt.plot(q_true[0])
plt.plot(q_true[1])
plt.title("True heating")
plt.xlabel("Time")
plt.ylim([-0.5,1.5])
#plt.title("$q_{\mathrm{true}}$ at x",q_x)

plt.subplot(232)
plt.pcolormesh(n.transpose(m0u))
plt.ylabel("x")
plt.xlabel("Time")
plt.axhline(q_x[1],color="red")
plt.axhline(q_x[2],color="red")
plt.axhline(u_x[0],color="white")
plt.axhline(u_x[1],color="white")
plt.axhline(u_x[2],color="white")
plt.title("Temperature (true)")
plt.colorbar()

plt.subplot(233)
plt.plot(u_meas_0,label="Measurement")
plt.plot(u_meas_1,label="Measurement")
plt.plot(u_meas_2,label="Measurement")
plt.title("Measurements")
plt.legend(loc=2)
plt.xlabel("Time")
plt.ylabel("Temperature")
#plt.title("Measured temperature at x ",u_x)

plt.subplot(234)
plt.plot(q_hat0,label="Estimate",color="black")
plt.plot(q_hat1,label="Estimate",color="black")
plt.plot(q_hat2,label="Estimate",color="red")
plt.plot(q_true[0],label="True")
plt.plot(q_true[1],label="True")
plt.legend()
plt.ylim([-0.5,1.5])
plt.title("Estimated heating")
#plt.title("$\hat{q}$ at x",q_x)

plt.subplot(235)
plt.pcolormesh(n.transpose(m0u_hat),vmin=n.min(m0u),vmax=n.max(m0u))
plt.axhline(u_x[0],color="white")
plt.axhline(u_x[1],color="white")
plt.axhline(u_x[2],color="white")
plt.axhline(q_x[0],color="red")
plt.axhline(q_x[1],color="red")
plt.axhline(q_x[2],color="red")
plt.ylabel("x")
plt.xlabel("Time")
plt.title("Estimated temperature")
plt.colorbar()

plt.subplot(236)
plt.plot(m0u_hat[:,u_x[0]],label="Estimated")
plt.plot(m0u_hat[:,u_x[1]],label="Estimated")
plt.plot(m0u_hat[:,u_x[2]],label="Estimated")
plt.plot(m0u[:,u_x[0]],label="True")
plt.plot(m0u[:,u_x[1]],label="True")
plt.plot(m0u[:,u_x[2]],label="True")
plt.legend(loc=2)
#plt.title("Estimated temperature at x ",u_x)
plt.xlabel("Time")
plt.show()