How To Solve A 1-D Steady-State Diffusion Heat System With Two Layers And Continuity Conditions Using scipy solve_bvp?
Question:
I have a steady-state 1-D diffusion equation (edited this to include a missing negative sign, after a solution was given by Patol75 below):
k T” = k (d^2/dz^2) T = -H
Where k (> 0 W/m K) is the thermal conductivity, and, H (>= 0 W/m^3) is a volumetric heating rate.
There are two layers:
0 <= z <= z_1:
k = k_1, H = H_1
z_1 < z <= z_2:
k = k_2, H = H_2
with k_2 >= k_1.
We have boundary conditions of T(z = 0 m) = T_u, T(z = z_2) = T_b, and, continuity conditions of the continuity of temperature and heat flux (k dT/dz) at z = z_1.
An analytical solution exists for this system.
However, solving this system numerically, using solve_bvp(), does not give the same result as the analytical solution.
Here is my code for the numerical solution:
import numpy as np
import scipy
from scipy.integrate import solve_bvp
def H_func(x, z_1, z_2, H_1, H_2, R):
#x is radius
R_1 = R - z_1
R_2 = R - z_2
if type(x) == np.ndarray:
H_arr = np.array([])
for i in np.arange(len(x)):
x_val = x[i]
if (x_val > R_1 and x_val <= R):
H_val = H_1
elif (x_val >= R_2 and x_val <= R_1):
H_val = H_2
else:
1/0
H_arr = np.append(H_arr, H_val)
return H_arr
elif type(x) == np.float64:
if (x > R_1 and x <= R):
H_val = H_1
elif (x >= R_2 and x <= R_1):
H_val = H_2
else:
1/0
return H_val
else:
1/0 #failure
return
def k_func(x, z_1, z_2, k_1, k_2, R):
R_1 = R - z_1
R_2 = R - z_2
if type(x) == np.ndarray:
k_arr = np.array([])
for j in np.arange(len(x)):
x_val = x[j]
if (x_val > R_1 and x_val <= R):
k_val = k_1
elif (x_val >= R_2 and x_val <= R_1):
k_val = k_2
else:
1/0
k_arr = np.append(k_arr, k_val)
return k_arr
elif type(x) == np.float64:
if (x > R_1 and x <= R):
k_val = k_1
elif (x >= R_2 and x <= R_1):
k_val = k_2
else:
1/0
return k_val
else:
1/0
return
def fun(x, y, z_1, z_2, k_1, k_2, H_1, H_2, R):
second_deriv_arr = np.array([])
for i in np.arange(len(x)):
x_Val = x[i]
z_val = R_p - x_Val
T_val = y[0][i]
dTdr_val = y[1][i]
dTdz_val = -1.*dTdr_val
#did not need to separate the two layers this way with IF/ELIF/ELSE statements, though, it is useful for alternative scenarios where the ODEs for the two layers are not the same
if (0. <= z_val and z_val <= z_1):
second_deriv_val = -1.*H_func(x_Val, z_1, z_2, H_1, H_2, R)/k_func(x_Val, z_1, z_2, k_1, k_2, R)
elif (z_1 < z_val and z_val <= z_2):
second_deriv_val = -1.*H_func(x_Val, z_1, z_2, H_1, H_2, R)/k_func(x_Val, z_1, z_2, k_1, k_2, R)
else:
print("Probing incorrect parameter space.")
1/0
second_deriv_arr = np.append(second_deriv_arr, second_deriv_val)
return np.vstack((y[1], second_deriv_arr))
def bc(ya, yb):
return np.array([ya[0] - Tu_global, yb[0] - Tb_global])
def get_XY_arrays(z_1, z_2, T_u, T_b, k_1, k_2, H_1, H_2, R, spacing1 = 25, spacing2 = 50, tolerance = 1.e-5):
R_1 = R - z_1
R_2 = R - z_2
#an array of radii
x_in = np.linspace(z_2, R, spacing1)
#an array of temperatures and temperature gradients
y_in = np.zeros((2, x_in.size))
y_temp = np.linspace(T_b, T_u, spacing1)
y_in[0] = y_temp
y_in[1] = -(T_b - T_u)/z_2
#not a great solution, just quick and dirty way to get the boundary conditions as I wish
global T_b
Tb_global = T_b
global Tu_global
Tu_global = T_u
res_func = solve_bvp(lambda x,y: fun(x, y, z_1, z_2, k_1, k_2, H_1, H_2, R), bc, x_in, y_in, tol = tolerance)
x_arr = np.linspace(R_2, R, spacing2)
y_arr = res_func.sol(x_arr)[0]
#dy_dx_arr = res_func.sol(x_arr)[1]
return x_arr, y_arr, res_func
Note that I have used radius, r, above in the code, though, I switch it back to depth, z, using a plane-parallel approximation for 1-D geometry.
There was probably a way to code the above to avoid radius altogether, but that’s not all that important at the moment.
For simplicity, we can assume that: H_1 = H_2 = 0 W/m^3.
The other parameters can be set as: T_u = 100 K, T_b = 1600 K, z_1 = 10000 m (10 km), z_2 = 20000 m (20 km), R = 100000 m (100 km), k_1 = 3 W/m K, k_2 = 4 W/m K.
With those parameters, and the above code, I do find a numerical solution to this system (as indicated by res_func.success == True), albeit, an incorrect one.
Here is a comparison plot of the resulting numerical and analytical results for these parameters:
Numerical vs. Analytical results
Setting H_1 and H_2 to small values, such as H_1 = H_2 = 1.e-8 W/m^3, does not help.
Setting k_1 = k_2 (with the H_1 = H_2, or, H_1 =/= H_2) does however give the same results for the analytical and numerical solutions.
I have tested other scenarios with this coding framework, i.e., having two layers, such as instead having different ODEs for the two layers, and it works in those cases, provided that we set k_1 = k_2.
In other words, the issue is not my implementation of the two layers themselves, via the IF/ELIF/ELSE statements in k_func(), H_func(), and, fun(), nor the boundary conditions.
I believe the source of the issue is the continuity of heat flux condition:
k_1 dT_1/dz (z = z_1) = k_2 dT_2/dz (z = z_1),
where T_1 and T_2 are the temperature profiles through layer 1 and layer 2.
I have not explicitly handled that in the above code, and I suspect that solve_bvp() is implicitly trying to allow for continuity of T and dT/dz, not T and k dT/dz.
Can anyone help me solve this dilemma?
Is it possible to enforce the continuity conditions with solve_bvp()?
And if not, how else could I handle the problem numerically?
By using sympy or some other approach?
(Note that the reason I want to solve the system numerically is that I have a more complicated scenario which I haven’t been able to solve analytically, but follows a similar set-up as the simpler example above.)
Edit (to clear up motivation for this question):
The goal was to find a temperature profile, T(z), through the two layers, having the two boundary conditions and continuity conditions specified above.
In this case, the system is in steady state (dT/dt = 0 K / s), and, can have internal heating within the two layers (when H_1 > 0 W/m^3 and/or H_2 > 0 W/m^3).
The more complicated system, alluded to above, will include an advective term:
k T” + H = rho cp v T’
(where rho = density, cp = specific heat, v = velocity).
Among other new terms in the ODE for layer #1.
Edit #2 (some additional information):
The analytical solution is given by:
def find_T_analytical(z, z_1, z_2, k_1, k_2, H_1, H_2, c1, c2, c3, c4):
if (0. <= z and z < z_1):
T_analytical = -(H_1*z**2)/(2.*k_1) + c1*z/k_1 + c2/k_1
elif (z_1 <= z and z <= z_2):
T_analytical = -(H_2*z**2)/(2.*k_2) + c3*z/k_2 + c4/k_2
else:
#incorrect parameter space
1/0
return T_analytical
c1 = -((2.*z_1*z_2*k_1*(H_2 - H_1) + (z_1**2)*(2.*k_1*H_1 - k_2*H_1 - k_1*H_2) - k_1*((z_2**2)*H_2 + 2.*k_2*(T_b - T_u)))/(2.*(z_2*k_1 + z_1*(k_2 - k_1))))
c2 = k_1*T_u
c3 = -((z_1**2)*(k_2*(H_1 - 2.*H_2) + k_1*H_2) - k_1*((z_2**2)*H_2 + 2.*k_2*(T_b - T_u)))/(2.*(z_2*k_1 + z_1*(k_2 - k_1)))
c4 = -((-(z_1**2)*z_2*(k_2*(H_1 - 2.*H_2) + k_1*H_2) + z_1*(k_1 - k_2)*((z_2**2)*H_2 + 2.*k_2*T_b) - 2.*z_2*k_1*k_2*T_u)/(2.*(z_2*k_1 + z_1*(k_2 - k_1))))
Which, with the following depth array (and the above parameters):
z_bottom = z_2/1000.
z_arr = np.arange(start = 0., stop = (z_bottom + 0.5), step = 0.5)
z_arr = z_arr*1000.
Gives the following temperature array:
T_arr = np.array([ 100., 142.85714286, 185.71428571, 228.57142857, 271.42857143, 314.28571429, 357.14285714, 400., 442.85714286, 485.71428571, 528.57142857, 571.42857143, 614.28571429, 657.14285714, 700., 742.85714286, 785.71428571, 828.57142857, 871.42857143, 914.28571429, 957.14285714, 989.28571429, 1021.42857143, 1053.57142857, 1085.71428571, 1117.85714286, 1150., 1182.14285714, 1214.28571429, 1246.42857143, 1278.57142857, 1310.71428571, 1342.85714286, 1375., 1407.14285714, 1439.28571429, 1471.42857143, 1503.57142857, 1535.71428571, 1567.85714286, 1600.])
Edit #3:
I’ve tried to the solution below, it works well for the H_1 = H_2 = 0 W/m^3 case (i.e., no internal heating):
For some reason, it doesn’t match the analytical solution when H_1 > 0 and/or H_2 > 0.
Here’s a plot with H_1 = H_2 = 1.e-5 W/m^3, and all other parameters the same as above:
Edit #4 (September 13th):
After @Patol75 ‘s latest update, I worked out the non-dimensionalization a bit differently, though, the solutions seem to be equivalent, given they both give the same result.
Here’s the updated functions from their latest update:
def rhs(x, y):
return np.vstack(
(
y[1], # RHS for temperature in upper layer
np.repeat(-(H_1/k_1)*(z_1**2), y[1].size
), # RHS for temperature derivative in upper layer
y[3], # RHS for temperature in lower layer
np.repeat(
-(H_2/k_2)*((z_2 - z_1)**2), y[3].size
), # RHS for temperature derivative in lower layer
)
)
def bc(ya, yb):
return np.array(
[
yb[0] - T_u, # BC for temperature at upper layer top
ya[2] - T_b, # BC for temperature at lower layer bottom
ya[0] - yb[2], # BC for continuity of temperature
k_1*ya[1]/(z_1) - k_2*yb[3]/((z_2 - z_1)), # BC for continuity of heat flux
]
)
z_1 = 10e3 # Upper layer thickness
z_2 = 30e3 # Total thickness
k_1 = 3 # Upper layer thermal conductivity
k_2 = 4 # Lower layer thermal conductivity
H_1 = 1e-6 # Upper layer volumetric heating rate
H_2 = 1e-5 # Lower layer volumetric heating rate
T_u = 100 # Temperature at upper layer top
T_b = 1600 # Temperature at lower layer bottom
With the remaining the code the same as Patol’s.
The line of thinking here is that only the z-variable is non-dimensionalized:
z’ = z/z_star
where z_star is z_1 for layer 1, and (z_2 – z_1) for layer 2.
This gives:
(d^2/d(z’ z_star)^2) T = (1/z_star)^2 (d^2/d(z’)^2) T = – H/k
Thus,
(d^2/d(z’)^2) T = -(H/k) (z_star)^2
For the heat-flux continuity condition, I’m a bit less sure about this, though, unit-wise, it seems we need to divide the temperature gradient by z_star:
k (1/z_star) dT/d(z’)
which gives units of K/m^2, as desired.
Answers:
I have not tried to understand what your code does, but I was interested in the mathematical problem and solve_bvp
. The following approach seems to replicate the analytical solution displayed in the image you uploaded:
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_bvp
def rhs(x, y):
return np.vstack(
(
y[1], # RHS for temperature in upper layer
np.repeat(
H_1 / k_1, y[1].size
), # RHS for temperature derivative in upper layer
y[3], # RHS for temperature in lower layer
np.repeat(
H_2 / k_2, y[3].size
), # RHS for temperature derivative in lower layer
)
)
def bc(ya, yb):
return np.array(
[
yb[0] - T_u, # BC for temperature at upper layer top
ya[2] - T_b, # BC for temperature at lower layer bottom
ya[0] - yb[2], # BC for continuity of temperature
k_1 * ya[1] - k_2 * yb[3], # BC for continuity of heat flux
]
)
k_1 = 3 # Upper layer thermal conductivity
k_2 = 4 # Lower layer thermal conductivity
H_1 = 0 # Upper layer volumetric heating rate
H_2 = 0 # Lower layer volumetric heating rate
T_u = 100 # Temperature at upper layer top
T_b = 1600 # Temperature at lower layer bottom
mesh_node_number = 21
sol = solve_bvp(
rhs,
bc,
np.linspace(0, 1, mesh_node_number),
np.vstack(
(
np.linspace(T_b, 900, mesh_node_number),
(T_b - T_u) / 2 * np.ones(mesh_node_number),
np.linspace(900, T_u, mesh_node_number),
(T_b - T_u) / 2 * np.ones(mesh_node_number),
)
),
)
plt.plot(
np.hstack((sol.y[2], sol.y[0][1:])), np.linspace(20, 0, mesh_node_number * 2 - 1)
)
plt.axhline(10, linestyle="dotted", linewidth=0.5, color="grey")
plt.axvline(sol.y[0][0], linestyle="dotted", linewidth=0.5, color="grey")
plt.xlabel("Temperature (K)")
plt.ylabel("Depth (km)")
plt.gca().invert_yaxis()
plt.show()
The idea is to solve the ODE twice, once per layer, in one solve_bvp
call using the boundary conditions to link the two solutions. The main drawback is that both layers share the same mesh. Moreover, you need to be careful while handling the derivative solution values if both layers do not have the same size.
Following the OP updates, I have modified the above code to handle cases with non-zero source terms and variable size layers:
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_bvp
def analytical_solution(z, k_1, k_2, H_1, H_2):
k_1 /= z_1
k_2 /= z_2 - z_1
H_1 /= z_1**3
H_2 /= (z_2 - z_1) ** 3
c1 = -(
(
2 * z_1 * z_2 * k_1 * (H_2 - H_1)
+ z_1**2 * (2 * k_1 * H_1 - k_2 * H_1 - k_1 * H_2)
- k_1 * (z_2**2 * H_2 + 2 * k_2 * (T_b - T_u))
)
/ (2 * (z_2 * k_1 + z_1 * (k_2 - k_1)))
)
c2 = k_1 * T_u
c3 = -(
z_1**2 * (k_2 * (H_1 - 2 * H_2) + k_1 * H_2)
- k_1 * (z_2**2 * H_2 + 2 * k_2 * (T_b - T_u))
) / (2 * (z_2 * k_1 + z_1 * (k_2 - k_1)))
c4 = -(
(
-(z_1**2) * z_2 * (k_2 * (H_1 - 2 * H_2) + k_1 * H_2)
+ z_1 * (k_1 - k_2) * (z_2**2 * H_2 + 2 * k_2 * T_b)
- 2 * z_2 * k_1 * k_2 * T_u
)
/ (2 * (z_2 * k_1 + z_1 * (k_2 - k_1)))
)
mask_upper = z < z_1
temperature = np.empty_like(z)
temperature[mask_upper] = (
-(H_1 * z[mask_upper] ** 2) / (2 * k_1) + c1 * z[mask_upper] / k_1 + c2 / k_1
)
temperature[~mask_upper] = (
-(H_2 * z[~mask_upper] ** 2) / (2 * k_2) + c3 * z[~mask_upper] / k_2 + c4 / k_2
)
return temperature
def rhs(x, y):
return np.vstack(
(
y[1], # RHS for temperature in upper layer
np.repeat(
-H_1 / k_1, y[1].size
), # RHS for temperature derivative in upper layer
y[3], # RHS for temperature in lower layer
np.repeat(
-H_2 / k_2, y[3].size
), # RHS for temperature derivative in lower layer
)
)
def bc(ya, yb):
return np.array(
[
yb[0] - T_u, # BC for temperature at upper layer top
ya[2] - T_b, # BC for temperature at lower layer bottom
ya[0] - yb[2], # BC for continuity of temperature
k_1 * ya[1] * ((z_2 - z_1) / z_1) ** 2
- k_2 * yb[3], # BC for continuity of heat flux
]
)
z_1 = 1e4 # Upper layer thickness
z_2 = 3e4 # Total thickness
k_1 = 3 * z_1 # Upper layer thermal conductivity
k_2 = 4 * (z_2 - z_1) # Lower layer thermal conductivity
H_1 = 1e-6 * z_1**3 # Upper layer volumetric heating rate
H_2 = 1e-5 * (z_2 - z_1) ** 3 # Lower layer volumetric heating rate
T_u = 100 # Temperature at upper layer top
T_b = 1600 # Temperature at lower layer bottom
mesh_node_number = int(max(z_1, z_2 - z_1) / 1e3) + 1 # At most 1 km spacing
depths = np.hstack(
(np.linspace(z_2, z_1, mesh_node_number), np.linspace(z_1, 0, mesh_node_number))
)
sol = solve_bvp(
rhs,
bc,
np.linspace(0, 1, mesh_node_number),
np.vstack(
(
np.linspace(T_b, (T_b + T_u) / 2, mesh_node_number),
np.repeat((T_b - T_u) / 2, mesh_node_number),
np.linspace((T_b + T_u) / 2, T_u, mesh_node_number),
np.repeat((T_b - T_u) / 2, mesh_node_number),
)
),
)
plt.plot(np.hstack((sol.y[2], sol.y[0])), depths / 1e3)
plt.plot(
analytical_solution(depths, k_1, k_2, H_1, H_2),
depths / 1e3,
linestyle="none",
marker="o",
)
plt.axhline(z_1 / 1e3, linestyle="dotted", linewidth=0.5, color="grey")
plt.axvline(sol.y[0][0], linestyle="dotted", linewidth=0.5, color="grey")
plt.xlabel("Temperature (K)")
plt.ylabel("Depth (km)")
plt.gca().invert_yaxis()
plt.show()
I have a steady-state 1-D diffusion equation (edited this to include a missing negative sign, after a solution was given by Patol75 below):
k T” = k (d^2/dz^2) T = -H
Where k (> 0 W/m K) is the thermal conductivity, and, H (>= 0 W/m^3) is a volumetric heating rate.
There are two layers:
0 <= z <= z_1:
k = k_1, H = H_1
z_1 < z <= z_2:
k = k_2, H = H_2
with k_2 >= k_1.
We have boundary conditions of T(z = 0 m) = T_u, T(z = z_2) = T_b, and, continuity conditions of the continuity of temperature and heat flux (k dT/dz) at z = z_1.
An analytical solution exists for this system.
However, solving this system numerically, using solve_bvp(), does not give the same result as the analytical solution.
Here is my code for the numerical solution:
import numpy as np
import scipy
from scipy.integrate import solve_bvp
def H_func(x, z_1, z_2, H_1, H_2, R):
#x is radius
R_1 = R - z_1
R_2 = R - z_2
if type(x) == np.ndarray:
H_arr = np.array([])
for i in np.arange(len(x)):
x_val = x[i]
if (x_val > R_1 and x_val <= R):
H_val = H_1
elif (x_val >= R_2 and x_val <= R_1):
H_val = H_2
else:
1/0
H_arr = np.append(H_arr, H_val)
return H_arr
elif type(x) == np.float64:
if (x > R_1 and x <= R):
H_val = H_1
elif (x >= R_2 and x <= R_1):
H_val = H_2
else:
1/0
return H_val
else:
1/0 #failure
return
def k_func(x, z_1, z_2, k_1, k_2, R):
R_1 = R - z_1
R_2 = R - z_2
if type(x) == np.ndarray:
k_arr = np.array([])
for j in np.arange(len(x)):
x_val = x[j]
if (x_val > R_1 and x_val <= R):
k_val = k_1
elif (x_val >= R_2 and x_val <= R_1):
k_val = k_2
else:
1/0
k_arr = np.append(k_arr, k_val)
return k_arr
elif type(x) == np.float64:
if (x > R_1 and x <= R):
k_val = k_1
elif (x >= R_2 and x <= R_1):
k_val = k_2
else:
1/0
return k_val
else:
1/0
return
def fun(x, y, z_1, z_2, k_1, k_2, H_1, H_2, R):
second_deriv_arr = np.array([])
for i in np.arange(len(x)):
x_Val = x[i]
z_val = R_p - x_Val
T_val = y[0][i]
dTdr_val = y[1][i]
dTdz_val = -1.*dTdr_val
#did not need to separate the two layers this way with IF/ELIF/ELSE statements, though, it is useful for alternative scenarios where the ODEs for the two layers are not the same
if (0. <= z_val and z_val <= z_1):
second_deriv_val = -1.*H_func(x_Val, z_1, z_2, H_1, H_2, R)/k_func(x_Val, z_1, z_2, k_1, k_2, R)
elif (z_1 < z_val and z_val <= z_2):
second_deriv_val = -1.*H_func(x_Val, z_1, z_2, H_1, H_2, R)/k_func(x_Val, z_1, z_2, k_1, k_2, R)
else:
print("Probing incorrect parameter space.")
1/0
second_deriv_arr = np.append(second_deriv_arr, second_deriv_val)
return np.vstack((y[1], second_deriv_arr))
def bc(ya, yb):
return np.array([ya[0] - Tu_global, yb[0] - Tb_global])
def get_XY_arrays(z_1, z_2, T_u, T_b, k_1, k_2, H_1, H_2, R, spacing1 = 25, spacing2 = 50, tolerance = 1.e-5):
R_1 = R - z_1
R_2 = R - z_2
#an array of radii
x_in = np.linspace(z_2, R, spacing1)
#an array of temperatures and temperature gradients
y_in = np.zeros((2, x_in.size))
y_temp = np.linspace(T_b, T_u, spacing1)
y_in[0] = y_temp
y_in[1] = -(T_b - T_u)/z_2
#not a great solution, just quick and dirty way to get the boundary conditions as I wish
global T_b
Tb_global = T_b
global Tu_global
Tu_global = T_u
res_func = solve_bvp(lambda x,y: fun(x, y, z_1, z_2, k_1, k_2, H_1, H_2, R), bc, x_in, y_in, tol = tolerance)
x_arr = np.linspace(R_2, R, spacing2)
y_arr = res_func.sol(x_arr)[0]
#dy_dx_arr = res_func.sol(x_arr)[1]
return x_arr, y_arr, res_func
Note that I have used radius, r, above in the code, though, I switch it back to depth, z, using a plane-parallel approximation for 1-D geometry.
There was probably a way to code the above to avoid radius altogether, but that’s not all that important at the moment.
For simplicity, we can assume that: H_1 = H_2 = 0 W/m^3.
The other parameters can be set as: T_u = 100 K, T_b = 1600 K, z_1 = 10000 m (10 km), z_2 = 20000 m (20 km), R = 100000 m (100 km), k_1 = 3 W/m K, k_2 = 4 W/m K.
With those parameters, and the above code, I do find a numerical solution to this system (as indicated by res_func.success == True), albeit, an incorrect one.
Here is a comparison plot of the resulting numerical and analytical results for these parameters:
Numerical vs. Analytical results
Setting H_1 and H_2 to small values, such as H_1 = H_2 = 1.e-8 W/m^3, does not help.
Setting k_1 = k_2 (with the H_1 = H_2, or, H_1 =/= H_2) does however give the same results for the analytical and numerical solutions.
I have tested other scenarios with this coding framework, i.e., having two layers, such as instead having different ODEs for the two layers, and it works in those cases, provided that we set k_1 = k_2.
In other words, the issue is not my implementation of the two layers themselves, via the IF/ELIF/ELSE statements in k_func(), H_func(), and, fun(), nor the boundary conditions.
I believe the source of the issue is the continuity of heat flux condition:
k_1 dT_1/dz (z = z_1) = k_2 dT_2/dz (z = z_1),
where T_1 and T_2 are the temperature profiles through layer 1 and layer 2.
I have not explicitly handled that in the above code, and I suspect that solve_bvp() is implicitly trying to allow for continuity of T and dT/dz, not T and k dT/dz.
Can anyone help me solve this dilemma?
Is it possible to enforce the continuity conditions with solve_bvp()?
And if not, how else could I handle the problem numerically?
By using sympy or some other approach?
(Note that the reason I want to solve the system numerically is that I have a more complicated scenario which I haven’t been able to solve analytically, but follows a similar set-up as the simpler example above.)
Edit (to clear up motivation for this question):
The goal was to find a temperature profile, T(z), through the two layers, having the two boundary conditions and continuity conditions specified above.
In this case, the system is in steady state (dT/dt = 0 K / s), and, can have internal heating within the two layers (when H_1 > 0 W/m^3 and/or H_2 > 0 W/m^3).
The more complicated system, alluded to above, will include an advective term:
k T” + H = rho cp v T’
(where rho = density, cp = specific heat, v = velocity).
Among other new terms in the ODE for layer #1.
Edit #2 (some additional information):
The analytical solution is given by:
def find_T_analytical(z, z_1, z_2, k_1, k_2, H_1, H_2, c1, c2, c3, c4):
if (0. <= z and z < z_1):
T_analytical = -(H_1*z**2)/(2.*k_1) + c1*z/k_1 + c2/k_1
elif (z_1 <= z and z <= z_2):
T_analytical = -(H_2*z**2)/(2.*k_2) + c3*z/k_2 + c4/k_2
else:
#incorrect parameter space
1/0
return T_analytical
c1 = -((2.*z_1*z_2*k_1*(H_2 - H_1) + (z_1**2)*(2.*k_1*H_1 - k_2*H_1 - k_1*H_2) - k_1*((z_2**2)*H_2 + 2.*k_2*(T_b - T_u)))/(2.*(z_2*k_1 + z_1*(k_2 - k_1))))
c2 = k_1*T_u
c3 = -((z_1**2)*(k_2*(H_1 - 2.*H_2) + k_1*H_2) - k_1*((z_2**2)*H_2 + 2.*k_2*(T_b - T_u)))/(2.*(z_2*k_1 + z_1*(k_2 - k_1)))
c4 = -((-(z_1**2)*z_2*(k_2*(H_1 - 2.*H_2) + k_1*H_2) + z_1*(k_1 - k_2)*((z_2**2)*H_2 + 2.*k_2*T_b) - 2.*z_2*k_1*k_2*T_u)/(2.*(z_2*k_1 + z_1*(k_2 - k_1))))
Which, with the following depth array (and the above parameters):
z_bottom = z_2/1000.
z_arr = np.arange(start = 0., stop = (z_bottom + 0.5), step = 0.5)
z_arr = z_arr*1000.
Gives the following temperature array:
T_arr = np.array([ 100., 142.85714286, 185.71428571, 228.57142857, 271.42857143, 314.28571429, 357.14285714, 400., 442.85714286, 485.71428571, 528.57142857, 571.42857143, 614.28571429, 657.14285714, 700., 742.85714286, 785.71428571, 828.57142857, 871.42857143, 914.28571429, 957.14285714, 989.28571429, 1021.42857143, 1053.57142857, 1085.71428571, 1117.85714286, 1150., 1182.14285714, 1214.28571429, 1246.42857143, 1278.57142857, 1310.71428571, 1342.85714286, 1375., 1407.14285714, 1439.28571429, 1471.42857143, 1503.57142857, 1535.71428571, 1567.85714286, 1600.])
Edit #3:
I’ve tried to the solution below, it works well for the H_1 = H_2 = 0 W/m^3 case (i.e., no internal heating):
For some reason, it doesn’t match the analytical solution when H_1 > 0 and/or H_2 > 0.
Here’s a plot with H_1 = H_2 = 1.e-5 W/m^3, and all other parameters the same as above:
Edit #4 (September 13th):
After @Patol75 ‘s latest update, I worked out the non-dimensionalization a bit differently, though, the solutions seem to be equivalent, given they both give the same result.
Here’s the updated functions from their latest update:
def rhs(x, y):
return np.vstack(
(
y[1], # RHS for temperature in upper layer
np.repeat(-(H_1/k_1)*(z_1**2), y[1].size
), # RHS for temperature derivative in upper layer
y[3], # RHS for temperature in lower layer
np.repeat(
-(H_2/k_2)*((z_2 - z_1)**2), y[3].size
), # RHS for temperature derivative in lower layer
)
)
def bc(ya, yb):
return np.array(
[
yb[0] - T_u, # BC for temperature at upper layer top
ya[2] - T_b, # BC for temperature at lower layer bottom
ya[0] - yb[2], # BC for continuity of temperature
k_1*ya[1]/(z_1) - k_2*yb[3]/((z_2 - z_1)), # BC for continuity of heat flux
]
)
z_1 = 10e3 # Upper layer thickness
z_2 = 30e3 # Total thickness
k_1 = 3 # Upper layer thermal conductivity
k_2 = 4 # Lower layer thermal conductivity
H_1 = 1e-6 # Upper layer volumetric heating rate
H_2 = 1e-5 # Lower layer volumetric heating rate
T_u = 100 # Temperature at upper layer top
T_b = 1600 # Temperature at lower layer bottom
With the remaining the code the same as Patol’s.
The line of thinking here is that only the z-variable is non-dimensionalized:
z’ = z/z_star
where z_star is z_1 for layer 1, and (z_2 – z_1) for layer 2.
This gives:
(d^2/d(z’ z_star)^2) T = (1/z_star)^2 (d^2/d(z’)^2) T = – H/k
Thus,
(d^2/d(z’)^2) T = -(H/k) (z_star)^2
For the heat-flux continuity condition, I’m a bit less sure about this, though, unit-wise, it seems we need to divide the temperature gradient by z_star:
k (1/z_star) dT/d(z’)
which gives units of K/m^2, as desired.
I have not tried to understand what your code does, but I was interested in the mathematical problem and solve_bvp
. The following approach seems to replicate the analytical solution displayed in the image you uploaded:
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_bvp
def rhs(x, y):
return np.vstack(
(
y[1], # RHS for temperature in upper layer
np.repeat(
H_1 / k_1, y[1].size
), # RHS for temperature derivative in upper layer
y[3], # RHS for temperature in lower layer
np.repeat(
H_2 / k_2, y[3].size
), # RHS for temperature derivative in lower layer
)
)
def bc(ya, yb):
return np.array(
[
yb[0] - T_u, # BC for temperature at upper layer top
ya[2] - T_b, # BC for temperature at lower layer bottom
ya[0] - yb[2], # BC for continuity of temperature
k_1 * ya[1] - k_2 * yb[3], # BC for continuity of heat flux
]
)
k_1 = 3 # Upper layer thermal conductivity
k_2 = 4 # Lower layer thermal conductivity
H_1 = 0 # Upper layer volumetric heating rate
H_2 = 0 # Lower layer volumetric heating rate
T_u = 100 # Temperature at upper layer top
T_b = 1600 # Temperature at lower layer bottom
mesh_node_number = 21
sol = solve_bvp(
rhs,
bc,
np.linspace(0, 1, mesh_node_number),
np.vstack(
(
np.linspace(T_b, 900, mesh_node_number),
(T_b - T_u) / 2 * np.ones(mesh_node_number),
np.linspace(900, T_u, mesh_node_number),
(T_b - T_u) / 2 * np.ones(mesh_node_number),
)
),
)
plt.plot(
np.hstack((sol.y[2], sol.y[0][1:])), np.linspace(20, 0, mesh_node_number * 2 - 1)
)
plt.axhline(10, linestyle="dotted", linewidth=0.5, color="grey")
plt.axvline(sol.y[0][0], linestyle="dotted", linewidth=0.5, color="grey")
plt.xlabel("Temperature (K)")
plt.ylabel("Depth (km)")
plt.gca().invert_yaxis()
plt.show()
The idea is to solve the ODE twice, once per layer, in one solve_bvp
call using the boundary conditions to link the two solutions. The main drawback is that both layers share the same mesh. Moreover, you need to be careful while handling the derivative solution values if both layers do not have the same size.
Following the OP updates, I have modified the above code to handle cases with non-zero source terms and variable size layers:
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_bvp
def analytical_solution(z, k_1, k_2, H_1, H_2):
k_1 /= z_1
k_2 /= z_2 - z_1
H_1 /= z_1**3
H_2 /= (z_2 - z_1) ** 3
c1 = -(
(
2 * z_1 * z_2 * k_1 * (H_2 - H_1)
+ z_1**2 * (2 * k_1 * H_1 - k_2 * H_1 - k_1 * H_2)
- k_1 * (z_2**2 * H_2 + 2 * k_2 * (T_b - T_u))
)
/ (2 * (z_2 * k_1 + z_1 * (k_2 - k_1)))
)
c2 = k_1 * T_u
c3 = -(
z_1**2 * (k_2 * (H_1 - 2 * H_2) + k_1 * H_2)
- k_1 * (z_2**2 * H_2 + 2 * k_2 * (T_b - T_u))
) / (2 * (z_2 * k_1 + z_1 * (k_2 - k_1)))
c4 = -(
(
-(z_1**2) * z_2 * (k_2 * (H_1 - 2 * H_2) + k_1 * H_2)
+ z_1 * (k_1 - k_2) * (z_2**2 * H_2 + 2 * k_2 * T_b)
- 2 * z_2 * k_1 * k_2 * T_u
)
/ (2 * (z_2 * k_1 + z_1 * (k_2 - k_1)))
)
mask_upper = z < z_1
temperature = np.empty_like(z)
temperature[mask_upper] = (
-(H_1 * z[mask_upper] ** 2) / (2 * k_1) + c1 * z[mask_upper] / k_1 + c2 / k_1
)
temperature[~mask_upper] = (
-(H_2 * z[~mask_upper] ** 2) / (2 * k_2) + c3 * z[~mask_upper] / k_2 + c4 / k_2
)
return temperature
def rhs(x, y):
return np.vstack(
(
y[1], # RHS for temperature in upper layer
np.repeat(
-H_1 / k_1, y[1].size
), # RHS for temperature derivative in upper layer
y[3], # RHS for temperature in lower layer
np.repeat(
-H_2 / k_2, y[3].size
), # RHS for temperature derivative in lower layer
)
)
def bc(ya, yb):
return np.array(
[
yb[0] - T_u, # BC for temperature at upper layer top
ya[2] - T_b, # BC for temperature at lower layer bottom
ya[0] - yb[2], # BC for continuity of temperature
k_1 * ya[1] * ((z_2 - z_1) / z_1) ** 2
- k_2 * yb[3], # BC for continuity of heat flux
]
)
z_1 = 1e4 # Upper layer thickness
z_2 = 3e4 # Total thickness
k_1 = 3 * z_1 # Upper layer thermal conductivity
k_2 = 4 * (z_2 - z_1) # Lower layer thermal conductivity
H_1 = 1e-6 * z_1**3 # Upper layer volumetric heating rate
H_2 = 1e-5 * (z_2 - z_1) ** 3 # Lower layer volumetric heating rate
T_u = 100 # Temperature at upper layer top
T_b = 1600 # Temperature at lower layer bottom
mesh_node_number = int(max(z_1, z_2 - z_1) / 1e3) + 1 # At most 1 km spacing
depths = np.hstack(
(np.linspace(z_2, z_1, mesh_node_number), np.linspace(z_1, 0, mesh_node_number))
)
sol = solve_bvp(
rhs,
bc,
np.linspace(0, 1, mesh_node_number),
np.vstack(
(
np.linspace(T_b, (T_b + T_u) / 2, mesh_node_number),
np.repeat((T_b - T_u) / 2, mesh_node_number),
np.linspace((T_b + T_u) / 2, T_u, mesh_node_number),
np.repeat((T_b - T_u) / 2, mesh_node_number),
)
),
)
plt.plot(np.hstack((sol.y[2], sol.y[0])), depths / 1e3)
plt.plot(
analytical_solution(depths, k_1, k_2, H_1, H_2),
depths / 1e3,
linestyle="none",
marker="o",
)
plt.axhline(z_1 / 1e3, linestyle="dotted", linewidth=0.5, color="grey")
plt.axvline(sol.y[0][0], linestyle="dotted", linewidth=0.5, color="grey")
plt.xlabel("Temperature (K)")
plt.ylabel("Depth (km)")
plt.gca().invert_yaxis()
plt.show()