Search code examples
pythonscipyodediscrete-mathematics

Solution from SciPy solve_ivp contains oscillations for a system of first-order ODEs


I'm trying to solve a system of coupled first-order ODEs:

system of odes

where Tf for this example is considered constant and Q(t) is given. A plot of Q(t) is shown below. The data file used to create the time vs Q plot is available at here.

plot of qt

My Python code for solving this system for the given Q(t) (designated as qheat) is:

import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_ivp

# Data

time, qheat = np.loadtxt('timeq.txt', unpack=True)

# Calculate Temperatures    

def tc_dt(t, tc, ts, q):
    rc = 1.94
    cc = 62.7
    return ((ts - tc) / (rc * cc)) + q / cc

def ts_dt(t, tc, ts):
    rc = 1.94
    ru = 3.08
    cs = 4.5
    tf = 298.15
    return ((tf - ts) / (ru * cs)) - ((ts - tc) / (rc * cs))

def func(t, y):
    idx = np.abs(time - t).argmin()
    q = qheat[idx]

    tcdt = tc_dt(t, y[0], y[1], q)
    tsdt = ts_dt(t, y[0], y[1])
    return tcdt, tsdt

t0 = time[0]
tf = time[-1]
sol = solve_ivp(func, (t0, tf), (298.15, 298.15), t_eval=time)

# Plot

fig, ax = plt.subplots()
ax.plot(sol.t, sol.y[0], label='tc')
ax.plot(sol.t, sol.y[1], label='ts')
ax.set_xlabel('Time [s]')
ax.set_ylabel('Temperature [K]')
ax.legend(loc='best')

plt.show()

This produces the plot shown below but unfortunately several oscillations occur in the results. Is there a better method to solve this coupled system of ODEs?

plot


Solution

  • I finally got a reasonable solution for the system of ODEs by providing the Jacobian matrix to the solver. See below for my working solution.

    import matplotlib.pyplot as plt
    import numpy as np
    from scipy.integrate import solve_ivp
    from scipy.interpolate import interp1d
    
    # Data
    
    time, qheat = np.loadtxt('timeq.txt', unpack=True)
    
    # Calculate Temperatures
    
    interp_qheat = interp1d(time, qheat)
    
    def tc_dt(t, tc, ts, q):
        """
        dTc/dt = (Ts-Tc)/(Rc*Cc) + Q/Cc
        """
        rc = 1.94
        cc = 62.7
        return ((ts - tc) / (rc * cc)) + q / cc
    
    def ts_dt(t, tc, ts):
        """
        dTs/dt = (Tf-Ts)/(Ru*Cs) - (Ts-Tc)/(Rc*Cs)
        """
        rc = 1.94
        ru = 3.08
        cs = 4.5
        tf = 298.15
        return ((tf - ts) / (ru * cs)) - ((ts - tc) / (rc * cs))
    
    def jacobian(t, y):
        """
        Given the following system of ODEs
    
        dTc/dt = (Ts-Tc)/(Rc*Cc) + Q/Cc
        dTs/dt = (Tf-Ts)/(Ru*Cs) - (Ts-Tc)/(Rc*Cs)
    
        determine the Jacobian matrix of the right-hand side as
    
        Jacobian matrix = [df1/dTc, df2/dTc]
                          [df1/dTs, df2/dTs]
    
        where
    
        f1 = (Ts-Tc)/(Rc*Cc) + Q/Cc
        f2 = (Tf-Ts)/(Ru*Cs) - (Ts-Tc)/(Rc*Cs)
        """
        cc = 62.7
        cs = 4.5
        rc = 1.94
        ru = 3.08
        jc = np.array([
            [-1 / (cc * rc), 1 / (cs * rc)],
            [1 / (cc * rc), -1 / (cs * ru) - 1 / (cs * rc)]
        ])
        return jc
    
    def func(t, y):
        """
        Right-hand side of the system of ODEs.
        """
        q = interp_qheat(t)
        tcdt = tc_dt(t, y[0], y[1], q)
        tsdt = ts_dt(t, y[0], y[1])
        return tcdt, tsdt
    
    t0 = time[0]
    tf = time[-1]
    sol = solve_ivp(func, (t0, tf), (298.15, 298.15), method='BDF', t_eval=time, jac=jacobian)
    
    # Plot
    
    fig, ax = plt.subplots(tight_layout=True)
    ax.plot(sol.t, sol.y[0], label='tc')
    ax.plot(sol.t, sol.y[1], label='ts')
    ax.set_xlabel('Time [s]')
    ax.set_ylabel('Temperature [K]')
    ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), frameon=False)
    
    plt.show()
    

    And the generated plot is shown below. plot

    The only advantage to interpolating Q was to speed up the execution of the code by removing the argmin() in the main function. Otherwise, interpolating Q did not improve the results.