2

Excuse me for the length of the title please but this is a pretty specific question. I'm currently simulating a launch of a rocket to mars in the 2022 launch window and I noticed that my rocket is a far distance away from Mars, even though it's traveling in the right direction. After simplifying my code to narrow down the problem, I simply plotted the orbits of the Earth and Mars (Using data from NASA's SPICE library) and propagated the position and velocity given to me by the lambert solver I implemented (Universal variables) to plot the final orbit of the rocket.

I'm only letting the Sun's gravity effect the rocket, not the Earth or Mars, to minimize my problem space. Yet even though I've simplified my problem so far, the intersection between Mars' and my rocket's orbits happens well before the time of flight has been simulated all the way, and the minimum distance between the two bodies is more than a million kilometers at all times.

Orbits

That being said, something must be wrong but I cannot find the problem. I've made sure the lambert solver code I copied is correct by comparing it to Dario Izzo's method and both gave the same results. Furthermore, I've also checked that my orbit propagator works by propagating Mars' and the Earth's orbits and comparing those ellipses to the data from SPICE.

In conclusion, I assume this must be a stupid little mistake I made somewhere, but cannot find because I lack experience in this field. Thank you for any help! :)

This is the JupyterLab notebook I used:

import numpy as np
import matplotlib.pyplot as plt
import json
import math
import spiceypy as spice

# Physics
G = 6.6741e-11

class Entity:
    def __init__(self, x, v, m, do_gravity):
        self.x = x
        self.v = v
        self.a = np.array([0,0,0])
        self.m = m
        self.do_gravity = do_gravity

    def apply_force(self, f):
        self.a = np.add(self.a, f / self.m);

    def step(self, dt):
        self.v = np.add(self.v, self.a * dt)
        self.x = np.add(self.x, self.v * dt)
        self.a = np.array([0,0,0])

class StaticEntity(Entity):
    def __init__(self, body, t, do_gravity):
        super().__init__(self.get_state(body, t)[:3], self.get_state(body, t)[3:], self.get_mass(body), do_gravity)
        self.body = body
        self.t = t

    def step(self, dt):
        self.t += dt
        state = self.get_state(self.body, self.t)
        self.x = state[:3]
        self.v = state[3:]

    @classmethod
    def get_state(self, body, t):
        [state, lt] = spice.spkezr(body, t, "J2000", "NONE", "SSB")
        return state * 1000

    @classmethod
    def get_mass(self, body):
        [dim, gm] = spice.bodvrd(body, "GM", 1)
        return gm * 1e9 / G

    def get_position(self, t):
        return self.get_state(self.body, t)[:3]

    def get_velocity(self, t):
        return self.get_state(self.body, t)[3:]

class Propagator:
    def __init__(self, entities):
        self.entities = entities

    def step(self, dt):
        for e1 in self.entities:
            for e2 in self.entities:
                if (e1 is e2) or (not e1.do_gravity) or isinstance(e2, StaticEntity):
                    continue

                diff = np.subtract(e1.x, e2.x)
                fg = G * e1.m * e2.m / np.dot(diff, diff)
                force = fg * diff / np.linalg.norm(diff)

                e2.apply_force(force)

        for entity in self.entities:
            entity.step(dt)


# Lambert solver

def C2(psi):
    if psi >= 0.0:
        sp = math.sqrt(psi)
        return (1 - math.cos(sp)) / psi
    else:
        sp = math.sqrt(-psi)
        return (1 - math.cosh(sp)) / psi

def C3(psi):
    if psi >= 0.0:
        sp = math.sqrt(psi)
        return (sp - math.sin(sp)) / (psi * sp)
    else:
        sp = math.sqrt(-psi)
        return (sp - math.sinh(sp)) / (psi * sp)

