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):

Fixed Analytical vs. Numerical solution, with H_1 = H_2 = 0 W/m^3

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:

Analytical vs. Numerical solutions, with H_1 = H_2 = 1.e-5 W/m^3

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.

Asked By: Canada709

||

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()

enter image description here

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()

enter image description here

Answered By: Patol75
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.