1
votes

I am trying to understand how to get the same results in Python as I get in MATLAB. Attached is the source code of what I have tried, results being incorrect for the two different methods. At the bottom of the code is the expected solution as the result of MATLAB. Any help with this problem would be greatly appreciated.

from scipy.integrate import ode
from scipy import integrate
import numpy as np


def function2(x, mu):
    x, y, z = x
    r1 = np.sqrt((x + mu) ** 2 + (y ** 2) + (z ** 2))
    r2 = np.sqrt((x - (1 - mu)) ** 2 + (y ** 2) + (z ** 2))
    u1_x = 1 - (1 - mu) * (1 / (r1 ** 3) - 3 * ((x + mu) ** 2) / (r1 ** 5)) - \
           mu * (1 / (r2 ** 3) - 3 * ((x - (1 - mu)) ** 2) / (r2 ** 5))
    u2_y = 1 - (1 - mu) * (1 / (r1 ** 3)) - 3 * y ** 2 / (r1 ** 5) - \
           mu * (1 / r2 ** 3 - 3 * y ** 2 / r2 ** 5)
    u3_z = (-1) * (1 - mu) * (1 / r1 ** 3) - 3 * z ** 2 / r1 ** 5 - mu * \
           (1 / r2 ** 3 - 3 * z ** 2 / r2 ** 5)
    u1_y = 3 * (1 - mu) * y * (x + mu) / r1 ** 5 + \
           3 * mu * y * (z - (1 - mu)) / r2 ** 5
    u1_z = 3 * (1 - mu) * z * (x + mu) / r1 ** 5 + \
           3 * mu * z * (x - (1 - mu)) / r2 ** 5
    u2_z = 3 * (1 - mu) * y * z / r1 ** 5 + 3 * mu * y * z / r2 ** 5
    u3_y = u2_z
    u2_x = u1_y
    u3_x = u1_z
    gmatrix = np.array([[u1_x, u1_y, u1_z],
                        [u2_x, u2_y, u2_z],
                        [u3_x, u3_y, u3_z]])
    return gmatrix


def function(t, y, mu):
    x = y[36:39]
    GMatrix = function2(x, mu)
    OxO = np.zeros([3, 3])
    Ind = np.identity(3)
    K = np.array([[0, 2, 0], [-2, 0, 0], [0, 0, 0]])
    Df = np.bmat([[OxO[0], Ind[0]],
                  [OxO[1], Ind[1]],
                  [OxO[2], Ind[2]],
                  [GMatrix[0], K[0]],
                  [GMatrix[1], K[1]],
                  [GMatrix[2], K[2]]])
    Df = np.reshape(Df, (6, 6))
    A_temp = np.squeeze(np.array(y))
    A_temp = A_temp.flatten()
    B_temp = [0]*42
    for i in range(len(A_temp)):
       B_temp[i] = A_temp[i]
    B_temp = B_temp[:-6]
    B_temp = np.array(B_temp)
    A = B_temp.reshape(6, 6)
    DfA = np.matmul(Df, A)
    a = [0] * 36
    b = np.squeeze(np.array(DfA))
    b = b.flatten()
    for i in range(len(b)):
        a[i] = b[i]
    r1 = np.sqrt((mu+y[36])**2 + (y[37]**2) + (y[38]**2))
    r2 = np.sqrt((1-mu-y[36])**2 + (y[37]**2) + (y[38]**2))
    m1 = 1 - mu
    m2 = mu
    c = [y[39],
         y[40],
         y[41],
         y[36] + 2 * y[40] + m1 * (-mu - y[36]) / (r1**3) + m2 * (1-mu-y[36]) / (r2**3),
         y[37] - 2 * y[39] - m1 * (y[37]) / (r1**3) - m2 * y[37] / (r2**3),
         -m1 * y[38] / (r1**3) - m2 * y[38] / (r2**3)]
    ydot = a + c
    return ydot

The driver that will integrate the ODE(s):