def lambert_solve(r1, r2, tof, mu, iterations, tolerance):
    R1 = np.linalg.norm(r1)
    R2 = np.linalg.norm(r2)

    cos_a = np.dot(r1, r2) / (R1 * R2)
    A = math.sqrt(R1 * R2 * (1.0 + cos_a))

    sqrt_mu = math.sqrt(mu)

    if A == 0.0:
        return None

    psi = 0.0
    psi_lower = -4.0 * math.pi * math.pi
    psi_upper =  4.0 * math.pi * math.pi

    c2 = 1.0 / 2.0
    c3 = 1.0 / 6.0

    for i in range(iterations):
        B = R1 + R2 + A * (psi * c3 - 1.0) / math.sqrt(c2)

        if A > 0.0 and B < 0.0:
            psi_lower += math.pi
            B = -B

        chi = math.sqrt(B / c2)
        chi3 = chi * chi * chi

        tof_new = (chi3 * c3 + A * math.sqrt(B)) / sqrt_mu

        if math.fabs(tof_new - tof) < tolerance:
            f = 1.0 - B / R1
            g = A * math.sqrt(B / mu)
            g_dot = 1.0 - B / R2

            v1 = (r2 - f * r1) / g
            v2 = (g_dot * r2 - r1) / g

            return (v1, v2)

        if tof_new <= tof:
            psi_lower = psi
        else:
            psi_upper = psi

        psi = (psi_lower + psi_upper) * 0.5
        c2 = C2(psi)
        c3 = C3(psi)

    return None


# Set up solar system
spice.furnsh('solar_system.tm')

inject_time = spice.str2et('2022 Sep 28 00:00:00')
exit_time = spice.str2et('2023 Jun 1 00:00:00')

sun   = StaticEntity("Sun",             inject_time, True)
earth = StaticEntity("Earth",           inject_time, False)
mars  = StaticEntity("Mars Barycenter", inject_time, False)

(v1, v2) = lambert_solve(earth.get_position(inject_time), mars.get_position(exit_time), exit_time - inject_time, G * sun.m, 1000, 1e-4)
rocket = Entity(earth.x, v1, 100000, False)

propagator = Propagator([sun, earth, mars, rocket])

# Generate data
earth_pos  = [[], [], []]
mars_pos   = [[], [], []]
rocket_pos = [[], [], []]

t = inject_time
dt = 3600 # seconds

while t < exit_time:
    propagator.step(dt)

    earth_pos[0].append(earth.x[0])
    earth_pos[1].append(earth.x[1])
    earth_pos[2].append(earth.x[2])

    mars_pos[0].append(mars.x[0])
    mars_pos[1].append(mars.x[1])
    mars_pos[2].append(mars.x[2])

    rocket_pos[0].append(rocket.x[0])
    rocket_pos[1].append(rocket.x[1])
    rocket_pos[2].append(rocket.x[2])

    t += dt

# Plot data

plt.figure()
plt.title('Transfer orbit')
plt.xlabel('x-axis')
plt.ylabel('y-axis')
plt.plot(earth_pos[0], earth_pos[1], color='blue')
plt.plot(mars_pos[0], mars_pos[1], color='orange')
plt.plot(rocket_pos[0], rocket_pos[1], color='green')

EDIT:

I recently remodeled my code so that it uses orbit class to represent the entities. This actually gave me acceptable results, even though the code is, in theory, not doing anything differently (as far as I can tell; obviously something must be different)

def norm(a):
    return np.dot(a, a)**0.5

def fabs(a):
    return -a if a < 0 else a 

def newton_raphson(f, f_dot, x0, n):
    res = x0

    for i in range(n):
        res = res - f(res) / f_dot(res)

    return res

def get_ephemeris(body, time):
    state, _ = sp.spkezr(body, time, "J2000", "NONE", "SSB")
    return np.array(state[:3]) * ap.units.km, np.array(state[3:]) * ap.units.km / ap.units.s

def get_mu(body):
    _, mu = sp.bodvrd(body, "GM", 1)
    return mu * ap.units.km**3 / ap.units.s**2

class orbit:
    def __init__(self, position, velocity, mu):
        self.position = position
        self.velocity = velocity
        self.mu = mu

    @staticmethod
    def from_body(name, center, time):
        return static_orbit(name, center, time)

    def get_ephemerides(self, t, dt):
        time = 0
        positions = []
        velocities = []
        #M = self.M
        position = self.position
        velocity = self.velocity

        delta_t = dt * ap.units.s
        t1 = t * ap.units.s

        while time < t1:
            g = self.mu / np.dot(position, position)
            g_vec = g * -position / norm(position)
            velocity = np.add(velocity, g_vec * delta_t)
            position = np.add(position, velocity * delta_t)

            positions.append(position)
            velocities.append(velocity)

            time = time + delta_t

        return positions, velocities

