Пример #1
0
def state2coes( state, args = {} ):
	_args = {
		'et'        : 0,
		'mu'        : pd.earth[ 'mu' ],
		'deg'       : True,
		'print_coes': False
	}

	rp,e,i,raan,aop,ma,t0,mu,ta,a,T = spice.oscltx( 
		state, _args[ 'et' ], _args[ 'mu' ] )

	if _args[ 'deg' ]:
		i    *= nt.r2d
		ta   *= nt.r2d
		aop  *= nt.r2d
		raan *= nt.r2d

	if _args[ 'print_coes' ]:
		print( 'a'   , a    )
		print( 'e'   , e    )
		print( 'i'   , i    )
		print( 'RAAN', raan )
		print( 'AOP' , aop  )
		print( 'TA'  , ta   )
		print()

	return [ a, e, i, ta, aop, raan ]
Пример #2
0
def orbital_elements(date, observer, target, frame='J2000', abcorr='LT+S'):
    state = get_state(date, observer, target, frame, abcorr)

    et = spiceypy.str2et(date)

    mu = spiceypy.bodvrd(observer, 'GM', 1)[1][0]

    #
    # Compute the orbital elements
    #
    elements = spiceypy.oscltx(state, et, mu)

    return elements
Пример #3
0
    def PlotJourneyCentralBodyOrbits(self, JourneyAxes, PlotOptions):
        if PlotOptions.PlotCentralBody != 'Journey central body':
            import spiceypy
            journey_central_body = self.central_body
                            
            if journey_central_body in ['Mars','Jupiter','Uranus','Neptune','Pluto']:
                journey_central_body = journey_central_body + ' Barycenter'

            if journey_central_body != PlotOptions.PlotCentralBody:
                body_state, LT = spiceypy.spkezr(journey_central_body, (self.missionevents[0].JulianDate - 2451545.0) * 86400.0, 'J2000', "None", PlotOptions.PlotCentralBody)
                
                
                r = np.linalg.norm(body_state[0:3])
                v = np.linalg.norm(body_state[3:6])

                mu = {'Mercury':                   22031.780000,
                      'Venus':                    324858.592000,
                      'Earth':                    398600.435436,
                      'Mars':                      42828.375214,
                      'Jupiter Barycenter':    126712764.800000,
                      'Saturn Barycenter':      37940585.200000,
                      'Uranus Barycenter':       5794548.600000,
                      'Neptune Barycenter':      6836527.100580,
                      'Pluto Barycenter':            977.000000,
                      'Sun':                132712440041.939400,
                      'Moon':                       4902.800066,
                      'Earth-Moon Barycenter':    403503.235502}

                elements = spiceypy.oscltx(body_state, (self.missionevents[0].JulianDate - 2451545.0) * 86400.0, mu[PlotOptions.PlotCentralBody])
                a = elements[-2]
                #a = r / (2.0 - r*v*v)
                if a > 0.0:
                    T = 2*math.pi  *math.sqrt(a**3 / mu[PlotOptions.PlotCentralBody]) 
                else:
                    T = 2*math.pi * self.TU

                t0 = (self.missionevents[0].JulianDate - 2451545.0) * 86400.0
                tf = t0 + T
                X = []
                Y = []
                Z = []
                for epoch in np.linspace(t0, tf, num=100):
                    body_state, LT = spiceypy.spkezr(journey_central_body, epoch, 'J2000', "None", PlotOptions.PlotCentralBody)
                            
                    X.append(body_state[0])
                    Y.append(body_state[1])
                    Z.append(body_state[2])

                JourneyAxes.plot(X, Y, Z, lw=1, c='0.75')
                                        et=DATETIME_ET, \
                                        ref='ECLIPJ2000',
                                        obs=10)

#%%

# Get the G*M value for the Sun
_, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1)

GM_SUN = GM_SUN_PRE[0]

#%%

# Compute the orbital elements of Ceres using the computed state vector
CERES_ORBITAL_ELEMENTS = spiceypy.oscltx(state=CERES_STATE_VECTOR, \
                                         et=DATETIME_ET, \
                                         mu=GM_SUN)

# Set and convert the semi-major axis and perihelion from km to AU
CERES_SEMI_MAJOR_AU = spiceypy.convrt(CERES_ORBITAL_ELEMENTS[9], \
                                      inunit='km', outunit='AU')