if __name__ == '__main__':
    t0 = 0
    tf = 1.450000000000000
    mu = 3.054248395728148e-06
    x_n = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
           0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
           0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0, 0, 0, 0, 0, 0, 1.0,
           0.9919755553772727, 0.0, -0.0018716577540106951,
           0.0, -0.0117506137115032, 0.0]
    #meth = 'adams'
    meth = 'bdf'
    r = ode(function).set_integrator('vode',method=meth,rtol=1e-13,
                                                        atol=1e-22,                                                      
                                      with_jacobian=False)
    r.set_initial_value(x_n,t0).set_f_params(mu)
    r.integrate(tf)
    temp = r.y
    index2 = [41, 40, 39, 38, 37, 36]
    temp = np.delete(temp,index2)
    temp = temp.reshape(6,6)
    time = [t0, tf]
    states = integrate.solve_ivp(fun=lambda t, y:
                                 function(t, x_n, mu),
                                 t_span=time, y0=x_n, method='LSODA', dense_output=True,
                                 rtol=1e-13,atol=1e-22)
    new_time = states.t
    new_temp = states.y[:,-1]
    index2 = [41, 40, 39, 38, 37, 36]
    new_temp = np.delete(new_temp,index2)
    new_temp = new_temp.reshape(6,6)
    print(new_temp)
    print(temp)

desired solution // MATLAB ode45 & ode113 same result

enter image description here

This is part of a greater series of scripts that I am writing and would prefer not have my code in MATLAB. I know the MATLAB answer is correct because the the end solutions provides the desired orbit I am trying to create. I should also note that it would appear that MATLAB is using adaptive step sizes and not a predefined time series one would create like in Python np.linspace(start,end,step)

A suggested method was the ivp_solver rk45 with dense_out = true enter image description here

however this method also does not provide the correct results. here are the results to that method: enter image description here

Update: When I manually calculate RK45 on paper with the first time step used by MATLAB I get the same answer. Also, when I force the time series to use the first time interval I get the same answer with the solve_ivp->RK45 with dense out. However even when using the same full time series from MATLAB I get results different from MATLAB.

@Lutz Lehmann After doing some research and testing of a variety of different methods you are correct in that r.integrate only integrate once. In order to integrate at each point a loop is required. Additionally, I was able to get ode and solve_ivp to same answer (although it is the wrong answer). When using solve_ivp I had to do the following which gave me the same answer when using ode.

    r = integrate.solve_ivp(fun=lambda t, y: function(t, y, mu),
                          t_span=time, y0=y, method='RK45', dense_output=True,
                            rtol=1e-13, atol=1e-22)
    i = 0
    while r.t[i] < tf:
        r = integrate.solve_ivp(fun=lambda t, y:  function(t, y, mu),
                                t_span=time, y0=y, method='RK45', dense_output=True,
                                rtol=1e-13, atol=1e-22)
        print(r.t[i])
        i += 1
    new_time = r.t
    new_temp = r.y[:, -1]
    index2 = [41, 40, 39, 38, 37, 36]
    new_temp = np.delete(new_temp, index2)
    print(new_temp)

    r = ode(function)
    r.set_integrator('vode', method='bdf', rtol=1e-13, atol=1e-22, with_jacobian=False)
    r.set_initial_value(y, t0)
    r.set_f_params(mu)
    r.integrate(tf)

    t = []
    Y = [y]

    while r.t < tf:
        r.integrate(tf, step=True)
        Y = np.vstack((Y, [r.y]))
        t.append([r.t])

    new_temp = Y[-1, :]
    index2 = [41, 40, 39, 38, 37, 36]
    new_temp = np.delete(new_temp, index2)
    test = new_temp.reshape(6,6)
    print(test)

I should note that the method using solve_ivp is much slower compared to using ode. The difference in speed yielding the same result probably means that ode is the preferred method (not sure).

This was the solution I got. enter image description here

Unfortunately what means is that based on this latest update conducted from your last post I am back to where I started. ODE and solve_ivp provide the same answer however this is still not the solution.

1
You can debug your ODE functions easily---pass the same inputs and see if they produce the same outputs from Matlab to Python. If that's working, then check the Ode solvers---check you are using the same methods, you have the syntax correct. It's hard to answer your question though because your post isn't formatted very well (you have inconsistent formatting, lots of blank space, it's hard to read). - David
Python also uses adaptive time stepping. Just as you could force evaluation at prescribed times in Matlab. The main difference in quick-use situations is that solve_ivp->RK45 does not have a Refine option as in ode45 (with default value 4, interpolation of additional points). So one would have to use the dense output to fill the gaps to plot a sensible graph. - Lutz Lehmann
@David when debugging the functions the initial time step does produce the same result, however it is the ode solver that is wrong. I am not sure how to correct the solve to produce the same results.Sorry about the formatting it would seem as I copied and past the code form spider a lot of spaces were introduced. If you remove the spaces and correct the formatting it does run in any given python ide. - John Shepard
@LutzLehmann I did try the solve_ivp -> rk45 with dense output unfortunately it produced results with even greater error compared to the method presented and matlab ode45. I am concerned that the adaptive step sizing is not the same and due to the stiffness of the ode that it is producing the incorrect results. If you would like I can post the outcome of the use of solve_ivp with rk45 with dense_output = True. - John Shepard
@LutzLehmann I was unable to post the method you suggested in the comments so I added it to the original post. Both the method and the output can be seen now at the bottom of the question. - John Shepard

