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 ]
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
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
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
#%% # 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'] = \
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
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