CERES_PERIHELION_AU = spiceypy.convrt(CERES_ORBITAL_ELEMENTS[0], \
                                      inunit='km', outunit='AU')

# Set the eccentricity
CERES_ECC = CERES_ORBITAL_ELEMENTS[1]

# Set and convert miscellaneous angular values from radians to degrees:
# inc: Inclination
# lnode: Longitude of ascending node
# argp: Argument of perihelion
Пример #5
0
    def __init__(self, body, tEntrance, state, tExit=None):

        self.body = body
        self.entranceState = state

        self.tExit = tExit
        self.tEntrance = tEntrance

        self.GM = body.Gmass[0]

        #output vars explanation
        #https://ssd.jpl.nasa.gov/?sb_elem
        #function documentation
        #https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/oscltx_c.html

        self.ele = sp.oscltx(state, convert(tEntrance),
                             self.GM)  #orbital elements in array form
        self.elements = {  #orbital elements in dict form, for clarity
            "RP": self.ele[0],  #Perifocal distance.
            "ECC": self.ele[1],  #Eccentricity.
            "INC": self.ele[2],  #Inclination.
            "LNODE": self.ele[3],  #Longitude of the ascending node.
            "ARGP": self.ele[4],  #Argument of periapsis.
            "M0": self.ele[5],  #Mean anomaly at epoch.
            "T0": self.ele[6],  #Epoch.
            "GM": self.ele[7],  #Gravitational parameter.
            "NU": self.ele[8],  #True anomaly at epoch.
            "A": self.ele[
                9]  #Semi-major axis. A is set to zero if it is not computable.
        }

        self.ele = self.ele[:8]  #trim for further use
        #print(self.elements)

        r = np.array(state[:3])
        v = np.array(state[3:6])
        #https://space.stackexchange.com/questions/22000/simulate-celestial-body-motion-on-hyperbolic-trajectory-2d
        h = np.cross(r, v)
        e = np.cross(v, h) / self.GM - r / mag(r)

        #eccentricity direction as i
        i = e / np.linalg.norm(e)
        self.eccVec = i
        #rotational momentum direction as k
        k = h / np.linalg.norm(h)
        self.up = k
        #figure out j from the first 2 vectors
        j = np.cross(k, i)

        self.rMtrx = np.array([i, j, k])
        #print('rMtrx:',self.rMtrx)

        #print("ijk:", i, j, k)
        #transform 3d coordinate to 2d coordinate
        rR = squish(r, self.rMtrx)
        vR = squish(v, self.rMtrx)

        self.angleIn = None
        self.angleOut = None

        # dA/dt = r * v / 2
        arealVelocity = mag(rR) * mag(vR) / 2
        self.av = arealVelocity

        if not tExit == None:
            #print(tExit - tEntrance).total_seconds()
            self.angleIn = angleFromR(r, self)
            tState = sp.conics(self.ele, convert(tExit))
            self.angleOut = angleFromR(tState[0:3], self)

            if self.angleOut < self.angleIn:
                self.angleOut += np.pi * 2
        return

        #print("results:", rR, vR)

        #
        #find total area sweeped by the object inside the sphere of influence
        #
        #focal distance = rp + semi major axis, take negative value
        foc = -1 * (abs(self.elements["RP"]) + abs(self.elements["A"]))
        A = self.elements['A']
        RP = self.elements['RP']
        E = self.elements['ECC']
        #x coordinate, when setting the actual origin instead
        #of the focus as the origin
        X = lambda x: foc + x
        # scipy integration -- quad (formula, lower bound, upper bound)
        Area = lambda r: 2 * abs(
            quad(lambda x: math.sqrt(x**2 - A**2), X(r[0]), X(RP))[0] * math.
            sqrt(E**2 - 1) + .5 * np.sign(r[0]) * abs(r[0] * r[1]))
        #print(Area(rR))

        self.deltaT = Area(rR) / arealVelocity
        """
        #exit state using symmetry
        vfR = np.array([vR[0]*-1, vR[1]])
        rfR = np.array([rR[0], rR[1]*-1])

        #convert back to 3d J2000 rather than 2d orbital-plane coords
        self.vf = unSquish(vfR, self.rMtrx)
        self.rf = unSquish(rfR, self.rMtrx)
        """

        #exit state using deltaT
        #self.exitState = sp.prop2b(self.GM, state, self.deltaT)
        self.exitState = sp.conics(self.ele, self.deltaT + self.elements['T0'])

        #print("deltaT: ", self.deltaT, "\nvf:", self.vf, "\nrf: ", self.rf)

        if not self.angleIn == None:
            self.angleIn = angleFromR(r, self)
        if not self.angleOut == None:
            self.angleOut = angleFromR(self.exitState[0:3], self)
            if self.angleOut < self.angleIn:
                self.angleOut += np.pi * 2