1 Answers

0
votes
  • In the solve_ivp call you defined the function wrong, the correct way is
fun=lambda t, y:  function(t, y, mu)
  • The ode solver in the function r.integrate seems to perform at most one internal step. To reach the final time tf, you have to loop this call:
while r.t < tf: r.integrate(tf)

The results of both then are sufficiently coincident, in the leading 10 or so digits. For the source to the differences to the Matlab results see the last section.


PS: You can drastically reduce the second function, all the flattening and copying is not really necessary. I rewrote it as

def function(t, u, mu):
    A = np.reshape(u[:36],[6,6])
    x = u[36:39]
    v = u[39:]

    GMatrix = function2(x, mu)
    OxO = np.zeros([3, 3])
    Ind = np.identity(3)
    K = np.array([[0, 2, 0], [-2, 0, 0], [0, 0, 0]])
 
    Df = np.block([[OxO, Ind],
                   [GMatrix, K]])
    DfA = np.matmul(Df, A)

    x,y,z = x
    vx,vy,vz = v
    r1 = np.sqrt((mu+x)**2 + (y**2) + (z**2))
    r2 = np.sqrt((1-mu-x)**2 + (y**2) + (z**2))
    m1 = 1 - mu
    m2 = mu

    a = [x + 2 * vy - m1 * (mu + x) / (r1**3) + m2 * (1-mu-x) / (r2**3),
         y - 2 * vx - m1 * y / (r1**3) - m2 * y / (r2**3),
         -m1 * z / (r1**3) - m2 * z / (r2**3)]

    ydot = np.concatenate([DfA.flatten(), v, a])
    return ydot

In the computation of the Hessean of the potential in function2 there were many small errors in indices and parentheses placement. Re-organized and with more structuring variables the function can look like

def function2(x, mu):
    ce1, ce2 = -mu, 1-mu
    m1, m2   = 1-mu, mu
    r1 = ((x[0]-ce1)**2+x[1]**2+x[2]**2)**0.5
    r2 = ((x[0]-ce2)**2+x[1]**2+x[2]**2)**0.5
 
    u1_x = 1 - m1 * (1 / r1**3 - 3 * (x[0] - ce1)**2 / r1**5) \
             - m2 * (1 / r2**3 - 3 * (x[0] - ce2)**2 / r2**5)

    u1_y = 3 * m1 * x[1] * (x[0] - ce1) / r1**5 \
         + 3 * m2 * x[1] * (x[0] - ce2) / r2**5

    u1_z = 3 * m1 * x[2] * (x[0] - ce1) / r1**5 \
         + 3 * m2 * x[2] * (x[0] - ce2) / r2**5

    u2_x = u1_y
    
    u2_y = 1 - m1 * (1 / r1**3 - 3 * x[1]**2 / r1**5) \
             - m2 * (1 / r2**3 - 3 * x[1]**2 / r2**5)

    u2_z = 3 * m1 * x[1] * x[2] / r1**5 + 3 * m2 * x[1] * x[2] / r2**5

    u3_x = u1_z
    u3_y = u2_z

    u3_z = - m1 * (1 / r1**3 - 3 * x[2]**2 / r1**5) \
           - m2 * (1 / r2**3 - 3 * x[2]**2 / r2**5)

    GMatrix = np.array([[u1_x, u1_y, u1_z],
                        [u2_x, u2_y, u2_z],
                        [u3_x, u3_y, u3_z]])

    return GMatrix

This gives the results

[ 23.55077315 -0.39182483  3.67078856  4.77188265  4.55322364  0.54862501]
[ -8.519012   -0.71609406 -1.5344374  -2.12546806 -1.4395364  -0.23934585]
[ -0.37941138  0.11874396 -1.39417439 -0.10224713 -0.13959515  0.19607223]
[ 43.56974185 -1.72339855  6.85563491  8.62759039  8.39374083  0.9221739 ]
[-28.39640391 -0.10433561 -4.47605353 -5.56490582 -6.06643015 -0.69034888]

which as far as I can see coincide with the Matlab results.