class static_orbit(orbit):
    def __init__(self, name, center, time):
        p, v = get_ephemeris(name, time)
        pc, vc = get_ephemeris(center, time)

        super().__init__(p - pc, v - vc, get_mu(center))

        self.name = name
        self.center = center
        self.time = time

    def get_ephemerides(self, t, dt):
        time = 0
        positions = []
        velocities = []

        while time < t:
            p, v = get_ephemeris(self.name, time + self.time)
            pc, vc = get_ephemeris(self.center, time + self.time)

            positions.append(p - pc)
            velocities.append(v - vc)

            time += dt

        return positions, velocities

sp.furnsh('solar_system.tm')

t1 = sp.str2et('2022 Sep 28 00:00:00')
t2 = sp.str2et('2023 Jun 10 00:00:00')

eo = orbit.from_body("Earth", "Sun", t1)
mo = orbit.from_body("Mars Barycenter", "Sun", t1)

earth_x, earth_v = eo.get_ephemerides(t2 - t1, 3600)
mars_x, mars_v = mo.get_ephemerides(t2 - t1, 3600)

l = lambert(earth_x[0], mars_x[-1], t2 - t1, get_mu("Sun"), 1000, 1e-6)

ro = orbit(earth_x[0], l.v1, get_mu("Sun"))

rocket_x, rocket_v = ro.get_ephemerides(t2 - t1, 60)

earth_x = np.array(earth_x)
mars_x = np.array(mars_x)
rocket_x = np.array(rocket_x)

fig = go.Figure()
fig.add_trace(go.Scatter3d(x=earth_x[:,0], y=earth_x[:,1], z=earth_x[:,2], marker_size=1, marker_color='blue'))
fig.add_trace(go.Scatter3d(x=mars_x[:,0], y=mars_x[:,1], z=mars_x[:,2], marker_size=1, marker_color='orange'))
fig.add_trace(go.Scatter3d(x=rocket_x[:,0], y=rocket_x[:,1], z=rocket_x[:,2], marker_size=1, marker_color='green'))
fig.show()

This method generated following plot:

Better orbit

Also, before this is mentioned again, I have varied my integration step size and lambert solver tolerance to no avail, the result was qualitatively different.

J. Lengel
  • 570
  • 3
  • 16
  • is your simulation 2D or 3D ? if 2D then your rocket trajectory intersect however in wrong time ... in case its 3D then also check if the rocket is in the same plane as Sun-Earth-Mars ... you know the rocket might be above/below Mars in the current plot camera settings ... Also take a look at this [Calculate an intercept to an orbit for a given speed](https://stackoverflow.com/a/51854275/2521214) for some ideas. – Spektre Apr 19 '21 at 06:46
  • Thank you for your question! It is in 3D and I have also used plotly to verify that the orbit is off in all three dimensions. It looks like the orbital plane is correct, but it seems like the gravity is just lower – J. Lengel Apr 19 '21 at 09:24
  • Not coding in Python so your code is gibberish to me however if plane is correct and your transit `deltaV` application is correct (both in magnitude and time) then you most likely hit some accuracy problem like too few steps per integration or too high accuracy threshold on either or both parts of the problem (solve the transit and simulate the 2 body problem). For example this `1000, 1e-4)` looks like really small accuracy ... Not sure in what units your tolerance is but 1000 iterations is not much I would try like 1000000 and even that is not much if not integrated properly – Spektre Apr 19 '21 at 10:22
  • for more info see [Is it possible to make realistic n-body solar system simulation in matter of size and mass?](https://stackoverflow.com/a/28020934/2521214) especially the **[Edit3] Improving Newton D'ALembert integration precision even more** part – Spektre Apr 19 '21 at 10:25
  • The tolerance is time, so 1e-4 is in seconds which is actually very low. And 1000 is the maximum amount of iterations so it doesn't really say much about the accuracy. To be honest with you, I didn't really understand much in the answer you linked, but I also know that I tried this stuff but modeled my code with Orbits instead of propagators and I got a final distance of a few thousand kilometers, which is much more acceptable – J. Lengel Apr 19 '21 at 11:17

1 Answers1

1

So, I managed to figure out what the problem was after much head-scratching. I was simply not taking into account that the Sun is not located at (0,0,0) in my coordinate system. I thought this was negligible, but that is what made the difference. In the end, I simply passed the difference between the Earth and Mars's and the Sun's position vectors and passed those into the Lambert solver. This finally gave me the desired results.

The reason that the error ended up being so "small" (It didn't seem like an obvious bug at first) was because my coordinates are centered at the solar system barycenter which is a few million kilometers away from the Sun, as one would expect.

Thanks for the comments!

J. Lengel
  • 570
  • 3
  • 16