Пример #6
0
#%%

# Set a sample Ephemeris Time to compute a sample Jupiter state vector and the
# corresponding orbital elements
sample_et = spiceypy.utc2et('2000-001T12:00:00')

# Compute the state vector of Jupiter as seen from the Sun in ECLIPJ2000
JUPITER_STATE, _ = spiceypy.spkgeo(targ=5, \
                                   et=sample_et, \
                                   ref='ECLIPJ2000', \
                                   obs=10)

# Determine the corresponding orbital elements of Jupiter
JUPITER_ORB_ELEM = spiceypy.oscltx(state=JUPITER_STATE, \
                                   et=sample_et, \
                                   mu=GM_SUN)

# Extract the semi-major axis of Jupiter ...
JUPITER_A = JUPITER_ORB_ELEM[-2]

# ... and print the results in AU
print('Semi-major axis of Jupiter in AU: ' \
      f'{spiceypy.convrt(JUPITER_A, inunit="km", outunit="AU")}')
print('\n')

#%%

# Compute the SOI radius of Jupiter
SOI_JUPITER_R = JUPITER_A * (GM_JUPITER / GM_SUN)**(2.0 / 5.0) * 1
# Create sample Ephemeris Time (ET)
SAMPLE_ET = spiceypy.utc2et('2000-001T00:00:00')

# Compute the state vector of Jupiter's barycentre at the defined ET as seen
# from the Sun
STATE_VEC_JUPITER, _ = spiceypy.spkgeo(targ=5, \
                                       et=SAMPLE_ET, \
                                       ref='ECLIPJ2000', \
                                       obs=10)

# Get the G*M value of the Sun
_, GM_SUN = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1)
GM_SUN = GM_SUN[0]

# Compute the orbital elements of Jupiter ...
ORB_ELEM_JUPITER = spiceypy.oscltx(STATE_VEC_JUPITER, SAMPLE_ET, GM_SUN)

# ... extract the semi-major axis and convert it from km to AU
A_JUPITER_KM = ORB_ELEM_JUPITER[-2]
A_JUPITER_AU = spiceypy.convrt(A_JUPITER_KM, 'km', 'AU')

#%%

# Set the inclination range (however, only from 0 to 90 degrees) and convert
# the degrees values to radians
INCL_RANGE_DEG = np.linspace(0, 90, 100)
INCL_RANGE_RAD = np.radians(INCL_RANGE_DEG)

# Set an array for the eccentricity
E_RANGE = np.linspace(0, 1, 100)
# Convert the UTC date-time strings to ET
comet_67p_df.loc[:, 'ET'] = comet_67p_df['UTC'].apply(lambda x: \
                              spiceypy.utc2et(x.strftime('%Y-%m-%dT%H:%M:%S')))

# Compute the ET corresponding state vectors
comet_67p_df.loc[:, 'STATE_VEC'] = \
    comet_67p_df['ET'].apply(lambda x: spiceypy.spkgeo(targ=1000012, \
                                                       et=x, \
                                                       ref='ECLIPJ2000', \
                                                       obs=10)[0])

# Compute the state vectors corresponding orbital elements
comet_67p_df.loc[:, 'STATE_VEC_ORB_ELEM'] = \
    comet_67p_df.apply(lambda x: spiceypy.oscltx(state=x['STATE_VEC'], \
                                                 et=x['ET'], \
                                                 mu=GM_SUN), \
                       axis=1)

#%%

# Assign miscellaneous orbital elements as individual columns
# Set the perihelion. Convert km to AU
comet_67p_df.loc[:, 'PERIHELION_AU'] = \
    comet_67p_df['STATE_VEC_ORB_ELEM'].apply(lambda x: \
                                             spiceypy.convrt(x[0], \
                                                             inunit='km', \
                                                             outunit='AU'))

# Set the eccentricity
comet_67p_df.loc[:, 'ECCENTRICITY'] = \
Пример #9
0
def cartesian2cometary(epoch, state, frame='ecliptic', mu=cnst.GM):
    """Spiceypy conversion from heliocentric
    cartesian states to cometary orbital elements.

    Parameters:
    -----------
    epoch ... epoch of orbital elements [time units]
    state ... heliocentric ecliptic cartesian state (x,y,z,vx,vy,vz)
    frame ... Coordinate frame of Cartesian states: 'ecliptic', 'icrf'
    mu    ... Gravitational parameter
    
    Returns:
    --------
    com ... orbital elements array
            q    = pericenter distance
            e    = eccentricity
            i    = inclination (deg)
            node = longitude of the ascending node (deg)
            w    = argument of pericenter (deg)
            Tp   = time of (next) pericenter passage [time units]
            
    epoch ... epoch of orbital elements
    period... orbital period [time units]

    External dependencies:
    ----------------------
    spiceypy (imported as sp)
    numpy (imported as np)

    """

    #   Output for spiceypy.oscltx:
    #   q    = pericenter distance
    #   e    = eccentricity
    #   i    = inclination (deg)
    #   node = longitude of the ascending node (deg)
    #   w    = argument of pericenter (deg)
    #   M    = mean anomaly at epoch (deg)
    #   T0   = epoch
    #   mu   = gravitational parameter
    #   NU   = True anomaly at epoch.
    #   a    = Semi-major axis. A is set to zero if it is not computable.
    #   TAU  = Orbital period. Applicable only for elliptical orbits. Set to zero otherwise.

    estate = frameCheck(state, frame)

    oscltx = sp.oscltx(estate, 0, mu)

    com = []
    com_add = com.append
    # pericenter distance q
    com_add(oscltx[0])
    # eccentricity
    com_add(oscltx[1])
    # inclination
    com_add(rad2deg(oscltx[2]))
    # node
    com_add(rad2deg(oscltx[3]))
    # w: argument of pericenter
    com_add(rad2deg(oscltx[4]))
    # period
    period = oscltx[10]
    # mean anomaly
    man = oscltx[5]

    # epoch of pericenter passage
    com_add(epoch - man / PIX2 * period)

    return com, epoch, period
Пример #10
0
def maven_orbit_image(time,
                      camera_pos=[1, 0, 0],
                      camera_up=[0, 0, 1],
                      extent=3,

                      parallel_projection=True,

                      view_from_orbit_normal=False,
                      view_from_periapsis=False,

                      show_maven=False,
                      show_orbit=True,
                      label_poles=None,

                      show=True,
                      transparent_background=False,
                      background_color=(0, 0, 0)):
    """Creates an image of Mars and the MAVEN orbit at a specified time.

    Parameters
    ----------
    time : str
        Time to diplay, in a string format interpretable by spiceypy.str2et.

    camera_pos : length 3 iterable
        Position of camera in MSO coordinates.
    camera_up : length 3 iterable
        Vector defining the image vertical.
    extent : float
        Half-width of image in Mars radii.

    parallel_projection : bool
        Whether to display an isomorphic image from the camera
        position. If False, goofy things happen. Defaults to True.

    view_from_orbit_normal : bool
        Override camera_pos with a camera position along MAVEN's orbit
        normal. Defaults to False.
    view_from_periapsis : bool
        Override camera_pos with a camera position directly above
        MAVEN's periapsis. Defaults to False.

    show_maven : bool
        Whether to draw a circle showing the position of MAVEN at the
        specified time. Defaults to False.
    show_orbit : bool
        Whether to draw the MAVEN orbit. Defaults to True.
    label_poles : bool
        Whether to draw an 'N' and 'S' above the visible poles of Mars.

    show : bool
        Whether to show the image when called, or supress
        display. Defaults to True.
    transparent_background : bool
        If True, the image background is transparent, otherwise it is
        set to background_color. Defaults to False.
    background_color : RGB1 tuple
        Background color to use if transparent_background=False.
        Specified as an RGB tuple with values between 0 and 1.

    Returns
    -------
    rgb_array : 1000x1000x3 numpy array of image RGB values
        Image RGB values.
    return_coords : dict
        Description of the image coordinate system useful for plotting
        on top of output image.

    Notes
    -----
    Call maven_iuvs.load_iuvs_spice() before calling this function to
    ensure kernels are loaded.

    """
    myet = spice.str2et(time)

    # disable mlab display (this is done by matplotlib later)
    mlab.options.offscreen = True

    # create a figure window (and scene)
    mlab_pix = 1000
    mfig = mlab.figure(size=(mlab_pix, mlab_pix),
                       bgcolor=background_color)

    # disable rendering as objects are added
    mfig.scene.disable_render = True

    #
    # Set up the planet surface
    #

    # load and map the Mars surface texture
    image_file = os.path.join(anc_dir, 'marssurface_2.jpg')
    img = tvtk.JPEGReader()
    img.file_name = image_file
    texture = tvtk.Texture(input_connection=img.output_port, interpolate=1)

    # attach the texture to a sphere
    mars_radius = 3395.
    sphere_radius = 1  # radius of planet is 1 rM
    sphere_resolution = 180  # 180 points on the sphere
    sphere = tvtk.TexturedSphereSource(radius=sphere_radius,
                                       theta_resolution=sphere_resolution,
                                       phi_resolution=sphere_resolution)
    sphere_mapper = tvtk.PolyDataMapper(input_connection=sphere.output_port)
    mars = tvtk.Actor(mapper=sphere_mapper, texture=texture)

    # adjust the reflection properties for a pretty image
    mars.property.ambient = 0.2  # so the nightside is slightly visible
    mars.property.specular = 0.15  # make it shinier near dayside

    # now apply the rotation matrix to the planet

    # tvtk only thinks about rotations with Euler angles, so we need
    # to use a SPICE routine to get these from the rotation matrix

    # to get from the surface to MSO coordinates we'd normally do
    # this:
    rmat = spice.pxform('IAU_MARS', 'MAVEN_MSO', myet)

    # but we need to use transpose because spice.m2eul assumes the matrix
    # defines a coordinate system rotation, the inverse of the matrix
    # to rotate vectors
    trmat = spice.pxform('MAVEN_MSO', 'IAU_MARS', myet)

    # now we can get the Euler angles
    rangles = np.rad2deg(spice.m2eul(trmat, 2, 1, 3))
    #                                      ^^^^^^^^
    #                                      2,1,3 because vtk performs
    #                                      rotations in the order
    #                                      z,x,y and SPICE wants these
    #                                      in REVERSE order

    mars.orientation = rangles[[1, 0, 2]]
    #                           ^^^^^^^
    #                           orientation must be specified as x,y,z
    #                           rotations in that order even though they
    #                           are applied in the order above

    # OK, that was hard, but now we're good!

    mfig.scene.add_actor(mars)

    #
    # make a lat/lon grid
    #

    line_x = []
    line_y = []
    line_z = []
    line_o = []

    line_t = np.linspace(0, 2*np.pi, 100)
    line_r = 1.0

    longrid = np.arange(0, 360, 30)
    for lon in longrid:
        line_x.append(line_r*np.cos(np.deg2rad(lon))*np.cos(line_t))
        line_x.append([0])
        line_y.append(line_r*np.sin(np.deg2rad(lon))*np.cos(line_t))
        line_y.append([0])
        line_z.append(line_r*np.sin(line_t))
        line_z.append([0])
        line_o.append(np.ones_like(line_t))
        line_o.append([0])

    latgrid = np.arange(-90, 90, 30)[1:]
    for lat in latgrid:
        line_x.append(line_r*np.cos(np.deg2rad(lat))*np.cos(line_t))
        line_x.append([0])
        line_y.append(line_r*np.cos(np.deg2rad(lat))*np.sin(line_t))
        line_y.append([0])
        line_z.append(line_r*np.sin(np.deg2rad(lat))*np.ones_like(line_t))
        line_z.append([0])
        line_o.append(np.ones_like(line_t))
        line_o.append([0])

    line_x = np.concatenate(line_x)
    line_y = np.concatenate(line_y)
    line_z = np.concatenate(line_z)
    line_o = np.concatenate(line_o)

    linearray = [np.matmul(rmat, [x, y, z]) for x, y, z in zip(line_x,
                                                               line_y,
                                                               line_z)]
    (line_x, line_y, line_z) = np.transpose(np.array(linearray))

    grid_linewidth = 0.25*mlab_pix/1000
    mlab.plot3d(line_x, line_y, line_z, line_o,
                transparent=True,
                color=(0, 0, 0),
                tube_radius=None,
                line_width=grid_linewidth)

    #
    # compute the spacecraft orbit
    #

    # for the given time, we determine the orbit period
    maven_state = spice.spkezr('MAVEN', myet,
                               'MAVEN_MME_2000', 'NONE', 'MARS')[0]
    marsmu = spice.bodvrd('MARS', 'GM', 1)[1][0]
    maven_elements = spice.oscltx(maven_state, myet, marsmu)
    orbit_period = 1.001*maven_elements[-1]

    # make an etlist corresponding to the half-orbit ahead and behind
    orbit_subdivisions = 2000
    etlist = (myet
              - orbit_period/2
              + orbit_period*np.linspace(0, 1,
                                         num=orbit_subdivisions))

    # get the position of the orbit in MSO
    statelist = spice.spkezr('MAVEN', etlist,
                             'MAVEN_MSO', 'NONE', 'MARS')[0]
    statelist = np.append(statelist, [statelist[0]], axis=0)  # close the orbit
    poslist = np.transpose(statelist)[:3]/mars_radius  # scale to Mars radius

    # plot the orbit
    orbitcolor = np.array([222, 45, 38])/255  # a nice red
    orbitcolor = tuple(orbitcolor)
    maven_x, maven_y, maven_z = poslist
    if show_orbit:
        mlab.plot3d(maven_x, maven_y, maven_z,
                    color=orbitcolor,
                    tube_radius=None,
                    line_width=3*mlab_pix/1000)

    if not parallel_projection:
        # add a dot indicating the location of the Sun
        # this only makes sense with a perspective transform... with
        # orthographic coordinates we're always too far away
        # TODO: non parallel projection results in goofy images
        sun_distance = 10
        sun_sphere = tvtk.SphereSource(center=(sun_distance, 0, 0),
                                       radius=1*np.pi/180*sun_distance,
                                       theta_resolution=sphere_resolution,
                                       phi_resolution=sphere_resolution)
        sun_sphere_mapper = tvtk.PolyDataMapper(input_connection=sun_sphere.output_port)
        sun_sphere = tvtk.Actor(mapper=sun_sphere_mapper)
        sun_sphere.property.ambient = 1.0
        sun_sphere.property.lighting = False
        # mfig.scene.add_actor(sun_sphere)

        # put a line along the x-axis towards the sun
        # sunline_x=np.arange(0, 5000, 1)
        # mlab.plot3d(sunline_x, 0*sunline_x, 0*sunline_x,
        #             color=(1.0,1.0,1.0),
        #             tube_radius=None,line_width=6)

    #
    # Define camera coordinates
    #

    if view_from_periapsis:
        # to do this we need to get the position of apoapsis and the
        # orbit normal
        rlist = [np.linalg.norm(p) for p in np.transpose(poslist)]
        apoidx = np.argmax(rlist)
        apostate = spice.spkezr('MAVEN', etlist[apoidx],
                                'MAVEN_MSO', 'NONE', 'MARS')[0]
        camera_pos = -1.0 * apostate[:3]
        camera_pos = 5 * (camera_pos/np.linalg.norm(camera_pos))
        camera_up = np.cross(apostate[:3], apostate[-3:])
        camera_up = camera_up/np.linalg.norm(camera_up)
        parallel_projection = True

    if view_from_orbit_normal:
        # to do this we need to get the position of apoapsis and the
        # orbit normal
        rlist = [np.linalg.norm(p) for p in np.transpose(poslist)]
        apoidx = np.argmax(rlist)
        apostate = spice.spkezr('MAVEN', etlist[apoidx],
                                'MAVEN_MSO', 'NONE', 'MARS')[0]
        camera_up = apostate[:3]
        camera_up = camera_up/np.linalg.norm(camera_up)
        camera_pos = np.cross(apostate[:3], apostate[-3:])
        camera_pos = 5 * (camera_pos/np.linalg.norm(camera_pos))
        parallel_projection = True

    # construct an orthonormal coordinate system
    camera_pos = np.array(camera_pos)
    camera_pos_norm = camera_pos/np.linalg.norm(camera_pos)
    camera_up = (camera_up
                 - camera_pos_norm*np.dot(camera_pos_norm,
                                          camera_up))
    camera_up = camera_up/np.linalg.norm(camera_up)
    camera_right = np.cross(-camera_pos_norm, camera_up)

    # set location of camera and orthogonal projection
    camera = mlab.gcf().scene.camera
    if parallel_projection:
        camera_pos = 5*camera_pos_norm
        camera.parallel_projection = True
        camera.parallel_scale = extent  # half box size
    else:
        # TODO: this results in goofy images, fix this
        camera.parallel_projection = False
        camera.view_angle = 50
    camera.position = np.array(camera_pos)
    camera.focal_point = (0, 0, 0)
    camera.view_up = camera_up
    camera.clipping_range = (0.01, 5000)

    #
    # Set up lighting
    #

    # The only light is the Sun, which is fixed on the MSO +x axis.

    # VTK's default lights are uniform and don't fall off with
    # distance, which is what we want
    mfig.scene.light_manager.light_mode = "vtk"
    sun = mfig.scene.light_manager.lights[0]
    sun.activate = True
    sun_vec = (1, 0, 0)

    # The only way to set a light in mayavi/vtk is with respect to the
    # camera position. This means we have to get elevation/azimuth
    # coordinates for the Sun with respect to the camera, which could
    # be anywhere.

    # Here's how the coordinate system is defined:
    # elevation:
    #    [-90 -- +90]
    #    +90 places the light along the direction of camera_up
    # azimuth:
    #    [-180 -- +180],
    #    +90 is in the plane of camera_up and camera_right.
    #    +/-180 is behind, pointing at the camera
    #    -90 places light to the left

    # so, to get elevation we need to put the sun in scene coordinates
    sun_scene = np.matmul([camera_right, camera_up, camera_pos_norm],
                          sun_vec)

    # elevation is the angle is latitude measured wrt the y-axis of
    # the scene
    sun_elevation = np.rad2deg(np.arcsin(np.dot(sun_scene, [0, 1, 0])))
    # azimuth is the angle in the x-z plane, clockwise from the z-axis
    sun_azimuth = np.rad2deg(np.arctan2(sun_scene[0], sun_scene[2]))

    # now we can set the location of the light, computed to always lie
    # along MSO+x
    sun.azimuth = sun_azimuth
    sun.elevation = sun_elevation

    # set the brightness of the Sun based on the ambient lighting of
    # Mars so there is no washing out
    sun.intensity = 1.0 - mars.property.ambient

    #
    # Render the 3D scene
    #

    mfig.scene.disable_render = False
    # mfig.scene.anti_aliasing_frames = 0 # can uncomment to make
    #                                     # rendering faster and uglier
    mlab.show()

    mode = 'rgba' if transparent_background else 'rgb'
    img = mlab.screenshot(mode=mode, antialiased=True)
    mlab.close(all=True)  # 3D stuff ends here

    #
    # Draw text and labels in matplotlib
    #

    fig, ax = plt.subplots(1, 1,
                           dpi=400*mlab_pix/1000,
                           figsize=(2.5, 2.5))
    ax.imshow(img)

    # put an arrow along the orbit direction
    if show_orbit:
        arrow_width = 5
        arrow_length = 1.5*arrow_width
        # by default, draw the arrow at the closest point on the orbit
        # to the viewer
        arrowidx = np.argmax([np.dot(camera_pos_norm, p) for p in
                              np.transpose(poslist)])
        if view_from_periapsis:
            # draw the arrow 45 degrees after periapsis
            arrowidx = np.argmax(
                [np.dot(
                    (camera_right + camera_pos_norm)/np.sqrt(2),
                    p)
                 for p in np.transpose(poslist)])
        if view_from_orbit_normal:
            # draw the arrow 45 degrees after periapsis
            arrowidx = np.argmax(
                [np.dot(
                    (camera_right-camera_up)/np.sqrt(2.),
                    p)
                 for p in np.transpose(poslist)])

        arrowetlist = etlist[arrowidx] + 5*60*np.array([0, 1])
        arrowstatelist = spice.spkezr('MAVEN', arrowetlist,
                                      'MAVEN_MSO', 'NONE', 'MARS')[0]
        arrowdir = arrowstatelist[1][:3] - arrowstatelist[0][:3]
        arrowdirproj = [np.dot(camera_right, arrowdir),
                        np.dot(camera_up, arrowdir)]
        arrowdirproj = arrowdirproj/np.linalg.norm(arrowdirproj)

        arrowloc = np.transpose(poslist)[arrowidx]
        arrowlocproj = np.array([np.dot(camera_right, arrowloc),
                                 np.dot(camera_up, arrowloc)])
        arrowlocdisp = (arrowlocproj + extent)/extent/2
        arrow = ax.annotate('',
                            xytext=(arrowlocdisp - 0.05*arrowdirproj),
                            xy=(arrowlocdisp + 0.05*arrowdirproj),
                            xycoords='axes fraction',
                            textcoords='axes fraction',
                            arrowprops=dict(facecolor=orbitcolor,
                                            edgecolor='none',
                                            width=0,
                                            headwidth=arrow_width,
                                            headlength=arrow_length))

    # label the poles
    if view_from_periapsis:
        label_poles = True
    if view_from_orbit_normal:
        label_poles = True
    if label_poles is None:
        label_poles = False

    if label_poles:
        # label the north and south pole if they are visible
        def label_pole(loc, lbl):
            polepos = np.matmul(rmat, loc)
            poleposproj = np.array([np.dot(camera_right, polepos),
                                    np.dot(camera_up, polepos)])
            poleposdisp = (poleposproj+extent)/extent/2

            # determine if the north pole is visible
            polevis = (not (np.linalg.norm([poleposproj]) < 1
                            and np.dot(camera_pos, polepos) < 0))
            if polevis:
                polelabel = ax.text(*poleposdisp, lbl,
                                    transform=ax.transAxes,
                                    color='#888888',
                                    ha='center', va='center',
                                    size=4, zorder=1)
                # outline the letter
                polelabel.set_path_effects([
                    path_effects.withStroke(linewidth=0.75, foreground='k')])

        label_pole([0, 0,  1], 'N')
        label_pole([0, 0, -1], 'S')

    if show_orbit:
        # add a mark for periapsis and apoapsis

        rlist = [np.linalg.norm(p) for p in np.transpose(poslist)]

        # find periapsis/apoapsis
        def label_apsis(apsis_fn, label, **kwargs):
            apsisidx = apsis_fn(rlist)
            apsispos = np.transpose(poslist)[apsisidx]
            apsisposproj = np.array([np.dot(camera_right, apsispos),
                                     np.dot(camera_up, apsispos)])
            apsisposdisp = (apsisposproj + extent)/extent/2
            apsisvis = (not (np.linalg.norm([apsisposproj]) < 1
                             and np.dot(camera_pos, apsispos) < 0))

            if apsisvis:
                apsis = mpatches.CirclePolygon(apsisposdisp, 0.015,
                                               resolution=4,
                                               transform=ax.transAxes,
                                               fc=orbitcolor, lw=0, zorder=10)
                ax.add_patch(apsis)
                ax.text(*apsisposdisp, label,
                        transform=ax.transAxes,
                        color='k',
                        ha='center',
                        size=4, zorder=10,
                        **kwargs)

        label_apsis(np.argmin, 'P', va='center_baseline')
        label_apsis(np.argmax, 'A', va='center')

    if show_maven:
        # add a dot for the spacecraft location
        mavenpos = spice.spkezr('MAVEN', myet,
                                'MAVEN_MSO', 'NONE', 'MARS')[0][:3]/mars_radius
        mavenposproj = np.array([np.dot(camera_right, mavenpos),
                                 np.dot(camera_up, mavenpos)])
        mavenposdisp = (mavenposproj + extent)/extent/2
        mavenvis = (not (np.linalg.norm([mavenposproj]) < 1
                         and np.dot(camera_pos, mavenpos) < 0))
        if mavenvis:
            maven = mpatches.Circle(mavenposdisp, 0.012,
                                    transform=ax.transAxes,
                                    fc=orbitcolor,
                                    lw=0, zorder=11)
            ax.add_patch(maven)
            ax.text(*mavenposdisp, 'M',
                    transform=ax.transAxes,
                    color='k', ha='center', va='center_baseline',
                    size=4, zorder=11)

    # suppress all whitespace around the plot
    plt.subplots_adjust(top=1, bottom=0, right=1, left=0,
                        hspace=0, wspace=0)
    plt.margins(0, 0)
    ax.set_axis_off()
    ax.xaxis.set_major_locator(plt.NullLocator())
    ax.yaxis.set_major_locator(plt.NullLocator())

    fig.canvas.draw()

    rgb_array = fig2rgb_array(fig)

    if not show:
        plt.close(fig)

    return_coords = {'extent': extent,
                     'scale': '3395 km',
                     'camera_pos': camera_pos,
                     'camera_pos_norm': camera_pos_norm,
                     'camera_up': camera_up,
                     'camera_right': camera_right,
                     'orbit_coords': poslist}

    return rgb_array, return_coords