def cassini_surfintercerpt(utc, output=False): target = 'TITAN' fixref = 'IAU_TITAN' dref = 'IAU_TITAN' method = 'ELLIPSOID' abcorr = 'NONE' obsrvr = 'CASSINI' state = cassini_phase(utc) dvec = spice.vhat(-state[:3]) et = spice.str2et(utc) [point, trgepc, srfvec] = spice.sincpt(method, target, et, fixref, abcorr, obsrvr, dref, dvec) temp = spice.reclat(point) radius = temp[0] colat = 90 + spice.convrt(temp[1], "RADIANS", "DEGREES") lon = np.mod(spice.convrt(temp[2], "RADIANS", "DEGREES"), 360) if output: print("Distance to Intercept Point", spice.vnorm(srfvec)) print("Radius", radius) print("Colatitude", colat) print("Longtitude", lon) return point, [radius, colat, lon], spice.vnorm(srfvec)
def dn2iof(DN, et, exptime, mode='4X4'): """ Convert DN values in array, to I/F values. Assumes target of Jupiter. Parameters ----- DN: Array of DN values. ET: Time in ET exptime: Exposure time in seconds Optional Parameters ----- Mode: '1X1' or '4X4'. """ RSOLAR_LORRI_1X1 = 221999.98 # Diffuse sensitivity, LORRI 1X1. Units are (DN/s/pixel)/(erg/cm^2/s/A/sr) RSOLAR_LORRI_4X4 = 3800640.0 # Diffuse sensitivity, LORRI 1X1. Units are (DN/s/pixel)/(erg/cm^2/s/A/sr) C = DN # Get the DN values of the ring. Typical value is 1 DN. # Define the solar flux, from Hal's paper. FSOLAR_LORRI = 176. # We want to be sure to use LORRI value, not MVIC value! F_solar = FSOLAR_LORRI # Flux from Hal's paper if '4' in mode: RSOLAR = RSOLAR_LORRI_4X4 if '1' in mode: RSOLAR = RSOLAR_LORRI_1X1 # Calculate the Jup-Sun distance, in AU. km2au = 1 / (u.au/u.km).to('1') # et = sp.utc2et(t_group['UTC'][0]) (st,lt) = sp.spkezr('Jupiter', et, 'J2000', 'LT', 'New Horizons') r_nh_jup = sp.vnorm(st[0:3]) * km2au # NH distance, in AU (st,lt) = sp.spkezr('Jupiter', et, 'J2000', 'LT', 'Sun') r_sun_jup = sp.vnorm(st[0:3]) * km2au # NH distance, in AU # pixscale_km = (r_nh_jup/km2au * (0.3*hbt.d2r / 256)) # km per pix (assuming 4x4) TEXP = exptime I = C / TEXP / RSOLAR # Could use RSOLAR, RJUPITER, or RPLUTO. All v similar, except for spectrum assumed. # Apply Hal's conversion formula from p. 7, to compute I/F and print it. IoF = math.pi * I * r_sun_jup**2 / F_solar # Equation from Hal's paper return IoF
def vector_params(self, vector) : ret = np.full(shape = [len(self.keys)], fill_value = -1000.0) # Calculate the distance of the boresight and the center of the target origin = np.array([0.0, 0.0, 0.0]) nearpoint, rayradius = spice.nplnpt(self.scloc, vector, origin) # Get the intercept to calculate the radius of of the target normal = spice.surfpt(origin, 1000.0 * nearpoint, self.radii[0], self.radii[1], self.radii[2]) ret[self.map['limb_distance']] = rayradius - spice.vnorm(normal) # Calculate the 'occultation' latitude and longitude ret[self.map['bodyintercept']], ret[self.map['limb_lat']], ret[self.map['limb_lon']] = spice.reclat(nearpoint) # Test if the boresight intersects with our target surface try: point = spice.surfpt(self.scloc, vector, self.radii[0], self.radii[1], self.radii[2]) intercept = True except: intercept = False if (intercept) : # Get some angles ret[self.map['phase']], ret[self.map['incidence']], ret[self.map['emission']] = spice.illum(self.target, self.et , self.abcorr, self.spacecraft, point) # Calculate the planetocentric coordinates distance, ret[self.map['lon']], ret[self.map['lat']] = spice.reclat(point) ret[self.map['losdist']] = spice.vnorm(self.scloc - point) # Calculate the planetographic coordinates ret[self.map['lon_graphic']], ret[self.map['lat_graphic']], bodyintercept = spice.recpgr(self.target, point, self.radii[0], self.flattening) # Get the localtime, and convert to decimal hours hr, min, sc, time, ampm = spice.et2lst(self.et, self.target_id, ret[self.map['lon']], 'PLANETOCENTRIC') ret[self.map['localtime']] = hr + min / 60.0 + sc / 3600.0 # For the angles, convert radians to degrees for key in self.angles : if (ret[self.map[key]] != -1000.0) : ret[self.map[key]] = np.rad2deg(ret[self.map[key]]) # Makes sure longitudes wrap 0 to 360, spice returns the Earth-like -180 to 180. longitudes = ['lon', 'limb_lon', 'lon_graphic'] for key in longitudes : ret[self.map[key]] = ret[self.map[key]] % 360 return np.array(ret)
def get_pixel_size_km(probe: str, body: str, time: datetime, fov_full_angle_deg: float, fov_full_px: int) -> float: """ Calculates size of one pixel on body's surface in km at given time. :param probe: SPICE name of probe :param body: SPICE name of target body :param time: datetime of computation :param fov_full_angle_deg: full angle of one FOV dimension :param fov_full_px: full pixel count of the same FOV dimension :return: length of square covered by one pixel in kilometers """ if fov_full_angle_deg <= 0.0: raise ValueError("fov_full_angle_deg must be positive.") if fov_full_angle_deg > 90.0: raise ValueError( f"with fov_full_angle_deg = {fov_full_angle_deg} the calculation would be wildly inaccurate." ) if fov_full_px < 1: raise ValueError("fov_full_px must be at least 1") et = datetime2et(time) nadir_vec = spy.subpnt("INTERCEPT/ELLIPSOID", body, et, f"IAU_{body}", "LT+S", probe)[2] nadir_dist = spy.vnorm(nadir_vec) half_angle_rad = 0.5 * fov_full_angle_deg * np.pi / 180 fov_half_px = fov_full_px / 2 half_angle_km = np.tan(half_angle_rad) * nadir_dist return half_angle_km / fov_half_px
def __init__(self, file): # Get the speed relative to MU69 file_tm = 'kernels_kem_prime.tm' # Start up SPICE if needed if (sp.ktotal('ALL') == 0): sp.furnsh(file_tm) utc_ca = '2019 1 Jan 05:33:00' et_ca = sp.utc2et(utc_ca) (st,lt) = sp.spkezr('New Horizons', et_ca, 'J2000', 'LT', 'MU69') velocity = sp.vnorm(st[3:6])*u.km/u.s # Save the velocity (relative to MU69) self.velocity = velocity # Save the name of file to read self.file = file # Save the area of the s/c self.area_sc = (1*u.m)**2 return
def update_text(date_time): obs2SunVector = sp.spkpos("SUN", date_time, referenceFrame, SPICE_ABERRATION_CORRECTION, SPICE_OBSERVER)[0] obs2SunUnitVector = obs2SunVector / sp.vnorm(obs2SunVector) # obs2SunAngle = sp.vsep(obs2SunUnitVector, np.asfarray([0.0, 0.0, 1.0])) return obs2SunUnitVector
def EoM_Nbody(y, t, masas): M = len(y) N = int(M / 6) #Vector de estado r = np.zeros((N, 3)) v = np.zeros((N, 3)) #Vectores nulo con las derivadas drdt = np.zeros((N, 3)) dvdt = np.zeros((N, 3)) #Asignacin de los vectores de estado for i in range(N): r[i] = y[3 * i:3 * i + 3] v[i] = y[3 * N + 3 * i:3 * N + 3 * i + 3] # Ecuaciones de movimiento for i in range(N): drdt[i] = v[i] for j in range(N): if i == j: continue dvdt[i] += -masas[j] / spy.vnorm(r[i] - r[j])**3 * (r[i] - r[j]) # Devuelve derivadas dydt = np.array([]) for i in range(N): dydt = np.concatenate((dydt, drdt[i])) for i in range(N): dydt = np.concatenate((dydt, dvdt[i])) return dydt
def sunDistanceAU(time: str, target: str) -> float: """Returns distance in AU between Sun and observed body from MRO.""" base_kernel_path = Path(isis.environ["ISIS3DATA"]) / "base" / "kernels" lsk = sorted(Path(base_kernel_path / "lsk").glob("naif*.tls"))[-1] pck = sorted(Path(base_kernel_path / "spk").glob("de*.bsp"))[-1] sat = sorted(Path(base_kernel_path / "spk").glob("mar*.bsp"))[-1] sclk = sorted( Path( Path(isis.environ["ISIS3DATA"]) / "mro" / "kernels" / "sclk" ).glob("MRO_SCLKSCET.*.65536.tsc") )[-1] spiceypy.furnsh([str(lsk), str(pck), str(sat), str(sclk)]) et = spiceypy.scs2e(-74999, time) targ = target.lower() if targ == "sky" or targ == "cal" or targ == "phobos" or targ == "deimos": targ = "mars" (sunv, lt) = spiceypy.spkpos(targ, et, "J2000", "LT+S", "sun") sunkm = spiceypy.vnorm(sunv) # Return in AU units return sunkm / 1.49597870691e8
def EoM_CRTBP(y, t, alpha): r1 = np.array([-alpha, 0, 0]) r2 = np.array([1 - alpha, 0, 0]) omega = np.array([0, 0, 1]) r = y[:3] v = y[3:] R1 = r - r1 R2 = r - r2 drdt = v dvdt = -(1 - alpha) / spy.vnorm( R1)**3 * R1 - alpha / spy.vnorm(R2)**3 * R2 - np.cross( omega, np.cross(omega, r)) - 2 * np.cross(omega, v) dydt = drdt.tolist() + dvdt.tolist() return dydt
def findBoresightUsed(et, boresight_vectors, boresight_names): obs2SunVector = sp.spkpos("SUN", et, SPICE_REFERENCE_FRAME, SPICE_ABERRATION_CORRECTION, SPICE_OBSERVER) v_norm = obs2SunVector[0]/sp.vnorm(obs2SunVector[0]) v_sep_min = 999.0 for boresight_vector, boresight_name in zip(boresight_vectors, boresight_names): v_sep = sp.vsep(v_norm, boresight_vector) * sp.dpr() * 60.0 #arcmins difference if v_sep < v_sep_min: v_sep_min = v_sep boresight_found = boresight_name return boresight_found, v_sep_min
def printBoresights(angleSeparationA, angleSeparationB): """input manual rotation angles from SPICE kernels to calculate new and old boresight""" oldSoBoresight = [0.0, 0.0, 1.0] oldUVISBoresight = [0.0, 0.0, 1.0] rotationMatrixSoUVIS = sp.pxform("TGO_NOMAD_SO", "TGO_NOMAD_UVIS_OCC", sp.utc2et("2018 APR 01 00:00:00 UTC")) oldSoBoresightUVIS = np.dot(oldSoBoresight, rotationMatrixSoUVIS.T) oldBoresightSeparation = sp.vsep(oldUVISBoresight, oldSoBoresightUVIS) * sp.dpr() * 60.0 print("oldBoresightSeparation") print(oldBoresightSeparation) print("angleSeparationB") print(angleSeparationB) #####SAVE THIS IT WORKS!!!###### newSoBoresightTGO = np.asfarray([ -1.0 * np.sin(angleSeparationB / sp.dpr()), \ np.sin(angleSeparationA / sp.dpr()) * np.cos(angleSeparationB / sp.dpr()), \ np.cos(angleSeparationA / sp.dpr()) * np.cos(angleSeparationB / sp.dpr())]) print("newSoBoresightTGO, vnorm = %0.6f" % sp.vnorm(newSoBoresightTGO)) print(newSoBoresightTGO) newUVISBoresightTGO = np.asfarray( [-0.922221097920913, -0.386613383297695, 0.006207330031467]) oldSoBoresightTGO = np.asfarray([-0.92156, -0.38819, 0.00618]) oldUVISBoresightTGO = np.asfarray( [-0.92207347097, -0.3869614566418, 0.0064300242046]) oldNewSoBoresightSeparation = sp.vsep(newSoBoresightTGO, oldSoBoresightTGO) * sp.dpr() * 60.0 print("oldNewSoBoresightSeparation") print(oldNewSoBoresightSeparation) oldNewUVISBoresightSeparation = sp.vsep( newUVISBoresightTGO, oldUVISBoresightTGO) * sp.dpr() * 60.0 print("oldNewUVISBoresightSeparation") print(oldNewUVISBoresightSeparation) newSoUVISBoresightSeparation = sp.vsep( newSoBoresightTGO, newUVISBoresightTGO) * sp.dpr() * 60.0 print("newSoUVISBoresightSeparation") print(newSoUVISBoresightSeparation) oldSoUVISBoresightSeparation = sp.vsep( oldSoBoresightTGO, oldUVISBoresightTGO) * sp.dpr() * 60.0 print("oldSoUVISBoresightSeparation") print(oldSoUVISBoresightSeparation)
def sc_pos(self, timestr): # Convert the timestring to ephemeris time self.et = spice.str2et(str(timestr)) self.et = self.et + self.time_offset self.data['sc_wrt_planet'], self.data['target_light_time'] = spice.spkpos(self.spacecraft, self.et, self.iref, 'NONE', self.target) self.data['target_distance'] = spice.vnorm(self.data['sc_wrt_planet']) self.data['sc_wrt_sun'], light_times = spice.spkpos(self.spacecraft, self.et, self.iref, 'NONE', 'SUN') #self.ticks = spice.sce2t(self.sc_id, self.et) # Retrieve the position of the spacecraft relative to the target state, self.ltcorr = spice.spkezr(self.target, self.et, self.iref, self.abcorr, self.spacecraft) scloc = state[0:3] # Get the sub-spacecraft coordinates point, epoch, vector = spice.subpnt('NEAR POINT/ELLIPSOID', self.target, self.et, self.framestring, self.abcorr, self.spacecraft) self.distance, self.data['lon_sc'], self.data['lat_sc'] = spice.reclat(point) # Get the localtime of the spaceship hr, min, sc, time, ampm = spice.et2lst(self.et, self.target_id, self.data['lon_sc'], 'PLANETOCENTRIC') self.data['localtime_sc'] = hr + min / 60.0 + sc / 3600.0 # Get the subsolar coordinates point, epoch, vector = spice.subslr('NEAR POINT/ELLIPSOID', self.target, self.et, self.framestring, self.abcorr, self.spacecraft) distance, self.data['lon_sun'], self.data['lat_sun'] = spice.reclat(point) # Convert angles from radians to degrees for key in ['lat_sc', 'lon_sc', 'lat_sun', 'lon_sun'] : self.data[key] = np.rad2deg(self.data[key]) for key in ['lon_sc', 'lon_sun'] : self.data[key] = self.data[key] % 360 state_planet = spice.spkssb(self.target_id, self.et, self.iref) inert2p = spice.pxform(self.iref, self.framestring, self.et) self.scloc = np.matmul(-inert2p, scloc) self.i2p = spice.pxform(self.instrument, self.framestring, self.et) return self.data
def dooneRandom(): ### Random normal unit vector unitNormal = sp.vhat([r.uniform(-1, 1) for i in xrange(3)]) ### Random positive semi-axes' lengths semi_axes = [abs(r.uniform(.5, 100)) for i in xrange(3)] ### Solve for surface point with that surface normal xyz = norm2surfpt(semi_axes, unitNormal) ### Use spiceypy.surfnm() to calculate surface unit normal vector, at ### solved-for surface point vector xyz, to compare to input unit normal ### vector, and calculate error magnitude, errMag surfnm = sp.surfnm(semi_axes[0], semi_axes[1], semi_axes[2], xyz) errVec = sp.vsub(surfnm, unitNormal) errMag = sp.vnorm(errVec) if doLog: pprint.pprint(dict(errVec=errVec, errMag=errMag)) ###return error magnitude return errMag
f'{TIME_ARRAY[indices_min[1]]}') print('\n') #%% # ... but is the ion tail "aiming" towards the trajectory of the spacecraft? # (at least within a few degrees?) # Compute the angular distance between the trajectories' closest approach # Set the closest approach vectors, based on the obtained indices for ATLAS and # the Solar Orbiter, respectively VEC_ATLAS_AP = atlas_vecs[indices_min[0]] VEC_SOLAR_ORB_AP = solar_orb_vecs[indices_min[1]] # Determine the norm of both closest approach vectors ATLAS_NORM_AP = spiceypy.vnorm(VEC_ATLAS_AP) SOLORB_NORM_AP = spiceypy.vnorm(VEC_SOLAR_ORB_AP) # Compute the dot product DOT_PRODUCT_AP = np.dot(VEC_ATLAS_AP, VEC_SOLAR_ORB_AP) # Compute the angle ANGULAR_DIST_AP = np.degrees(np.arccos((DOT_PRODUCT_AP) \ / (ATLAS_NORM_AP * SOLORB_NORM_AP))) # Print the angular distance between ATLAS' ion tail direction and the position # vector of the spacecraft at the closest approach print('Minimum angular distance between a possible ion tail and the ' \ 'Solar Orbiter\'s trajectory in degrees: ' \ f'{np.round(ANGULAR_DIST_AP, 2)}')
SOLAR_SYSTEM_DF.loc[:, 'POS_SSB_WRT_SUN'] = \ SOLAR_SYSTEM_DF['ET'].apply(lambda x: spiceypy.spkgps(targ=0, \ et=x, \ ref='ECLIPJ2000', \ obs=10)[0]) # Now the SSB position vector is scaled with the Sun's radius SOLAR_SYSTEM_DF.loc[:, 'POS_SSB_WRT_SUN_SCALED'] = \ SOLAR_SYSTEM_DF['POS_SSB_WRT_SUN'].apply(lambda x: x / RADIUS_SUN) # Finally the distance between the Sun and the SSB is computed. The length # (norm) of the vector needs to be determined with the SPICE function vnorm(). # numpy provides an identical function in: numpy.linalg.norm() SOLAR_SYSTEM_DF.loc[:, 'SSB_WRT_SUN_SCALED_DIST'] = \ SOLAR_SYSTEM_DF['POS_SSB_WRT_SUN_SCALED'].apply(lambda x: \ spiceypy.vnorm(x)) #%% # Import the matplotlib library from matplotlib import pyplot as plt # Set a figure FIG, AX = plt.subplots(figsize=(12, 8)) # Plot the distance between the Sun and the SSB AX.plot(SOLAR_SYSTEM_DF['UTC'], SOLAR_SYSTEM_DF['SSB_WRT_SUN_SCALED_DIST'], \ color='tab:blue') # Set a label for the x and y axis and color the y ticks accordingly AX.set_xlabel('Date in UTC')
degPerHour = 2.0 * dpr * twopi / hpd xTolerance = lambda xDiff: xDiff < 1e-10 xDiffTolerance = lambda x, xExpect: xTolerance(abs(x - xExpect)) et, iPass = et0, 0 while et < (et0 + spd + 1): ### Get the state of the MINUTE and HOUR bodies every half hour stMinute, lt = sp.spkezr(sMinute, et, 'J2000', 'NONE', sClock) stHour, lt = sp.spkezr(sHour, et, 'J2000', 'NONE', sClock) if (iPass % 2): ### On the half hour, the minute hand will be at RA=180, along [-1,0,0] assert xTolerance( sp.vnorm(sp.vsub([-1.0, 0., 0.], sp.vhat(stMinute[:3])))) else: ### On the hour, the minute hand will be at RA=0, at [+1,0,0] assert xTolerance( sp.vnorm(sp.vsub([1.0, 0., 0.], sp.vhat(stMinute[:3])))) vsepDeg = dpr * sp.vsep(stHour[:3], stMinute[:3]) vsepExpectDeg = (degPerHour * (iPass >> 1)) % 360 ### On the hour, the houre hand will be (30 * iPass) degrees clockwise ### from +X, and therefor also the same from the minute hand assert xDiffTolerance(vsepDeg, vsepExpectDeg) or xDiffTolerance( (360 - vsepDeg), vsepExpectDeg) ### Step to next half hour and pass et += halfHour iPass += 1
### fixed star position with LT+S correction earth2star_lts = sp.spkcpt(vstar, ssb, j2000, et, j2000, "observer", LTS, "earth")[0] if do_debug: ### Ensure that light-time correction is zero for earth->star vector ### for star at fixed positon relative to SSB earth2star_lt = sp.spkcpt(vstar, ssb, j2000, et, j2000, "observer", LT, "earth")[0] assert 0.0 == sp.vnormg(sp.vsubg(earth2star_none, earth2star_lt, 6), 6) ### Calculate differences between gaiif_util.py and SPICE results ... ab_vec_spice = sp.vsub(earth2star_lts[:3], earth2star_none[:3]) ab_vec_gaia = sp.vscl(range2star, sp.vsub(uvstar_ab, uvstar_noab)) ab_vec_diff = sp.vsub(ab_vec_spice, ab_vec_gaia) ab_diff_frac = sp.vnorm(ab_vec_diff) / sp.vnorm(ab_vec_spice) no_ab_diff = sp.vsep(uvstar_noab, sp.vhat(earth2star_none[:3])) ab_diff = sp.vsep(uvstar_ab, sp.vhat(earth2star_lts[:3])) ab_diff_frac = sp.vsep(uvstar_ab, sp.vhat(earth2star_lts[:3])) try: ### ... Success assert 1e-15 > no_ab_diff assert 1e-8 > ab_diff except: ### ... Failure print( dict(no_ab_diff=no_ab_diff, ab_diff=ab_diff, ab_diff_frac=ab_diff_frac,
def __init__(self, dir, do_force=False, do_verbose=False, nmax=None, prefix='lor', do_save=False) : """ Init method: load the index and all files. This does not align, register, stack, or anything like that. It just loads the images. If a saved .pkl file is available, then it will be used instead of reading individual images. Parameters ---- dir: The directory with all the files Optional keyword parameters ---- do_force: Force loading stack from original files, rather than from a .pkl file. do_verbose: List each file explicitly when reading prefix: A text prefix which each filename must match (e.g., 'lor' to match only LORRI files). do_save: If set, save the results of this stacking as a .pkl file """ # If we are passed a pickle file, then restore the file from pickle, rather than by reading in explicity. # I haven't tested this -- not sure if it works. if 'pkl' in dir: self.file_save = dir self.load() return name_target = 'MU69' do_lorri_destripe = True # I didn't use this at first, but it is a clear improvement. do_lorri_dedark = True # Remove dark current from LORRI? files1 = glob.glob(os.path.join(dir, prefix + '*.fit*')) # Look in dir files2 = glob.glob(os.path.join(dir, '*', prefix + '*.fit*')) # Look in subdirs files = files1 + files2 # Truncate the list, if requested if (nmax): files = files[0:nmax] num_files = len(files) self.file_save = os.path.join(dir, 'image_stack_n{}.pkl'.format(num_files)) # Initialize the center of this image. The shfits of each image are taken to be relative to this. # It could be that we just keep this at zero. Time will tell. self.shift_x_pix_center = 0 self.shift_y_pix_center = 0 # Set the internal zoom level, which will be used when flattening self.zoom = 1 # Set a flag to indicate if flattened or not self.flattened = False # If a save file exists, then load it, and immediately return if (os.path.isfile(self.file_save)) and not(do_force): self.load() return mode = [] exptime = [] filename_short = [] exptime = [] visitnam = [] sapname = [] sapdesc = [] reqid = [] et = [] utc = [] target = [] # Set up the table 't'. This is an astropy table within the stack, that has a list of all of the # useful image parameters taken from the FITS header. # Fields in the table are: # filename_short # exptime # visitname # sapname # sapdesc # target # reqid # et # utc # shift_x_pix -- the shift of this image, relative to the zero point (tbd) # shift_y_pix -- the shift of this image, relative to the zero point (tbd) # ra -- # dec # angle # dx_pix -- x dimension # dy_pix -- y dimension self.t = Table( [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [] ], names=('filename', 'filename_short', 'exptime', 'visitname', 'sapname', 'sapdesc', 'target', 'reqid', 'et', 'utc', 'shift_x_pix', 'shift_y_pix', 'ra_center', 'dec_center', 'angle', 'dx_pix', 'dy_pix', 'pixscale_x_km', 'pixscale_y_km', 'dist_target_km', 'wcs', 'data'), dtype = ('U150', 'U50', 'float64', 'U50', 'U50', 'U50', 'U50', 'U50', 'float64', 'U50', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', 'object', 'object' )) # Read the LORRI dark frame. This is a one-off frame for MU69 approach, made by Marc Buie. # It is only valid for 4x4 LORRI. # Units are DN/sec of dark current. if do_lorri_dedark: file_dark = '/Users/throop/Data/MU69_Approach/2018dark_mwb_v2.fits' hdu = fits.open(file_dark) arr_lorri_dark = hdu['PRIMARY'].data hdu.close() if (len(files)): print("Reading {} files from {}".format(len(files), dir)) for i,file in enumerate(files): # Read the data hdulist = fits.open(file) arr = hdulist[0].data err = hdulist[1].data quality = hdulist[2].data backplane_radius = hdulist['RADIUS_EQ'].data dx_pix = hbt.sizex(arr) # Usually 256 or 1024 dy_pix = hbt.sizey(arr) filename_short = os.path.basename(file).replace('.fits', '').replace('lor_', '')\ .replace('_0x633_pwcs','')\ .replace('_0x630_pwcs','') exptime = hdulist[0].header['EXPTIME'] visitnam= hdulist[0].header['VISITNAM'] sapname = hdulist[0].header['SAPNAME'] sapdesc = hdulist[0].header['SAPDESC'] target = hdulist[0].header['TARGET'] reqid = hdulist[0].header['REQID'] et = hdulist[0].header['SPCSCET'] angle = hdulist[0].header['SPCEMEN']*hbt.d2r # Boresight roll angle utc = sp.et2utc(et, 'C', 1) if do_verbose: print("Read {}/{} {}".format(i, len(files), filename_short)) else: print(".", end="") hdulist.close() # Destripe if requested (aka remove jailbars) if do_lorri_destripe: arr = hbt.lorri_destripe(arr) # Calibrate out dark current, if requested, and if a 4X4 if do_lorri_dedark and (hbt.sizex(arr) == 256): arr = arr - exptime * arr_lorri_dark # Read the WCS coords of this file. # XXX Suppress warnings about WCS SIP coords which Buie's files get. # However, looks like AstroPy doesn't use proper warning mechanism?? with warnings.catch_warnings(): warnings.simplefilter("ignore") w = WCS(file) # Get the RA/Dec location of the central pixel, in radians ra_center = hdulist[0].header['CRVAL1']*hbt.d2r dec_center = hdulist[0].header['CRVAL2']*hbt.d2r pixscale_x = abs(hdulist[0].header['CD1_1']*hbt.d2r) # radians per 4x4 pixel pixscale_y = abs(hdulist[0].header['CD2_2']*hbt.d2r) # Sometimes this is negative? # Initialize the shift amount for this image, in pixels shift_x_pix = 0 shift_y_pix = 0 # Calc the distance to MU69, and the pixel scale (non-zoomed) (st, lt) = sp.spkezr(name_target, et, 'J2000', 'LT', 'New Horizons') dist_target_km = sp.vnorm(st[0:2]) pixscale_x_km = dist_target_km * pixscale_x pixscale_y_km = dist_target_km * pixscale_y # Load the values for this image into a row of the astropy table # *** Need to add backplane data here, as planes[] or backplanes[] *** self.t.add_row( [file, filename_short, exptime, visitnam, sapname, sapdesc, target, reqid, et, utc, shift_x_pix, shift_y_pix, ra_center, dec_center, angle, dx_pix, dy_pix, # X and Y dimensions pixscale_x_km, pixscale_y_km, dist_target_km, w, # WCS object arr]) # Actual imaging data # End of loop over files else: print(f"No files found in {dir}!") return # if do_verbose: print("\n") # Print CR at end of "...." # Sort by ET. self.t.sort('et') # Save the pixel scale, from most recent image. We assume that the pixel scale of all frames is identical. self.pixscale_x = pixscale_x self.pixscale_y = pixscale_y self.pixscale_x_km = pixscale_x_km self.pixscale_y_km = pixscale_y_km self.dist_target_km = dist_target_km self.et = et # Save the image size, from most recent image self.dx_pix = dx_pix self.dy_pix = dy_pix # Save the image stack size so it can be easily retrieved self.size = (len(files), dx_pix, dy_pix) # Finally, remove a few columns that we don't need, or that are wrong. self.t.remove_column('sapdesc') self.t.remove_column('sapname') self.t.remove_column('target') self.t.remove_column('visitname') # Initialize the 'indices' vector, which indicates which planes we use for flattening self.indices = np.ones(len(self.t), dtype=bool) # Initialize the num_planes vector to set the size self.num_planes = len(files) # If we generated the files manually (not by reloading), and flag is not set, then offer to save them # if not(do_save): # # print(f'File to save: {self.file_save}') # answer = input(f'Save to pickle file {self.file_save.split("/")[-1]}? ') # if ('y' in answer): # do_save = True if do_save: self.save() # Return. Looks like an init method should not return anything. print()
plt.plot((radec_corners[:,0]-ra_jup)*hbt.r2d * (-1), (radec_corners[:,1]-dec_jup)*hbt.r2d, label = f'{index_group}/{index_image}') # Plot all the four points for this box is_limb_left = (radec_corners[0,0]-ra_jup)>0 if is_limb_left: name_limb_arr.append('Left') else: name_limb_arr.append('Right') # Get the distance from Jupiter center to image center vec_nh_center = sp.radrec(1, radec_center[0][0], radec_center[0][1]) # Vector from NH, to center of LORRI frame ang_jup_center = sp.vsep(vec_nh_jup, vec_nh_center) # Ang Sep btwn Jup and LORRI, radians dist_jup_center_rj = ang_jup_center * sp.vnorm(vec_nh_jup) / rj_km # Convert from radians into RJ width_lorri = 0.3*hbt.d2r # LORRI full width, radians dist_jup_center_rj_range = np.array([ang_jup_center-width_lorri/2, ang_jup_center+width_lorri/2]) * \ sp.vnorm(vec_nh_jup) / rj_km # Finally, we have min and max dist # in the central row of array, in rj range_rj_arr.append(sp.vnorm(vec_nh_jup)/rj_km) # Calc the elevation angle (aka sub-obs lat on Jup) mx = sp.pxform('J2000', 'IAU_JUPITER', t_i['ET']) vec_nh_jup_jup = sp.mxv(mx, vec_nh_jup) (dist, lon, lat) = sp.recrad(-vec_nh_jup_jup) angle_elev_arr.append(lat * hbt.r2d) # Save the sub-obs latitude, in degrees
def nh_ort1_make_stacks(): """ This program takes a directory full of individual NH KEM Hazard frames, stacks them, and subtracts a stack of background field. This reveals rings, etc. in the area. Written for NH MU69 ORT1, Jan-2018. """ do_force = False # Boolean: Do we force reloading of all of the images from FITS, or just restore from pkl? # Pkl (aka False) is faster. But if we have made changes to the core algorithms, must # reload from disk (aka True). stretch_percent = 90 stretch = astropy.visualization.PercentileInterval(stretch_percent) # PI(90) scales to 5th..95th %ile. reqids_haz = ['K1LR_HAZ00', 'K1LR_HAZ01', 'K1LR_HAZ02', 'K1LR_HAZ03', 'K1LR_HAZ04'] reqid_field = 'K1LR_MU69ApprField_115d_L2_2017264' dir_data = '/Users/throop/Data/ORT1/throop/backplaned/' zoom = 4 # Set the edge padding large enough s.t. all output stacks will be the same size. # This value is easy to compute: loop over all stacks, and take max of stack.calc_padding()[0] padding = 61 # Start up SPICE if needed if (sp.ktotal('ALL') == 0): sp.furnsh('kernels_kem_prime.tm') # Set the RA/Dec of MU69. We could look this up from SPICE but it changes slowly, so just keep it fixed for now. radec_mu69 = (4.794979838984583, -0.3641418801015417) # Load and stack the field images stack_field = image_stack(os.path.join(dir_data, reqid_field), do_force=do_force) stack_field.align(method = 'wcs', center = radec_mu69) img_field = stack_field.flatten(zoom=zoom, padding=padding) if do_force: stack_field.save() hbt.figsize((12,12)) hbt.set_fontsize(15) for reqid in reqids_haz: stack_haz = image_stack(os.path.join(dir_data, reqid), do_force=do_force) stack_haz.align(method = 'wcs', center = radec_mu69) img_haz = stack_haz.flatten(zoom=zoom, padding=padding) if do_force: stack_haz.save() # Make the plot diff = img_haz - img_field diff_trim = hbt.trim_image(diff) plt.imshow(stretch(diff_trim)) plt.title(f"{reqid} - field, zoom = {zoom}") # Save the stacked image as a FITS file file_out = os.path.join(dir_data, reqid, "stack_n{}_z{}.fits".format(stack_haz.size[0], zoom)) hdu = fits.PrimaryHDU(stretch(diff_trim)) hdu.writeto(file_out, overwrite=True) print(f'Wrote: {file_out}') # Save the stack as a PNG file_out_plot_stack = file_out.replace('.fits', '.png') plt.savefig(file_out_plot_stack, bbox_inches='tight') print("Wrote: {}".format(file_out_plot_stack)) # Display it # This must be done *after* the plt.savefig() plt.show() # Make a radial profile pos = np.array(np.shape(diff))/2 (radius, profile) = get_radial_profile_circular(diff, pos=pos, width=1) hbt.figsize((10,8)) hbt.set_fontsize(15) plt.plot(radius, profile) plt.xlim((0, 50*zoom)) plt.ylim((-1,np.amax(profile))) plt.xlabel('Radius [pixels]') plt.title(f'Ring Radial Profile, {reqid}, zoom={zoom}') plt.ylabel('Median DN') plt.show() # ============================================================================= # Calculate how many DN MU69 should be at encounter (K-20d, etc.) # Or alternatively, convert all of my DN values, to I/F values # ============================================================================= # Convert DN values in array, to I/F values RSOLAR_LORRI_1X1 = 221999.98 # Diffuse sensitivity, LORRI 1X1. Units are (DN/s/pixel)/(erg/cm^2/s/A/sr) RSOLAR_LORRI_4X4 = 3800640.0 # Diffuse sensitivity, LORRI 1X1. Units are (DN/s/pixel)/(erg/cm^2/s/A/sr) C = profile # Get the DN values of the ring. Typical value is 1 DN. # Define the solar flux, from Hal's paper. FSOLAR_LORRI = 176. # We want to be sure to use LORRI value, not MVIC value! F_solar = FSOLAR_LORRI # Flux from Hal's paper RSOLAR = RSOLAR_LORRI_4X4 # Calculate the MU69-Sun distance, in AU (or look it up). km2au = 1 / (u.au/u.km).to('1') et = stack_haz.t['et'][0] (st,lt) = sp.spkezr('MU69', et, 'J2000', 'LT', 'New Horizons') r_nh_mu69 = sp.vnorm(st[0:3]) * km2au # NH distance, in AU (st,lt) = sp.spkezr('MU69', et, 'J2000', 'LT', 'Sun') r_sun_mu69 = sp.vnorm(st[0:3]) * km2au # NH distance, in AU pixscale_km = (r_nh_mu69/km2au * (0.3*hbt.d2r / 256)) / zoom # km per pix (assuming 4x4) TEXP = stack_haz.t['exptime'][0] I = C / TEXP / RSOLAR # Could use RSOLAR, RJUPITER, or RPLUTO. All v similar, except for spectrum assumed. # Apply Hal's conversion formula from p. 7, to compute I/F and print it. IoF = math.pi * I * r_sun_mu69**2 / F_solar # Equation from Hal's paper plt.plot(radius * pixscale_km, IoF) plt.xlim((0, 50000)) plt.ylim((-1e-7, 4e-7)) # plt.ylim((0,np.amax(IoF))) # plt.yscale('log') plt.xlabel('Radius [km]') plt.title(f'Ring Radial Profile, {reqid}, zoom={zoom}') plt.ylabel('Median I/F') file_out_plot_profile = file_out.replace('.fits', '_profile.png') plt.savefig(file_out_plot_profile, bbox_inches='tight') plt.show() print(f'Wrote: {file_out_plot_profile}') # Write it to a table t = Table([radius, radius * pixscale_km, profile, IoF], names = ['RadiusPixels', 'RadiusKM', 'DN/pix', 'I/F']) file_out_table = file_out.replace('.fits', '_profile.txt') t.write(file_out_table, format='ascii', overwrite=True) print("Wrote: {}".format(file_out_table))
d_radius = 500 if (sequence == 'D211'): d_radius = 1000 if (sequence == 'D202_LORRI'): d_radius = 500 if (sequence == 'D305_LORRI'): d_radius = 4000 # Look up the distance using SPICE for name_body_i in name_body: (vec_body,junk) = sp.spkezr(name_body_i, et, 'IAU_PLUTO', 'LT', 'Pluto') d_pluto_body[name_body_i] = sp.vnorm(vec_body[0:3]) # Generate a pixel mask showing the orbit of each body mask_orbit[name_body_i] = \ np.array(radius > (d_pluto_body[name_body_i] - d_radius)) & \ np.array(radius < (d_pluto_body[name_body_i] + d_radius)) r_h = d_pluto_body['Hydra'] # Hydra orbital radius mask_orbit['Hydra x 2'] = \ np.array(radius > (r_h*2 - d_radius)) & np.array(radius < (r_h*2 + d_radius)) mask_orbit['Hydra x 4'] = \ np.array(radius > (r_h*4 - d_radius)) & np.array(radius < (r_h*4 + d_radius))
ddt = 1*minute # Time offset for my calculation of velocity width_pix_rad_4x4 = 4 * (0.3*hbt.d2r / 1024) # LORRI 4x4 pixel size, in radians width_pix_rad_1x1 = (0.3*hbt.d2r / 1024) # LORRI 4x4 pixel size, in radians et_0 = et_ca + dt (state_0, lt_0) = sp.spkezr('MU69', et_0, 'J2000', 'LT+S', 'New Horizons') (state_1, lt_1) = sp.spkezr('MU69', et_0 + ddt, 'J2000', 'LT+S', 'New Horizons') (junk, ra_0, dec_0) = sp.recrad(state_0[0:3]) (junk, ra_1, dec_1) = sp.recrad(state_1[0:3]) omega_kbo = sp.vsep(state_0[0:3], state_1[0:3]) / ddt # Radians per second of sky motion that the KBO has, from NH dist_kbo = sp.vnorm(state_0[0:3]) # Distance to KBO, in km # Calculate the shadow velocity of the KBO v_shadow_kbo = omega_kbo * dist_kbo # km/sec of the shadow # Calculate the time resolution of the LORRI driftscan. # That is, how long does it take for LORRI to drift one 4x4 LORRI pixel? # [A: Basically one sec: 0.681 sec, technically.] dt_lorri_driftscan = width_pix_rad_4x4 / rate_drift # Calculate Roche radius r_kbo = 16.5 * u.km r_roche = 2.5 * r_kbo
def norm2surfpt(semi_axes, inputNormal): ### Ellipsoid semi-axes' lengths, per the ellipsoid formula: ### ### 2 2 2 ### / x \ / y \ / z \ ### ( --- ) + ( --- ) + ( --- ) = 1 ### \ a / \ b / \ c / ### Ensure abc components are positive abc = sp.vequ([abs(semi_axis) for semi_axis in semi_axes[:3]]) ### Get unit vector, n, parallel to input normal n = sp.vhat(inputNormal) ### Direction of normal, N, at [x,y,z] is [ x/(a*a), y/(b*b), z/(c*c)] ### - Vector N is unknown, and is not necessarily a unit vector ### - Argument inputNormal is parallel to N, and of arbitrary length ### - Unit vector parallel to N is n, calculated above from inputNormal ### - Assume length of N is scalar 1/k; k is initially unknown ### - Scaling unit normal, n, by k yields N: ### ### n/k = N = [x/(a*a), y/(b*b), z/(c*c)] Eqn. 1 ### ### so ### ### nx/k = x/(a*a) Eqn. 2x ### ny/k = y/(b*b) Eqn. 2y ### nz/k = z/(c*c) Eqn. 2z ### ### and, solving for surface point components, [x,y,z]: ### ### x = nx*a*a/k Eqn. 3x ### y = ny*b*b/k Eqn. 3y ### z = nz*c*c/k Eqn. 3z ### ### Substituting Eqns. 3x, 3y, and 3z for x, y, and z ### back into the ellipsoid formula (x^2/a^2 + ... = 1): ### ### (nx*a*a/k)^2/(a*a) ### + (ny*b*b/k)^2/(b*b) ### + (nz*c*c/k)^2/(c*c) = 1 Eqn. 4 ### ### and solving for k: ### ### (nx*a)^2 + (ny*b)^2 + (nz*c)^2 = k^2 Eqn. 5 ### ### Since nx, a, ny, b, nz, and c are all known, k ### can be calculated directly using Eqn. 5, and the ### surface point components, x, y, and z, can then ### be calculated using Eqns. 3x, 3y, and 3z. ### Calculate two vectors: ### - [a*a, b*b, c*c] ### - [nx*nx, ny*ny, nz*nz] abcXabc = vXv(abc, abc) nXn = vXv(n, n) ### Solve for k using abcXabc, nXn, and Eqn. 5: k2 = sp.vdot(abcXabc, nXn) k = math.sqrt(k2) ### Use k, abcXabc, n, and Eqns. 3x, 3y, and ### 3z to calculate surface point vector xyz xyz = sp.vscl(1. / k, vXv(abcXabc, n)) ### Debug logging: if doLog: ### Calculate (x/a)^2 + (y/b)^2 + (z/c)^2; it should be = 1 one = sp.vdot(vXv(xyz, xyz), 1 / abcXabc) pprint.pprint( dict(n=n, abc=abc, abcXabc=abcXabc, nXn=nXn, nMag=sp.vnorm(n), k=k, k2=k2, xyz=xyz, one=one)) ### Return surface point return xyz
def getData(eqdata): import spiceypy as spice import numpy as np import constants as con import math #Load the meta kernel file for the determination of the position of the planets. metakernel = '/home/jdw/UM2019Autumn/M561/Project/kernels/M561ProjectKernels.txt' spice.furnsh(metakernel) #Find the number of earthquakes to be analyzed. nquake = len(eqdata) #Create a numpy array with nquake rows and 49 columns. data = np.zeros((nquake, 284), dtype='float64') #Get the ephemeris times from the earthquake dictionary keys. et = list(eqdata.keys()) #Get the earthquake data from the earthquake dictionary values. eqdata = list(eqdata.values()) #Get the positions #First create a vector of planet names to be fed to the spice #function. Planets = [ 'Sun', 'Mercury_Barycenter', 'Venus_Barycenter', 'Moon', 'Mars_Barycenter', 'Jupiter_Barycenter', 'Saturn_Barycenter', 'Uranus_Barycenter', 'Neptune_Barycenter', 'Pluto_Barycenter' ] #Create a time offset index. I want to look not only at the exact earthquake #time but also times that are a given number of seconds before and after #the earthquake. k = [3000, 2400, 1800, 1200, 600, 0, -600] for i in range(nquake): #First fill in the earthquake data. data[i][0] = et[i] data[i][1] = eqdata[i][0] #Earthquake Magnitude data[i][2] = eqdata[i][1] #Earthquake Latitude (degrees) data[i][3] = eqdata[i][2] #Earthquake Longitude (degrees) #Now loop through the planets to get the distances. for p in range(len(Planets)): pindex = p * 28 + 4 #Now loop through the seven times around the earthquake. for t in range(7): #Create a time value. The earthquakes are given in the et #vector and I want to look at times before those earthquake #times. time = et[i] - k[t] #Determine the position of a given planet with respect to Earth #for a given et in the J2000 coordinate system. pos, ltime = spice.spkpos( Planets[p], time, 'J2000', 'NONE', 'Earth', ) #Calculate the distance in meters. Vnorm returns a distance in kilometers #so we have to convert. 1km = 1000.0 meters. dist = spice.vnorm(pos) * 1000.0 #Return the range, longitude and latitude of the position vector #pointing from Earth to the planet. These are output in radians. ran, lon, lat = spice.reclat(pos) #Convert the radians to degrees. longitude = math.degrees(lon) latitude = math.degrees(lat) #Get the gravitational force for the times preceeding and #following the earthquake. F = getGravForce(p, dist) #Fill the data array. data[i][pindex + t * 4] = dist data[i][pindex + t * 4 + 1] = latitude data[i][pindex + t * 4 + 2] = longitude data[i][pindex + t * 4 + 3] = F return data
ceres_df.loc[:, 'UTC_PARSED'] = DATETIME_RANGE.strftime('%Y-%j') # Convert the date-time to Ephemeris Time ceres_df.loc[:, 'ET_TIME'] = ceres_df['UTC_TIME'] \ .apply(lambda x: spiceypy.utc2et(str(x))) #%% # Compute the distance between Ceres and the Sun and convert the resulting # distance value given in km to AU ceres_df.loc[:, 'DIST_SUN_AU'] = \ ceres_df['ET_TIME'].apply(lambda x: \ spiceypy.convrt( \ spiceypy.vnorm( \ spiceypy.spkgps(targ=CERES_ID, \ et=x, \ ref='ECLIPJ2000', \ obs=10)[0]), \ 'km', 'AU')) # # Compute the distance between Ceres and the Earth and convert the resulting # distance value given in km to AU ceres_df.loc[:, 'DIST_EARTH_AU'] = \ ceres_df['ET_TIME'].apply(lambda x: \ spiceypy.convrt( \ spiceypy.vnorm( \ spiceypy.spkgps(targ=CERES_ID, \ et=x, \ ref='ECLIPJ2000', \ obs=399)[0]), \ 'km', 'AU'))
def nh_find_simulated_rings_lorri(): # ============================================================================= # Now go thru the synthetic ring images. # Load and stack the synthetic implanted images. # Load and stack the original 'raw' frames # Difference them, and see if we can find a ring in there. # ============================================================================= dir_porter = '/Users/throop/Dropbox/Data/NH_KEM_Hazard/Porter_Sep17/' dir_synthetic = '/Users/throop/Dropbox/Data/NH_KEM_Hazard/synthetic/' do_subpixel = False # Flag: Do we use sub-pixel shifting when doing the flattening? # It is slower and in theory better, but in reality makes a trivial difference. # Start up SPICE file_kernel = 'kernels_kem.tm' sp.furnsh(file_kernel) # Load the images into a table images_raw = image_stack(dir_porter) images_syn = image_stack(dir_synthetic, do_force=False) stretch = astropy.visualization.PercentileInterval(95) plt.set_cmap('Greys_r') # ============================================================================= # If desired, do a one-time routine for the synthetic images: # extract the I/F and ring size from the filenames, and append that to the table. # This routine should be run after creating new synthetic images (e.g., adding an I/F value) # ============================================================================= DO_APPEND = False if (DO_APPEND): t_syn = images_syn.t num_images_syn = (np.shape(t_syn))[0] iof_ring = np.zeros(num_images_syn, dtype=float) size_ring = np.zeros(num_images_syn, dtype='U30') for i in range(num_images_syn): f = t_syn['filename_short'][i] m = re.search('ring_(.*)_iof(.*)_K', f) # Call regexp to parse it. iof_ring[i] = eval(m.group(2)) size_ring[i] = m.group(1) t_syn['size_ring'] = size_ring t_syn['iof_ring'] = iof_ring images_syn.t = t_syn images_syn.save() # Save the whole pickle archive (including images and table) back to disk data_raw = images_raw.data data_syn = images_syn.data t_raw = images_raw.t t_syn = images_syn.t num_images_raw = (np.shape(t_raw))[0] num_images_syn = (np.shape(t_syn))[0] # Look up the time offset, from the image title. (Would be better to have it stored in table, but this will do.) match = re.search('_K(.*)d', t_syn['filename_short'][0]) dt_ca = ((match.group(1)*u.day).to('s')) # Weird: we don't need .value here. I can't explain it. utc_ca = '2019 1 Jan 05:33' et_ca = sp.utc2et(utc_ca) et_obs = et_ca + dt_ca # Set the pixel scale vec,lt = sp.spkezr('2014 MU69', et_obs, 'J2000', 'LT', 'New Horizons') vec_sc_targ = vec[0:3] dist_target_km = (sp.vnorm(vec_sc_targ)*u.km).value scale_pix_lorri_1x1_rad = 0.3*hbt.d2r / 1024 scale_pix_lorri_4x4_rad = scale_pix_lorri_1x1_rad * 4 scale_pix_km_dict = {'1X1' : scale_pix_lorri_1x1_rad * dist_target_km, '4X4' : scale_pix_lorri_4x4_rad * dist_target_km} # We are # Create a bunch of possible image sets, based on various parameters # Indices for 'raw' images indices_sep17_raw = t_raw['et'] > sp.utc2et('15 sep 2017') # The positon of MU69 has changed a few pixels. # We can't blindly co-add between sep and pre-sep indices_jan17_raw = t_raw['et'] < sp.utc2et('1 sep 2017') indices_rot0_raw = t_raw['angle'] < 180 # One rotation angle indices_rot90_raw = t_raw['angle'] > 180 # The other rotation angle indices_10sec_raw = np.logical_and( t_raw['exptime'] < 10, t_raw['exptime'] > 5 ) indices_20sec_raw = np.logical_and( t_raw['exptime'] < 20, t_raw['exptime'] > 10 ) indices_30sec_raw = np.logical_and( t_raw['exptime'] < 30, t_raw['exptime'] > 20 ) indices_1x1_raw = t_raw['naxis1'] == 1024 indices_4x4_raw = t_raw['naxis1'] == 256 indices_30sec_4x4_raw = np.logical_and(indices_4x4_raw, indices_30sec_raw) # 94 # Indices for synthetic images indices_ring_small_syn = t_syn['size_ring'] == 'small' indices_ring_large_syn = t_syn['size_ring'] == 'large' indices_iof_1em7_syn = t_syn['iof_ring'] == 1e-7 indices_iof_3em7_syn = t_syn['iof_ring'] == 3e-7 indices_iof_1em6_syn = t_syn['iof_ring'] == 1e-6 indices_iof_1em5_syn = t_syn['iof_ring'] == 1e-5 indices_iof_1em4_syn = t_syn['iof_ring'] == 1e-4 indices_small_1em7_syn = np.logical_and(indices_iof_1em7_syn, indices_ring_small_syn) indices_small_3em7_syn = np.logical_and(indices_iof_3em7_syn, indices_ring_small_syn) indices_small_1em6_syn = np.logical_and(indices_iof_1em6_syn, indices_ring_small_syn) indices_small_1em5_syn = np.logical_and(indices_iof_1em5_syn, indices_ring_small_syn) indices_small_1em4_syn = np.logical_and(indices_iof_1em4_syn, indices_ring_small_syn) indices_large_1em7_syn = np.logical_and(indices_iof_1em7_syn, indices_ring_large_syn) indices_large_3em7_syn = np.logical_and(indices_iof_3em7_syn, indices_ring_large_syn) indices_large_1em6_syn = np.logical_and(indices_iof_1em6_syn, indices_ring_large_syn) indices_large_1em5_syn = np.logical_and(indices_iof_1em5_syn, indices_ring_large_syn) indices_large_1em4_syn = np.logical_and(indices_iof_1em4_syn, indices_ring_large_syn) # Choose which indiex. ** THIS IS WHERE WE SET THE RING TO USE!! indices_raw = indices_30sec_4x4_raw.copy() # 94 of 344 indices_syn = indices_small_1em6_syn.copy() # 94 of 752 # Now take the first half of the synthetic indices, and the second half of the raw ones # This is to assure that we are using different images for the two stacks! Otherwise, the results are trivial. frames_max = int(np.sum(indices_raw) / 2) # Total number of frames (94) w = np.where(indices_raw)[0] indices_raw[w[frames_max]:] = False # De-activate all frames *below* frames_max w = np.where(indices_syn)[0] indices_syn[:w[frames_max]] = False # De-activate all frames above frames_max # Set the indices images_raw.set_indices(indices_raw) images_syn.set_indices(indices_syn) # Do the flattening arr_raw = images_raw.flatten(do_subpixel=do_subpixel) arr_syn = images_syn.flatten(do_subpixel=do_subpixel) # arr_raw_sub = images_raw.flatten(do_subpixel=True) # arr_syn_sub = images_syn.flatten(do_subpixel=True) # Extract various fields from the data table. We can look up from any of the images -- they should be all the same. t_syn = images_syn.t # Get the data table iof_ring = t_syn[indices_syn]['iof_ring'][0] size_ring = t_syn[indices_syn]['size_ring'][0] exptime = t_syn[indices_syn]['exptime'][0] # The two flattened images need some offsetting. Do that. shift = ird.translation(arr_raw, arr_syn)['tvec'] # shift = np.round(shift).astype('int') # arr_syn_shift = np.roll(np.roll(arr_syn, int(round(shift[0])), axis=0), int(round(shift[1])), axis=1) arr_syn_shift = scipy.ndimage.shift(arr_syn, shift, order=5) # This allows sub-pixel shifts, apparently. *NO*! # a = arr_syn.copy() # a_05_05 = scipy.ndimage.shift(arr_syn, (0.5, 0.5), order=5) # Ugh. 0.5, 0.5 and 1, 1 are *exactly* the same. # a_1_05 = scipy.ndimage.shift(arr_syn, (1, 0.5), order=5) # a_1_1 = scipy.ndimage.shift(arr_syn, (1, 1), order=5) # a_1_15 = scipy.ndimage.shift(arr_syn, (1, 1.5), order=5) # a_1_0 = scipy.ndimage.shift(arr_syn, (1, 0), order=5) # a_05_0 = scipy.ndimage.shift(arr_syn, (0.5, 0), order=5) arr_diff = arr_syn_shift - arr_raw pos = (images_raw.y_pix_mean*4, images_raw.x_pix_mean*4) # Set the binning width of the radial profiles binning_pix = 5 # Extract the radial profiles (dist_pix_1d, profile_1d_median) = get_radial_profile_circular(arr_diff, pos, method='median', width=binning_pix) (dist_pix_1d, profile_1d_mean) = get_radial_profile_circular(arr_diff, pos, method='mean', width=binning_pix) str_title = ('Synthetic ring - raw, I/F = {:.0e}, {}, {} x {:.1f}s'.format( iof_ring, size_ring, frames_max, exptime)) plt.imshow(stretch(arr_diff)) plt.title(str_title) plt.plot(pos[1], pos[0], marker='.', color='red') plt.show() # Set the scale for the effective mode of these observations. Many are taken as 4x4, but we've rebinned to 1x1 if (np.shape(arr_raw)[0] == 1024): scale_mode = '1X1' else: scale_mode = '4X4' scale_pix_km = scale_pix_km_dict[scale_mode] # Make a plot of the radial profile. Don't plot the innermost bin. It is useless, since it has so few pixels in it. hbt.figsize((12,8)) plt.plot(dist_pix_1d[1:] * scale_pix_km, profile_1d_median[1:], label = 'Annulus median', alpha = 0.7) # plt.plot(dist_pix_1d[1:] * scale_pix_km, profile_1d_mean[1:], label = 'Mean', alpha = 0.2) plt.xlabel('Distance [km]') plt.ylabel('DN per pixel') plt.title(str_title + ', binning = {}'.format(binning_pix)) plt.xlim((0,30000)) # Set the y axis range. This is really stupid. Can't matplotlib figure this out itself? ax = plt.gca() lims = ax.get_xlim() i = np.where( (dist_pix_1d * scale_pix_km > lims[0]) & (dist_pix_1d*scale_pix_km < lims[1]) )[0] ax.set_ylim( profile_1d_median[i].min(), profile_1d_median[i].max() ) plt.legend() plt.show() plt.savefig()
# Add one hour to the date-time stamp and convert it ot ET datetime_stamp = datetime_stamp + datetime.timedelta(hours=1) et_stamp = spiceypy.datetime2et(datetime_stamp) # Compute the state vector of 67P based on the initial orbital elements # (Sun-centric in ECLIPJ2000) COMET_67P_STATE_ORB = spiceypy.conics(COMET_67P_ORB_ELEM, et_stamp) # Compute Jupiter's state vector in as seen from the Sun JUPITER_STATE, _ = spiceypy.spkgeo(targ=5, \ et=et_stamp, \ ref='ECLIPJ2000', \ obs=10) # Compute the distance between Jupiter and 67P comet_jup_dist = spiceypy.vnorm(JUPITER_STATE[:3] - COMET_67P_STATE_ORB[:3]) #%% # If the while condition is not fulfilled, 67P crosses Jupiter's SOI! Let's # take a look when this happened and also let's verify the distance to # Jupiter: print(f'67P entering Jupiter\'s SOI: {datetime_stamp.strftime("%Y-%m-%d")}') print('67P distance to Jupiter at SOI crossing in AU: ' \ f'{spiceypy.convrt(comet_jup_dist, inunit="km", outunit="AU")}') #%% # Transform the state vector of 67P from a Sun-centric system to a Jupiter- # centric system ... COMET_67P_STATE_JUP_CNTR = COMET_67P_STATE_ORB - JUPITER_STATE
def get_illuminated_shape(probe: str, body: str, time: datetime, angular_unit: str) -> Polygon: """ Calculates the shape of sun-illuminated part of SPICE body as viewed from a probe. :param probe: Name of probe, e.g. "JUICE" :param body: Name of body, e.g. "CALLISTO" :param time: Time of observation :param angular_unit: Angular unit, one of ["deg", "rad", "arcMin", "arcSec"] :return: Polygon marking the illuminated part of body as viewed from probe, centered on the nadir point. The x-direction points towards the Sun. """ if angular_unit not in angular_units: raise ValueError( f"Unknown angular_unit: '{angular_unit}'. Allowed units: {angular_units}" ) et = datetime2et(time) ncuts = 20 # we need to compute our own coordinate system, where +z is the probe->body vector, # the Sun lies in the x-z plane, with +x direction towards the Sun # this coordinate system is left-handed (from view of probe, +z points into the screen, # +x points right, and +y points up) sun_position_from_probe = spy.spkpos("SUN", et, f"IAU_{body}", "LT+S", probe)[0] body_position_from_probe = spy.spkpos(body, et, f"IAU_{body}", "LT+S", probe)[0] # we only need to know the orientations os x-z and y-z planes, we don't care about point of origin x_z_plane_normal_vector = spy.vcrss(sun_position_from_probe, body_position_from_probe) y_z_plane_normal_vector = spy.vcrss(body_position_from_probe, x_z_plane_normal_vector) # the illuminated side of limb in our coordinate system is always on the right side, so we start # with +y direction (x_z_plane_normal_vector), and rotate clockwise for 180 degrees step_limb = -np.pi / ncuts limb_points = spy.limbpt("TANGENT/ELLIPSOID", body, et, f"IAU_{body}", "LT+S", "CENTER", probe, x_z_plane_normal_vector, step_limb, ncuts, 1.0, 1.0, ncuts)[3] assert (len(limb_points == ncuts)) # if we preserve the upwards direction of y axis, but look at the body from POV of the Sun, # the probe will always be on the left side. That means we start slicing again at +y direction, # but this time rotate counter-clockwise for 180 degrees, to get terminator points that are # actually visible from probe step_terminator = np.pi / ncuts terminator_points = spy.termpt("UMBRAL/TANGENT/ELLIPSOID", "SUN", body, et, f"IAU_{body}", "LT+S", "CENTER", probe, x_z_plane_normal_vector, step_terminator, ncuts, 1.0, 1.0, ncuts)[3] assert (len(terminator_points <= ncuts)) # reverse the order of terminator points so we go # (limb top -> ... -> limb bottom -> terminator bottom -> ... -> terminator top) # these vectors emanate from probe towards the body limb and terminator points_3d = list(limb_points) + list(reversed(terminator_points)) # project the points from IAU body-fixed 3d frame, to our probe POV 2d frame points_2d = [] for p in points_3d: # x-coordinate of each point is the angle vector with the y-z plane (this also recognizes the sign) x_rad = np.arcsin( spy.vdot(y_z_plane_normal_vector, p) / (spy.vnorm(y_z_plane_normal_vector) * spy.vnorm(p))) # similarly, y-coordinate is angle with x-z plane y_rad = np.arcsin( spy.vdot(x_z_plane_normal_vector, p) / (spy.vnorm(x_z_plane_normal_vector) * spy.vnorm(p))) # convert the angular coordinate from radians to desired unit, and add to list points_2d.append((convertAngleFromTo(x_rad, "rad", angular_unit), convertAngleFromTo(y_rad, "rad", angular_unit))) return Polygon(points_2d)
NEO_1997BQ_ORBITAL_ELEMENTS = [NEO_1997BQ_PERIHELION_KM, \ NEO_1997BQ_ECC, \ NEO_1997BQ_INC_RAD, \ NEO_1997BQ_LNODE_RAD, \ NEO_1997BQ_ARGP_RAD, \ NEO_1997BQ_M0_AT_T0_RAD, \ NEO_1997BQ_T0, \ GM_SUN] # Compute the state vector NEO_1997BQ_STATE_VECTOR = spiceypy.conics(NEO_1997BQ_ORBITAL_ELEMENTS, DATETIME_ET) print(f'Current state vector of 1997BQ in km and km/s ({DATETIME_UTC})):\n' \ f'{NEO_1997BQ_STATE_VECTOR}') print('\n') #%% # Now compute the state vector of the Earth: EARTH_STATE_VECTOR, _ = spiceypy.spkgeo(targ=399, \ et=DATETIME_ET, \ ref='ECLIPJ2000', obs=10) # Compute the current distance of the Earth and the asteroids in LD EARTH_1997BQ_DIST_KM = spiceypy.vnorm(EARTH_STATE_VECTOR[:3] \ - NEO_1997BQ_STATE_VECTOR[:3]) print(f'Current distance between the Earth and 1997BQ ({DATETIME_UTC}):\n' \ f'{EARTH_1997BQ_DIST_KM / ONE_LD} LDe')
def test_ck(kernels, frames_to_convert, fks, stop_ets, output_ck): """ Text CK against FKs at times at which the FKs are valid """ ### Load base kernels (LSK, new FK, SCLK) for kernel in kernels + [output_ck]: sp.furnsh(kernel) ### Set last ET to None so it will be initialized in loop's first pass last_et = None ### Create dict of info; keys will be new FK filenames dt = dict() while fks: ### Pop FK and stop ET off of lists fk = fks.pop() stop_et = stop_ets.pop() ### Create dict for this FK dt[fk] = dict() ### Loop over refernce frames for reffrm in frames_to_convert: ### Get reffrm ID, SCLK ID; N.B. latter comes from new FK reffrm_id = sp.gipool('FRAME_{}'.format(reffrm.upper()), 0, 1)[0] sclk_id = sp.gipool('CK_{}_{}'.format(reffrm_id, 'SCLK'), 0, 1)[0] ### Set start DP-SCLK of window for this old FK (outer loop) ### - Set to zero for first window ### - Covnert ET to DP-SCLK for subsequent windows if last_et is None: et_lo = sp.sct2e(sclk_id, 0.) else: et_lo = last_et ### Load old FK, get RELATIVE frame name, get time-invariant ### matrix, and unload old FK sp.furnsh(fk) relative_reffrm = sp.gcpool( 'TKFRAME_{}_{}'.format(reffrm_id, 'RELATIVE'), 0, 1, 99)[0] sp.unload(fk) ### Get ETs at which to do the tests: ### - 10s after start of window ### - 10s before end of window, or 1e6s after start if last window if stop_et < -1e30: et_test_lo = et_lo + 10. et_test_hi = et_lo + 1e6 else: et_delta = min([10., (stop_et - et_lo) / 3.]) et_test_lo = et_lo + et_delta et_test_hi = stop_et - et_delta ### Save the relative reffrm, the reffrm, the window, and an empty ### dict for this reffrm under this FK dt[fk][reffrm] = (relative_reffrm, et_test_lo, et_test_hi, dict()) ### For next pass last_et = stop_et ### Clear all kernels, and test sp.kclear() assert 0 == sp.ktotal('all') ### Load base kernels including new FK and new CK for kernel in kernels + [output_ck]: sp.furnsh(kernel) ### Loop over old FKs, reffrms, and ETs for fk in dt: for reffrm in dt[fk]: ### Retrieve relative reffrm, ETs and quat dict relative_reffrm, et_test_lo, et_test_hi, dtquat = dt[fk][reffrm] for et in ( et_test_lo, et_test_hi, ): ### Lookup CK-based matrix, convrt to and save quat at each ET dtquat[et] = sp.m2q(sp.pxform(relative_reffrm, reffrm, et)) ### Loop over the old FKs again for fk in dt: ### Clear all kernels, and test sp.kclear() assert 0 == sp.ktotal('all') ### Load only the old FK sp.furnsh(fk) ### Loop over reffrms, and ETs for reffrm in dt[fk]: relative_reffrm, et_test_lo, et_test_hi, dtquat = dt[fk][reffrm] for et in ( et_test_lo, et_test_hi, ): ### Calculate norm of difference of CK-based and FK-based quats quat_error = round( sp.vnorm(dtquat[et] - sp.m2q(sp.pxform(relative_reffrm, reffrm, et))), 16) ### Output that norm as an error for each case, which norm ### should be zero print( dict(fk=fk, quat_error=quat_error, relative_reffrm=relative_reffrm, reffrm=reffrm, et='{:015.4f}'.format(et)))
def __Geometry(self, boresight=''): #if self.geometry_flag is True and \ # self.time.window.all() == self.previous_tw.all(): # return distance = [] altitude = [] boresight_latitude = [] boresight_longitude = [] latitude = [] longitude = [] subpoint_xyz = [] subpoint_pgc = [] subpoint_pcc = [] zaxis_target_angle = [] myaxis_target_angle = [] yaxis_target_angle = [] xaxis_target_angle = [] beta_angle = [] qs, qx, qy, qz = [], [], [] ,[] x, y, z = [],[],[] tar = self.target time = self.time for et in time.window: try: # # Compute the distance # ptarg, lt = spiceypy.spkpos(tar.name, et, tar.frame, time.abcorr, self.name) x.append(ptarg[0]) y.append(ptarg[1]) z.append(ptarg[2]) vout, vmag = spiceypy.unorm(ptarg) distance.append(vmag) # # Compute the geometric sub-observer point. # if tar.frame == 'MARSIAU': tar_frame = 'IAU_MARS' else: tar_frame = tar.frame spoint, trgepc, srfvec = spiceypy.subpnt(tar.method, tar.name, et, tar_frame, time.abcorr, self.name) subpoint_xyz.append(spoint) # # Compute the observer's altitude from SPOINT. # dist = spiceypy.vnorm(srfvec) altitude.append(dist) # # Convert the sub-observer point's rectangular coordinates to # planetographic longitude, latitude and altitude. # spglon, spglat, spgalt = spiceypy.recpgr(tar.name, spoint, tar.radii_equ, tar.flat) # # Convert radians to degrees. # spglon *= spiceypy.dpr() spglat *= spiceypy.dpr() subpoint_pgc.append([spglon, spglat, spgalt]) # # Convert sub-observer point's rectangular coordinates to # planetocentric radius, longitude, and latitude. # spcrad, spclon, spclat = spiceypy.reclat(spoint) # # Convert radians to degrees. # spclon *= spiceypy.dpr() spclat *= spiceypy.dpr() subpoint_pcc.append([spclon, spclat, spcrad]) latitude.append(spclat) #TODO: Remove with list extraction longitude.append(spclon) # TODO: Remove with list extraction # # Compute the geometric sub-boresight point. # if tar.frame == 'MARSIAU': tar_frame = 'IAU_MARS' else: tar_frame = tar.frame if boresight: try: id = spiceypy.bodn2c(boresight) (shape,framen, bsight, n, bounds) = spiceypy.getfov(id, 80) mat = spiceypy.pxform(framen,tar_frame,et) except: framen = boresight bsight = 0,0,1 else: bsight = self.name try: if tar.method == 'INTERCEPT/ELLIPSOID': method = 'ELLIPSOID' else: method = tar.method spoint, trgepc, srfvec = spiceypy.sincpt(method, tar.name, et, tar_frame, time.abcorr, self.name, framen, bsight) # # Convert the sub-observer point's rectangular coordinates to # planetographic longitude, latitude and altitude. # spglon, spglat, spgalt = spiceypy.recpgr(tar.name, spoint, tar.radii_equ, tar.flat) # # Convert radians to degrees. # spglon *= spiceypy.dpr() spglat *= spiceypy.dpr() # # Convert sub-observer point's rectangular coordinates to # planetocentric radius, longitude, and latitude. # spcrad, spclon, spclat = spiceypy.reclat(spoint) # # Convert radians to degrees. # spclon *= spiceypy.dpr() spclat *= spiceypy.dpr() boresight_latitude.append(spclat) boresight_longitude.append(spclon) except: pass # # Compute the angle between the observer's S/C axis and the # geometric sub-observer point # obs_tar, ltime = spiceypy.spkpos(tar.name, et, 'J2000', time.abcorr, self.name) obs_zaxis = [0, 0, 1] obs_myaxis = [0, -1, 0] obs_yaxis = [0, 1, 0] obs_xaxis = [1, 0, 0] # # We need to account for when there is no CK attitude available. # try: matrix = spiceypy.pxform(self.frame, 'J2000', et) z_vecout = spiceypy.mxv(matrix, obs_zaxis) zax_target_angle = spiceypy.vsep(z_vecout, obs_tar) zax_target_angle *= spiceypy.dpr() zaxis_target_angle.append(zax_target_angle) my_vecout = spiceypy.mxv(matrix, obs_myaxis) myax_target_angle = spiceypy.vsep(my_vecout, obs_tar) myax_target_angle *= spiceypy.dpr() myaxis_target_angle.append(myax_target_angle) y_vecout = spiceypy.mxv(matrix, obs_myaxis) yax_target_angle = spiceypy.vsep(y_vecout, obs_tar) yax_target_angle *= spiceypy.dpr() yaxis_target_angle.append(yax_target_angle) x_vecout = spiceypy.mxv(matrix, obs_myaxis) xax_target_angle = spiceypy.vsep(x_vecout, obs_tar) xax_target_angle *= spiceypy.dpr() xaxis_target_angle.append(xax_target_angle) quat = spiceypy.m2q(spiceypy.invert(matrix)) qs.append(quat[0]) qx.append(-1*quat[1]) qy.append(-1*quat[2]) qz.append(-1*quat[3]) except: zaxis_target_angle.append(0.0) myaxis_target_angle.append(0.0) yaxis_target_angle.append(0.0) xaxis_target_angle.append(0.0) qs.append(0.0) qx.append(0.0) qy.append(0.0) qz.append(0.0) beta_angle.append(spiops.beta_angle(self.name, self.target.name, et)) except: boresight_latitude = 0 boresight_longitude = 0 distance = 0 altitude = 0 latitude = 0 longitude = 0 subpoint_xyz = [0,0,0] subpoint_pgc = [0,0,0] subpoint_pcc = [0,0,0] zaxis_target_angle = 0 myaxis_target_angle = 0 yaxis_target_angle = 0 xaxis_target_angle = 0 beta_angle = 0 (qx, qy, qz, qs) = 0, 0, 0, 0 (x, y, z) = 0, 0, 0 self.boresight_latitude = boresight_latitude self.boresight_longitude = boresight_longitude self.distance = distance self.altitude = altitude self.latitude = latitude self.longitude = longitude self.subpoint_xyz = subpoint_xyz self.subpoint_pgc = subpoint_pgc self.subpoint_pcc = subpoint_pcc self.zaxis_target_angle = zaxis_target_angle self.myaxis_target_angle = myaxis_target_angle self.yaxis_target_angle = yaxis_target_angle self.xaxis_target_angle = xaxis_target_angle self.beta_angle = beta_angle self.quaternions = [qx, qy, qz, qs] self.trajectory = [x,y,z] self.geometry_flag = True self.previous_tw = self.time.window return
#============================================================================== n_exp = n_footprints * exp_per_footprint # Total number of exposures index_exp = hbt.frange(0,n_exp-1) index_footprint = np.trunc(index_exp/3).astype('int') # Define 'et' as an array, one per exposure, of the time that exposure is taken et = et_start + (index_exp * exptime) + (index_footprint * dt_slew) dist_kbo = np.zeros(n_exp) for i,et_i in enumerate(et): (st, junk) = sp.spkezr(name_target, et_i, 'J2000', 'LT+S', name_observer) dist_kbo[i] = sp.vnorm(st[0:2]) dist_kbo = dist_kbo * u.km width_fov_km = (dist_kbo * width_fov_rad).to('km').value # Width, in km height_fov_km = (dist_kbo * height_fov_rad).to('km').value # Height, in km #============================================================================== # Make the plot #============================================================================== #%% fig, ax = plt.subplots() hbt.set_fontsize(20) hbt.figsize((10,10)) lim_image_km = (-radius_image.to('km').value, radius_image.to('km').value)
def _get_solar_constant(self): dist = spice.vnorm(self.center_to_sun) # SPICE returns in [km] !! return L_sol / (4 * np.pi * (dist * 1e3) ** 2)
METAKERNEL_NAME = r"em16_ops_win.tm" os.chdir(KERNEL_DIRECTORY) sp.furnsh(KERNEL_DIRECTORY + os.sep + METAKERNEL_NAME) print(sp.tkvrsn("toolkit")) os.chdir(BASE_DIRECTORY) origin = np.asfarray([0.0, 0.0, 0.0]) #vectorOffsetX = 3.0**(-1/2) #vectorOffsetY = 3.0**(-1/2) vectorOffsetX = np.arcsin(1.9992 / sp.dpr()) #2 degrees vectorOffsetY = np.arcsin(15.0 * 88 / 90 / sp.dpr()) #15 vectorOffsetZ = np.sqrt(1.0 - vectorOffsetX**2 - vectorOffsetY**2) vector = np.asfarray([vectorOffsetX, vectorOffsetY, vectorOffsetZ]) vectorMagnitude = sp.vnorm(vector) #should be 1 vector2 = np.asfarray([0.0, 0.0, 1.0]) angleSeparation = sp.vsep(vector / vectorMagnitude, vector2) * sp.dpr() #X to Z #vectorA = np.asfarray([vector[0], 0.0, vector[2]])/sp.vnorm([vector[0], 0.0, vector[2]]) vectorA = np.asfarray([vector[0], 0.0, np.sqrt(1.0 - vector[0]**2)]) angleSeparationA = sp.vsep(vectorA / sp.vnorm(vectorA), vector2) * sp.dpr() #Y to Z #vectorB = np.asfarray([0.0, vector[1], vector[2]])/sp.vnorm([0.0, vector[1], vector[2]]) vectorB = np.asfarray([0.0, vector[1], np.sqrt(1.0 - vector[1]**2)]) angleSeparationB = sp.vsep(vectorB / sp.vnorm(vectorB), vector2) * sp.dpr()
def solar_constant(self): "float : With global value L_s, solar constant at coordinates of body center." dist = spice.vnorm(self.center_to_sun.value) * u.km return (L_sun / (2 * tau * (dist)**2)).to(u.W / u.m / u.m)
def compute_backplanes(file, name_target, frame, name_observer, angle1=0, angle2=0, angle3=0): """ Returns a set of backplanes for a single specified image. The image must have WCS coords available in its header. Backplanes include navigation info for every pixel, including RA, Dec, Eq Lon, Phase, etc. The results are returned to memory, and not written to a file. SPICE kernels must be alreaded loaded, and spiceypy running. Parameters ---- file: String. Input filename, for FITS file. frame: String. Reference frame of the target body. 'IAU_JUPITER', 'IAU_MU69', '2014_MU69_SUNFLOWER_ROT', etc. This is the frame that the Radius_eq and Longitude_eq are computed in. name_target: String. Name of the central body. All geometry is referenced relative to this (e.g., radius, azimuth, etc) name_observer: String. Name of the observer. Must be a SPICE body name (e.g., 'New Horizons') Optional Parameters ---- angle{1,2,3}: **NOT REALLY TESTED. THE BETTER WAY TO CHANGE THE ROTATION IS TO USE A DIFFERENT FRAME.** Rotation angles which are applied when defining the plane in space that the backplane will be generated for. These are applied in the order 1, 2, 3. Angles are in radians. Nominal values are 0. This allows the simulation of (e.g.) a ring system inclined relative to the nominal body equatorial plane. For MU69 sunflower rings, the following descriptions are roughly accurate, becuase the +Y axis points sunward, which is *almost* toward the observer. But it is better to experiment and find the appropriate angle that way, than rely on this ad hoc description. These are close for starting with. - `angle1` = Tilt front-back, from face-on. Or rotation angle, if tilted right-left. - `angle2` = Rotation angle, if tilted front-back. - `angle3` = Tilt right-left, from face-on. do_fast: Boolean. If set, generate only an abbreviated set of backplanes. **NOT CURRENTLY IMPLEMENTED.** Output ---- Output is a tuple, consisting of each of the backplanes, and a text description for each one. The size of each of these arrays is the same as the input image. The position of each of these is the plane defined by the target body, and the normal vector to the observer. output = (backplanes, descs) backplanes = (ra, dec, radius_eq, longitude_eq, phase) descs = (desc_ra, desc_dec, desc_radius_eq, desc_longitude_eq, desc_phase) Radius_eq: Radius, in the ring's equatorial plane, in km Longitude_eq: Longitude, in the equatorial plane, in radians (0 .. 2pi) RA: RA of pixel, in radians Dec: Dec of pixel, in dRA: Projected offset in RA direction between center of body (or barycenter) and pixel, in km. dDec: Projected offset in Dec direction between center of body (or barycenter) and pixel, in km. Z: Vertical value of the ring system. With special options selected (TBD), then additional backplanes will be generated -- e.g., a set of planes for each of the Jovian satellites in the image, or sunflower orbit, etc. No new FITS file is written. The only output is the returned tuple. """ if not(frame): raise ValueError('frame undefined') if not(name_target): raise ValueError('name_target undefined') if not(name_observer): raise ValueError('name_observer undefined') name_body = name_target # Sometimes we use one, sometimes the other. Both are identical fov_lorri = 0.3 * hbt.d2r abcorr = 'LT' do_satellites = False # Flag: Do we create an additional backplane for each of Jupiter's small sats? # Open the FITS file w = WCS(file) # Warning: I have gotten a segfault here before if passing a FITS file with no WCS info. hdulist = fits.open(file) et = float(hdulist[0].header['SPCSCET']) # ET of mid-exposure, on s/c n_dx = int(hdulist[0].header['NAXIS1']) # Pixel dimensions of image. Both LORRI and MVIC have this. n_dy = int(hdulist[0].header['NAXIS2']) hdulist.close() # Setup the output arrays lon_arr = np.zeros((n_dy, n_dx)) # Longitude of pixel (defined with recpgr) lat_arr = np.zeros((n_dy, n_dx)) # Latitude of pixel (which is zero, so meaningless) radius_arr = np.zeros((n_dy, n_dx)) # Radius, in km altitude_arr= np.zeros((n_dy, n_dx)) # Altitude above midplane, in km ra_arr = np.zeros((n_dy, n_dx)) # RA of pixel dec_arr = np.zeros((n_dy, n_dx)) # Dec of pixel dra_arr = np.zeros((n_dy, n_dx)) # dRA of pixel: Distance in sky plane between pixel and body, in km. ddec_arr = np.zeros((n_dy, n_dx)) # dDec of pixel: Distance in sky plane between pixel and body, in km. phase_arr = np.zeros((n_dy, n_dx)) # Phase angle x_arr = np.zeros((n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords y_arr = np.zeros((n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords z_arr = np.zeros((n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords # ============================================================================= # Do the backplane, in the general case. # This is a long routine, because we deal with all the satellites, the J-ring, etc. # ============================================================================= if (True): # Look up body parameters, used for PGRREC(). (num, radii) = sp.bodvrd(name_target, 'RADII', 3) r_e = radii[0] r_p = radii[2] flat = (r_e - r_p) / r_e # Define a SPICE 'plane' along the plane of the ring. # Do this in coordinate frame of the body (IAU_JUPITER, 2014_MU69_SUNFLOWER_ROT, etc). # ============================================================================= # Set up the Jupiter system specifics # ============================================================================= if (name_target.upper() == 'JUPITER'): plane_target_eq = sp.nvp2pl([0,0,1], [0,0,0]) # nvp2pl: Normal Vec + Point to Plane. Jupiter north pole? # For Jupiter only, define a few more output arrays for the final backplane set ang_metis_arr = np.zeros((n_dy, n_dx)) # Angle from pixel to body, in radians ang_adrastea_arr = ang_metis_arr.copy() ang_thebe_arr = ang_metis_arr.copy() ang_amalthea_arr = ang_metis_arr.copy() # ============================================================================= # Set up the MU69 specifics # ============================================================================= if ('MU69' in name_target.upper()): # Define a plane, which is the plane of sunflower rings (ie, X-Z plane in Sunflower frame) # If additional angles are passed, then create an Euler matrix which will do additional angles of rotation. # This is defined in the 'MU69_SUNFLOWER' frame vec_plane = [0, 1, 0] # Use +Y (anti-sun dir), which is normal to XZ. # vec_plane = [0, 0, 1] # Use +Y (anti-sun dir), which is normal to XZ. plane_target_eq = sp.nvp2pl(vec_plane, [0,0,0]) # "Normal Vec + Point to Plane". 0,0,0 = origin. # XXX NB: This plane in in body coords, not J2K coords. This is what we want, because the # target intercept calculation is also done in body coords. # ============================================================================= # Set up the various output planes and arrays necessary for computation # ============================================================================= # Get xformation matrix from J2K to target system coords. I can use this for points *or* vectors. mx_j2k_frame = sp.pxform('J2000', frame, et) # from, to, et # Get vec from body to s/c, in both body frame, and J2K. # NB: The suffix _j2k indicates j2K frame. _frame indicates the frame of target (IAU_JUP, MU69_SUNFLOWER, etc) (st_target_sc_frame, lt) = sp.spkezr(name_observer, et, frame, abcorr, name_target) (st_sc_target_frame, lt) = sp.spkezr(name_target, et, frame, abcorr, name_observer) (st_target_sc_j2k, lt) = sp.spkezr(name_observer, et, 'J2000', abcorr, name_target) (st_sc_target_j2k, lt) = sp.spkezr(name_target, et, 'J2000', abcorr, name_observer) vec_target_sc_frame = st_target_sc_frame[0:3] vec_sc_target_frame = st_sc_target_frame[0:3] vec_target_sc_j2k = st_target_sc_j2k[0:3] vec_sc_target_j2k = st_sc_target_j2k[0:3] dist_target_sc = sp.vnorm(vec_target_sc_j2k) # Get target distance, in km # vec_sc_target_frame = -vec_target_sc_frame # ACTUALLY THIS IS NOT TRUE!! ONLY TRUE IF ABCORR=NONE. # Name this vector a 'point'. INRYPL requires a point argument. pt_target_sc_frame = vec_target_sc_frame # Look up RA and Dec of target (from sc), in J2K (_, ra_sc_target, dec_sc_target) = sp.recrad(vec_sc_target_j2k) # Get vector from target to sun. We use this later for phase angle. (st_target_sun_frame, lt) = sp.spkezr('Sun', et, frame, abcorr, name_target) # From body to Sun, in body frame vec_target_sun_frame = st_target_sun_frame[0:3] # Create a 2D array of RA and Dec points # These are made by WCS, so they are guaranteed to be right. xs = range(n_dx) ys = range(n_dy) (i_x_2d, i_y_2d) = np.meshgrid(xs, ys) (ra_arr, dec_arr) = w.wcs_pix2world(i_x_2d, i_y_2d, False) # Returns in degrees ra_arr *= hbt.d2r # Convert to radians dec_arr *= hbt.d2r # Compute the projected distance from MU69, in the sky plane, in km, for each pixel. # dist_target_sc is the distance to MU69, and we use this to convert from radians, to km. # 16-Oct-2018. I had been computing this erroneously. It should be *cosdec, not /cosdec. dra_arr = (ra_arr - ra_sc_target) * dist_target_sc * np.cos(dec_arr) ddec_arr = (dec_arr - dec_sc_target) * dist_target_sc # Convert to km # ============================================================================= # Compute position for additional Jupiter bodies, as needed # ============================================================================= if (name_target.upper() == 'JUPITER'): vec_metis_j2k,lt = sp.spkezr('Metis', et, 'J2000', abcorr, 'New Horizons') vec_adrastea_j2k,lt = sp.spkezr('Adrastea', et, 'J2000', abcorr, 'New Horizons') vec_thebe_j2k,lt = sp.spkezr('Thebe', et, 'J2000', abcorr, 'New Horizons') vec_amalthea_j2k,lt = sp.spkezr('Amalthea', et, 'J2000', abcorr, 'New Horizons') vec_metis_j2k = np.array(vec_metis_j2k[0:3]) vec_thebe_j2k = np.array(vec_thebe_j2k[0:3]) vec_adrastea_j2k = np.array(vec_adrastea_j2k[0:3]) vec_amalthea_j2k = np.array(vec_amalthea_j2k[0:3]) # ============================================================================= # Loop over pixels in the output image # ============================================================================= for i_x in xs: for i_y in ys: # Look up the vector direction of this single pixel, which is defined by an RA and Dec # Vector is thru pixel to ring, in J2K. # RA and Dec grids are made by WCS, so they are guaranteed to be right. vec_pix_j2k = sp.radrec(1., ra_arr[i_y, i_x], dec_arr[i_y, i_x]) # Convert vector along the pixel direction, from J2K into the target body frame vec_pix_frame = sp.mxv(mx_j2k_frame, vec_pix_j2k) # And calculate the intercept point between this vector, and the ring plane. # All these are in body coordinates. # plane_target_eq is defined as the body's equatorial plane (its XZ for MU69). # ** Some question as to whether we should shoot this vector at the ring plane, or the sky plane. # Ring plane is normally the one we want. But, for the case of edge-on rings, the eq's break down. # So, we should use the sky plane instead. Testing shows that for the case of MU69 Eq's break down # for edge-on rings... there is always an ambiguity. # ** For testing, try intersecting the sky plane instead of the ring plane. # ** Confirmed: Using sky plane gives identical results in case of face-on rings. # And it gives meaningful results in case of edge-on rings, where ring plane did not. # However, for normal rings (e.g., Jupiter), we should continue using the ring plane, not sky plane. do_sky_plane = True # For ORT4, where we want to use euler angles, need to set this to False if do_sky_plane and ('MU69' in name_target): plane_sky_frame = sp.nvp2pl(vec_sc_target_frame, [0,0,0]) # Frame normal to s/c vec, cntrd on MU69 (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_sky_frame) # pt_intersect_frame is the point where the ray hits the skyplane, in the coordinate frame # of the target body. else: # Calc intersect into equator of target plane (ie, ring plane) (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_target_eq) # pt, vec, plane # Swap axes in target frame if needed. # In the case of MU69 (both sunflower and tunacan), the frame is defined s.t. the ring # is in the XZ plane, not XY. This is strange (but correct). # I bet MU69 is the only ring like this. Swap it so that Z means 'vertical, out of plane' -- # that is, put it into normal XYZ rectangular coords, so we can use RECLAT etc on it. if ('MU69' in name_target): # Was 0 2 1. But this makes tunacan radius look in wrong dir. # 201 looks same # 210 similar # 201 similar # 102, 120 similar. # ** None of these change orientation of 'radius' backplane. OK. pt_intersect_frame = np.array([pt_intersect_frame[0], pt_intersect_frame[2], pt_intersect_frame[1]]) # Get the radius and azimuth of the intersect, in the ring plane # Q: Why for the TUNACAN is the radius zero here along horizontal (see plot)? # A: Ahh, it is not zero. It is just that the 'projected radius' of a ring that is nearly edge-on # can be huge! Basically, if we try to calc the intersection with that plane, it will give screwy # answers, because the plane is so close to edge-on that intersection could be a long way # from body itself. # Instead, I really want to take the tangent sky plane, intersect that, and then calc the # position of that (in xyz, radius, longitude, etc). # Since that plane is fixed, I don't see a disadvantage to doing that. # We want the 'radius' to be the radius in the equatorial plane -- that is, sqrt(x^2 + y^2). # We don't want it to be the 'SPICE radius', which is the distance. # (For MU69 equatorial plane is nominally XZ, but we have already changed that above to XY.) _radius_3d, lon, lat = sp.reclat(pt_intersect_frame) radius_eq = sp.vnorm([pt_intersect_frame[0], pt_intersect_frame[1], 0]) # radius_eq = sp.vnorm([pt_intersect_frame[0], pt_intersect_frame[1], pt_intersect_frame[2]]) # Get the vertical position (altitude) altitude = pt_intersect_frame[2] # Calculate the phase angle: angle between s/c-to-ring, and ring-to-sun vec_ring_sun_frame = -pt_intersect_frame + vec_target_sun_frame angle_phase = sp.vsep(-vec_pix_frame, vec_ring_sun_frame) # Save various derived quantities radius_arr[i_y, i_x] = radius_eq lon_arr[i_y, i_x] = lon phase_arr[i_y, i_x] = angle_phase altitude_arr[i_y, i_x] = altitude # Save these just for debugging x_arr[i_y, i_x] = pt_intersect_frame[0] y_arr[i_y, i_x] = pt_intersect_frame[1] z_arr[i_y, i_x] = pt_intersect_frame[2] # Now calc angular separation between this pixel, and the satellites in our list # Since these are huge arrays, cast into floats to make sure they are not doubles. if (name_body.upper() == 'JUPITER'): ang_thebe_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_thebe_j2k) ang_adrastea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_adrastea_j2k) ang_metis_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_metis_j2k) ang_amalthea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_amalthea_j2k) # Now, fix a bug. The issue is that SP.INRYPL uses the actual location of the bodies (no aberration), # while their position is calculated (as it should be) with abcorr=LT. This causes a small error in the # positions based on the INRYPL calculation. This should probably be fixed above, but it was not # obvious how. So, instead, I am fixing it here, by doing a small manual offset. # Calculate the shift required, by measuring the position of MU69 with abcorr=NONE, and comparing it to # the existing calculation, that uses abcorr=LT. This is brute force, but it works. For MU69 approach, # it is 0.75 LORRI 4x4 pixels (ie, 3 1X1 pixels). This is bafflingly huge (I mean, we are headed # straight toward MU69, and it takes a month to move a pixel, and RTLT is only a few minutes). But I have # confirmed the math and the magnitude, and it works. (st_sc_target_j2k_nolt, _) = sp.spkezr(name_target, et, 'J2000', 'NONE', name_observer) vec_sc_target_j2k_nolt = st_sc_target_j2k_nolt[0:3] (_, ra_sc_target_nolt, dec_sc_target_nolt) = sp.recrad(vec_sc_target_j2k_nolt) (x0,y0) = w.wcs_world2pix(ra_sc_target_nolt*hbt.r2d, dec_sc_target_nolt*hbt.r2d, 1) (x1,y1) = w.wcs_world2pix(ra_sc_target *hbt.r2d, dec_sc_target *hbt.r2d, 1) dx = x1-x0 dy = y1-y0 print(f'Compute backplanes: INRYPL pixel shift = {dx}, {dy}') dx_int = int(round(dx)) dy_int = int(round(dy)) do_roll = True if do_roll: print(f'compute_backplanes: Rolling by {dx_int}, {dy_int} due to INRYPL') # Now shift all of the planes that need fixing. The dRA_km and dDec_km are calculated before INRYPL() # is applied, so they do not need to be shifted. I have validated that by plotting them. # # XXX NP.ROLL() is really not ideal. I should use a function that introduces NaN at the edge, not roll it. radius_arr = np.roll(np.roll(radius_arr, dy_int, axis=0), dx_int, axis=1) lon_arr = np.roll(np.roll(lon_arr, dy_int, axis=0), dx_int, axis=1) phase_arr = np.roll(np.roll(phase_arr, dy_int, axis=0), dx_int, axis=1) altitude_arr = np.roll(np.roll(altitude_arr, dy_int, axis=0), dx_int, axis=1) else: print(f'compute_backplanes: Skipping roll due to INRYPL, based on do_roll={do_roll}') # Assemble the results into a backplane backplane = { 'RA' : ra_arr.astype(float), # return radians 'Dec' : dec_arr.astype(float), # return radians 'dRA_km' : dra_arr.astype(float), 'dDec_km' : ddec_arr.astype(float), 'Radius_eq' : radius_arr.astype(float), 'Longitude_eq' : lon_arr.astype(float), 'Phase' : phase_arr.astype(float), 'Altitude_eq' : altitude_arr.astype(float), # 'x' : x_arr.astype(float), # 'y' : y_arr.astype(float), # 'z' : z_arr.astype(float), # } # Assemble a bunch of descriptors, to be put into the FITS headers desc = { 'RA of pixel, radians', 'Dec of pixel, radians', 'Offset from target in target plane, RA direction, km', 'Offset from target in target plane, Dec direction, km', 'Projected equatorial radius, km', 'Projected equatorial longitude, km', 'Sun-target-observer phase angle, radians', 'Altitude above midplane, km', # 'X position of sky plane intercept', # 'Y position of sky plane intercept', # 'Z position of sky plane intercept' } # In the case of Jupiter, add a few extra fields if (name_body.upper() == 'JUPITER'): backplane['Ang_Thebe'] = ang_thebe_arr.astype(float) # Angle to Thebe, in radians backplane['Ang_Metis'] = ang_metis_arr.astype(float) backplane['Ang_Amalthea'] = ang_amalthea_arr.astype(float) backplane['Ang_Adrastea'] = ang_adrastea_arr.astype(float) # If distance to any of the small sats is < 0.3 deg, then delete that entry in the dictionary if (np.amin(ang_thebe_arr) > fov_lorri): del backplane['Ang_Thebe'] else: print("Keeping Thebe".format(np.min(ang_thebe_arr) * hbt.r2d)) if (np.amin(ang_metis_arr) > fov_lorri): del backplane['Ang_Metis'] else: print("Keeping Metis, min = {} deg".format(np.min(ang_metis_arr) * hbt.r2d)) if (np.amin(ang_amalthea_arr) > fov_lorri): del backplane['Ang_Amalthea'] else: print("Keeping Amalthea, min = {} deg".format(np.amin(ang_amalthea_arr) * hbt.r2d)) if (np.amin(ang_adrastea_arr) > fov_lorri): del backplane['Ang_Adrastea'] else: print("Keeping Adrastea".format(np.min(ang_adrastea_arr) * hbt.r2d)) # And return the backplane set return (backplane, desc)
def get_vector(date_time, reference_frame): # print("SUN", date_time, reference_frame, SPICE_ABERRATION_CORRECTION, SPICE_OBSERVER) obs2SunVector = sp.spkpos("SUN", date_time, reference_frame, SPICE_ABERRATION_CORRECTION, SPICE_OBSERVER)[0] obs2SunUnitVector = obs2SunVector / sp.vnorm(obs2SunVector) return -1 * obs2SunUnitVector #-1 is there to switch the directions to be like in cosmographia
def geometry(et, bsight, target, frame, sensor, observer=''): if not observer: observer = sensor # Time tag [UTC] # pixel id [(x,y)] # corner id [(x,y)] # Requested geometry # lat lon intersection (planetocentric) # lat lon subspacecraft # lat lon subsolar # target distance intersection # target angular diameter # local solar time intersection # phase angle intersection # emission angle intersection # incidence angle intersection # # We retrieve the camera information using GETFOV. More info available: # # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/getfov_c.html # sensor_id = spiceypy.bodn2c(sensor) (shape, sensor_frame, ibsight, vectors, bounds) = spiceypy.getfov(sensor_id, 100) visible = spiceypy.fovtrg(sensor, target, 'ELLIPSOID', frame, 'LT+S', observer, et) if not visible: return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 tarid = spiceypy.bodn2c(target) n, radii = spiceypy.bodvrd(target, 'RADII', 3) re = radii[0] rp = radii[2] f = (re - rp) / re try: # # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/sincpt_c.html # # For each pixel we compute the possible intersection with the target, if # the target is intersected we then compute the illumination angles. We # use the following SPICE APIs: SINCPT and ILLUMF # # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/sincpt_c.html # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/illumf_c.html # (spoint, trgepc, srfvec) = \ spiceypy.sincpt('ELLIPSOID', target, et, frame, 'LT+S', observer, sensor_frame, bsight) (tarlon, tarlat, taralt) = spiceypy.recgeo(spoint, re, f) tardis = spiceypy.vnorm(srfvec) # # Angular diameter # tarang = np.degrees( 2 * np.arctan(max(radii) / spiceypy.vnorm(spoint + srfvec))) # # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/illumf_c.html # (trgenpc, srfvec, phase, incdnc, emissn, visiblef, iluminatedf) = \ spiceypy.illumf('ELLIPSOID', target, 'SUN', et, frame, 'LT+S', observer, spoint) phase *= spiceypy.dpr() incdnc *= spiceypy.dpr() emissn *= spiceypy.dpr() # # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/et2lst_c.html # # VARIABLE I/O DESCRIPTION # -------- --- -------------------------------------------------- # et I Epoch in seconds past J2000 epoch. # body I ID-code of the body of interest. # lon I Longitude of surface point (RADIANS). # type I Type of longitude "PLANETOCENTRIC", etc. # timlen I Available room in output time string. # ampmlen I Available room in output `ampm' string. # hr O Local hour on a "24 hour" clock. # mn O Minutes past the hour. # sc O Seconds past the minute. # time O String giving local time on 24 hour clock. # ampm O String giving time on A.M./ P.M. scale. (hr, mn, sc, ltime, ampm) = \ spiceypy.et2lst(et, tarid, tarlon, 'PLANETOCENTRIC', 80, 80) # # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/subpnt_c.html # # Variable I/O Description # -------- --- -------------------------------------------------- # method I Computation method. # target I Name of target body. # et I Epoch in TDB seconds past J2000 TDB. # fixref I Body-fixed, body-centered target body frame. # abcorr I Aberration correction flag. # obsrvr I Name of observing body. # spoint O Sub-observer point on the target body. # trgepc O Sub-observer point epoch. # srfvec O Vector from observer to sub-observer point # (spoint, trgepc, srfev) = \ spiceypy.subpnt('INTERCEPT/ELLIPSOID', target, et, frame, 'LT+S', observer) (sublon, sublat, subalt) = spiceypy.recgeo(spoint, re, f) # # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/subslr_c.html # # Variable I/O Description # -------- --- -------------------------------------------------- # method I Computation method. # target I Name of target body. # et I Epoch in ephemeris seconds past J2000 TDB. # fixref I Body-fixed, body-centered target body frame. # abcorr I Aberration correction. # obsrvr I Name of observing body. # spoint O Sub-solar point on the target body. # trgepc O Sub-solar point epoch. # srfvec O Vector from observer to sub-solar point. # (spoint, trgepc, srfev) = \ spiceypy.subslr('INTERCEPT/ELLIPSOID', target, et, frame, 'LT+S', observer) (sunlon, sunlat, sunalt) = spiceypy.recgeo(spoint, re, f) return tarlon, tarlat, sublon, sublat, sunlon, sunlat, tardis, tarang, ltime, phase, emissn, incdnc except: return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
def compute_backplanes(file, name_target, frame, name_observer, angle1=0, angle2=0, angle3=0): """ Returns a set of backplanes for a single specified image. The image must have WCS coords available in its header. Backplanes include navigation info for every pixel, including RA, Dec, Eq Lon, Phase, etc. The results are returned to memory, and not written to a file. SPICE kernels must be alreaded loaded, and spiceypy running. Parameters ---- file: String. Input filename, for FITS file. frame: String. Reference frame of the target body. 'IAU_JUPITER', 'IAU_MU69', '2014_MU69_SUNFLOWER_ROT', etc. This is the frame that the Radius_eq and Longitude_eq are computed in. name_target: String. Name of the central body. All geometry is referenced relative to this (e.g., radius, azimuth, etc) name_observer: String. Name of the observer. Must be a SPICE body name (e.g., 'New Horizons') Optional Parameters ---- angle{1,2,3}: **NOT REALLY TESTED. THE BETTER WAY TO CHANGE THE ROTATION IS TO USE A DIFFERENT FRAME.** Rotation angles which are applied when defining the plane in space that the backplane will be generated for. These are applied in the order 1, 2, 3. Angles are in radians. Nominal values are 0. This allows the simulation of (e.g.) a ring system inclined relative to the nominal body equatorial plane. For MU69 sunflower rings, the following descriptions are roughly accurate, becuase the +Y axis points sunward, which is *almost* toward the observer. But it is better to experiment and find the appropriate angle that way, than rely on this ad hoc description. These are close for starting with. - `angle1` = Tilt front-back, from face-on. Or rotation angle, if tilted right-left. - `angle2` = Rotation angle, if tilted front-back. - `angle3` = Tilt right-left, from face-on. do_fast: Boolean. If set, generate only an abbreviated set of backplanes. **NOT CURRENTLY IMPLEMENTED.** Output ---- Output is a tuple, consisting of each of the backplanes, and a text description for each one. The size of each of these arrays is the same as the input image. The position of each of these is the plane defined by the target body, and the normal vector to the observer. output = (backplanes, descs) backplanes = (ra, dec, radius_eq, longitude_eq, phase) descs = (desc_ra, desc_dec, desc_radius_eq, desc_longitude_eq, desc_phase) Radius_eq: Radius, in the ring's equatorial plane, in km Longitude_eq: Longitude, in the equatorial plane, in radians (0 .. 2pi) RA: RA of pixel, in radians Dec: Dec of pixel, in dRA: Projected offset in RA direction between center of body (or barycenter) and pixel, in km. dDec: Projected offset in Dec direction between center of body (or barycenter) and pixel, in km. Z: Vertical value of the ring system. With special options selected (TBD), then additional backplanes will be generated -- e.g., a set of planes for each of the Jovian satellites in the image, or sunflower orbit, etc. No new FITS file is written. The only output is the returned tuple. """ if not (frame): raise ValueError('frame undefined') if not (name_target): raise ValueError('name_target undefined') if not (name_observer): raise ValueError('name_observer undefined') name_body = name_target # Sometimes we use one, sometimes the other. Both are identical fov_lorri = 0.3 * hbt.d2r abcorr = 'LT' do_satellites = False # Flag: Do we create an additional backplane for each of Jupiter's small sats? # Open the FITS file w = WCS( file ) # Warning: I have gotten a segfault here before if passing a FITS file with no WCS info. hdulist = fits.open(file) et = float(hdulist[0].header['SPCSCET']) # ET of mid-exposure, on s/c n_dx = int(hdulist[0].header['NAXIS1'] ) # Pixel dimensions of image. Both LORRI and MVIC have this. n_dy = int(hdulist[0].header['NAXIS2']) hdulist.close() # Setup the output arrays lon_arr = np.zeros( (n_dy, n_dx)) # Longitude of pixel (defined with recpgr) lat_arr = np.zeros( (n_dy, n_dx)) # Latitude of pixel (which is zero, so meaningless) radius_arr = np.zeros((n_dy, n_dx)) # Radius, in km altitude_arr = np.zeros((n_dy, n_dx)) # Altitude above midplane, in km ra_arr = np.zeros((n_dy, n_dx)) # RA of pixel dec_arr = np.zeros((n_dy, n_dx)) # Dec of pixel dra_arr = np.zeros( (n_dy, n_dx) ) # dRA of pixel: Distance in sky plane between pixel and body, in km. ddec_arr = np.zeros( (n_dy, n_dx) ) # dDec of pixel: Distance in sky plane between pixel and body, in km. phase_arr = np.zeros((n_dy, n_dx)) # Phase angle x_arr = np.zeros( (n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords y_arr = np.zeros( (n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords z_arr = np.zeros( (n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords # ============================================================================= # Do the backplane, in the general case. # This is a long routine, because we deal with all the satellites, the J-ring, etc. # ============================================================================= if (True): # Look up body parameters, used for PGRREC(). (num, radii) = sp.bodvrd(name_target, 'RADII', 3) r_e = radii[0] r_p = radii[2] flat = (r_e - r_p) / r_e # Define a SPICE 'plane' along the plane of the ring. # Do this in coordinate frame of the body (IAU_JUPITER, 2014_MU69_SUNFLOWER_ROT, etc). # ============================================================================= # Set up the Jupiter system specifics # ============================================================================= if (name_target.upper() == 'JUPITER'): plane_target_eq = sp.nvp2pl( [0, 0, 1], [0, 0, 0 ]) # nvp2pl: Normal Vec + Point to Plane. Jupiter north pole? # For Jupiter only, define a few more output arrays for the final backplane set ang_metis_arr = np.zeros( (n_dy, n_dx)) # Angle from pixel to body, in radians ang_adrastea_arr = ang_metis_arr.copy() ang_thebe_arr = ang_metis_arr.copy() ang_amalthea_arr = ang_metis_arr.copy() # ============================================================================= # Set up the MU69 specifics # ============================================================================= if ('MU69' in name_target.upper()): # Define a plane, which is the plane of sunflower rings (ie, X-Z plane in Sunflower frame) # If additional angles are passed, then create an Euler matrix which will do additional angles of rotation. # This is defined in the 'MU69_SUNFLOWER' frame vec_plane = [0, 1, 0] # Use +Y (anti-sun dir), which is normal to XZ. # vec_plane = [0, 0, 1] # Use +Y (anti-sun dir), which is normal to XZ. plane_target_eq = sp.nvp2pl( vec_plane, [0, 0, 0]) # "Normal Vec + Point to Plane". 0,0,0 = origin. # XXX NB: This plane in in body coords, not J2K coords. This is what we want, because the # target intercept calculation is also done in body coords. # ============================================================================= # Set up the various output planes and arrays necessary for computation # ============================================================================= # Get xformation matrix from J2K to target system coords. I can use this for points *or* vectors. mx_j2k_frame = sp.pxform('J2000', frame, et) # from, to, et # Get vec from body to s/c, in both body frame, and J2K. # NB: The suffix _j2k indicates j2K frame. _frame indicates the frame of target (IAU_JUP, MU69_SUNFLOWER, etc) (st_target_sc_frame, lt) = sp.spkezr(name_observer, et, frame, abcorr, name_target) (st_sc_target_frame, lt) = sp.spkezr(name_target, et, frame, abcorr, name_observer) (st_target_sc_j2k, lt) = sp.spkezr(name_observer, et, 'J2000', abcorr, name_target) (st_sc_target_j2k, lt) = sp.spkezr(name_target, et, 'J2000', abcorr, name_observer) vec_target_sc_frame = st_target_sc_frame[0:3] vec_sc_target_frame = st_sc_target_frame[0:3] vec_target_sc_j2k = st_target_sc_j2k[0:3] vec_sc_target_j2k = st_sc_target_j2k[0:3] dist_target_sc = sp.vnorm( vec_target_sc_j2k) # Get target distance, in km # vec_sc_target_frame = -vec_target_sc_frame # ACTUALLY THIS IS NOT TRUE!! ONLY TRUE IF ABCORR=NONE. # Name this vector a 'point'. INRYPL requires a point argument. pt_target_sc_frame = vec_target_sc_frame # Look up RA and Dec of target (from sc), in J2K (_, ra_sc_target, dec_sc_target) = sp.recrad(vec_sc_target_j2k) # Get vector from target to sun. We use this later for phase angle. (st_target_sun_frame, lt) = sp.spkezr('Sun', et, frame, abcorr, name_target) # From body to Sun, in body frame vec_target_sun_frame = st_target_sun_frame[0:3] # Create a 2D array of RA and Dec points # These are made by WCS, so they are guaranteed to be right. xs = range(n_dx) ys = range(n_dy) (i_x_2d, i_y_2d) = np.meshgrid(xs, ys) (ra_arr, dec_arr) = w.wcs_pix2world(i_x_2d, i_y_2d, False) # Returns in degrees ra_arr *= hbt.d2r # Convert to radians dec_arr *= hbt.d2r # Compute the projected distance from MU69, in the sky plane, in km, for each pixel. # dist_target_sc is the distance to MU69, and we use this to convert from radians, to km. # 16-Oct-2018. I had been computing this erroneously. It should be *cosdec, not /cosdec. dra_arr = (ra_arr - ra_sc_target) * dist_target_sc * np.cos(dec_arr) ddec_arr = (dec_arr - dec_sc_target) * dist_target_sc # Convert to km # ============================================================================= # Compute position for additional Jupiter bodies, as needed # ============================================================================= if (name_target.upper() == 'JUPITER'): vec_metis_j2k, lt = sp.spkezr('Metis', et, 'J2000', abcorr, 'New Horizons') vec_adrastea_j2k, lt = sp.spkezr('Adrastea', et, 'J2000', abcorr, 'New Horizons') vec_thebe_j2k, lt = sp.spkezr('Thebe', et, 'J2000', abcorr, 'New Horizons') vec_amalthea_j2k, lt = sp.spkezr('Amalthea', et, 'J2000', abcorr, 'New Horizons') vec_metis_j2k = np.array(vec_metis_j2k[0:3]) vec_thebe_j2k = np.array(vec_thebe_j2k[0:3]) vec_adrastea_j2k = np.array(vec_adrastea_j2k[0:3]) vec_amalthea_j2k = np.array(vec_amalthea_j2k[0:3]) # ============================================================================= # Loop over pixels in the output image # ============================================================================= for i_x in xs: for i_y in ys: # Look up the vector direction of this single pixel, which is defined by an RA and Dec # Vector is thru pixel to ring, in J2K. # RA and Dec grids are made by WCS, so they are guaranteed to be right. vec_pix_j2k = sp.radrec(1., ra_arr[i_y, i_x], dec_arr[i_y, i_x]) # Convert vector along the pixel direction, from J2K into the target body frame vec_pix_frame = sp.mxv(mx_j2k_frame, vec_pix_j2k) # And calculate the intercept point between this vector, and the ring plane. # All these are in body coordinates. # plane_target_eq is defined as the body's equatorial plane (its XZ for MU69). # ** Some question as to whether we should shoot this vector at the ring plane, or the sky plane. # Ring plane is normally the one we want. But, for the case of edge-on rings, the eq's break down. # So, we should use the sky plane instead. Testing shows that for the case of MU69 Eq's break down # for edge-on rings... there is always an ambiguity. # ** For testing, try intersecting the sky plane instead of the ring plane. # ** Confirmed: Using sky plane gives identical results in case of face-on rings. # And it gives meaningful results in case of edge-on rings, where ring plane did not. # However, for normal rings (e.g., Jupiter), we should continue using the ring plane, not sky plane. do_sky_plane = True # For ORT4, where we want to use euler angles, need to set this to False if do_sky_plane and ('MU69' in name_target): plane_sky_frame = sp.nvp2pl( vec_sc_target_frame, [0, 0, 0]) # Frame normal to s/c vec, cntrd on MU69 (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_sky_frame) # pt_intersect_frame is the point where the ray hits the skyplane, in the coordinate frame # of the target body. else: # Calc intersect into equator of target plane (ie, ring plane) (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_target_eq) # pt, vec, plane # Swap axes in target frame if needed. # In the case of MU69 (both sunflower and tunacan), the frame is defined s.t. the ring # is in the XZ plane, not XY. This is strange (but correct). # I bet MU69 is the only ring like this. Swap it so that Z means 'vertical, out of plane' -- # that is, put it into normal XYZ rectangular coords, so we can use RECLAT etc on it. if ( 'MU69' in name_target ): # Was 0 2 1. But this makes tunacan radius look in wrong dir. # 201 looks same # 210 similar # 201 similar # 102, 120 similar. # ** None of these change orientation of 'radius' backplane. OK. pt_intersect_frame = np.array([ pt_intersect_frame[0], pt_intersect_frame[2], pt_intersect_frame[1] ]) # Get the radius and azimuth of the intersect, in the ring plane # Q: Why for the TUNACAN is the radius zero here along horizontal (see plot)? # A: Ahh, it is not zero. It is just that the 'projected radius' of a ring that is nearly edge-on # can be huge! Basically, if we try to calc the intersection with that plane, it will give screwy # answers, because the plane is so close to edge-on that intersection could be a long way # from body itself. # Instead, I really want to take the tangent sky plane, intersect that, and then calc the # position of that (in xyz, radius, longitude, etc). # Since that plane is fixed, I don't see a disadvantage to doing that. # We want the 'radius' to be the radius in the equatorial plane -- that is, sqrt(x^2 + y^2). # We don't want it to be the 'SPICE radius', which is the distance. # (For MU69 equatorial plane is nominally XZ, but we have already changed that above to XY.) _radius_3d, lon, lat = sp.reclat(pt_intersect_frame) radius_eq = sp.vnorm( [pt_intersect_frame[0], pt_intersect_frame[1], 0]) # radius_eq = sp.vnorm([pt_intersect_frame[0], pt_intersect_frame[1], pt_intersect_frame[2]]) # Get the vertical position (altitude) altitude = pt_intersect_frame[2] # Calculate the phase angle: angle between s/c-to-ring, and ring-to-sun vec_ring_sun_frame = -pt_intersect_frame + vec_target_sun_frame angle_phase = sp.vsep(-vec_pix_frame, vec_ring_sun_frame) # Save various derived quantities radius_arr[i_y, i_x] = radius_eq lon_arr[i_y, i_x] = lon phase_arr[i_y, i_x] = angle_phase altitude_arr[i_y, i_x] = altitude # Save these just for debugging x_arr[i_y, i_x] = pt_intersect_frame[0] y_arr[i_y, i_x] = pt_intersect_frame[1] z_arr[i_y, i_x] = pt_intersect_frame[2] # Now calc angular separation between this pixel, and the satellites in our list # Since these are huge arrays, cast into floats to make sure they are not doubles. if (name_body.upper() == 'JUPITER'): ang_thebe_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_thebe_j2k) ang_adrastea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_adrastea_j2k) ang_metis_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_metis_j2k) ang_amalthea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_amalthea_j2k) # Now, fix a bug. The issue is that SP.INRYPL uses the actual location of the bodies (no aberration), # while their position is calculated (as it should be) with abcorr=LT. This causes a small error in the # positions based on the INRYPL calculation. This should probably be fixed above, but it was not # obvious how. So, instead, I am fixing it here, by doing a small manual offset. # Calculate the shift required, by measuring the position of MU69 with abcorr=NONE, and comparing it to # the existing calculation, that uses abcorr=LT. This is brute force, but it works. For MU69 approach, # it is 0.75 LORRI 4x4 pixels (ie, 3 1X1 pixels). This is bafflingly huge (I mean, we are headed # straight toward MU69, and it takes a month to move a pixel, and RTLT is only a few minutes). But I have # confirmed the math and the magnitude, and it works. (st_sc_target_j2k_nolt, _) = sp.spkezr(name_target, et, 'J2000', 'NONE', name_observer) vec_sc_target_j2k_nolt = st_sc_target_j2k_nolt[0:3] (_, ra_sc_target_nolt, dec_sc_target_nolt) = sp.recrad(vec_sc_target_j2k_nolt) (x0, y0) = w.wcs_world2pix(ra_sc_target_nolt * hbt.r2d, dec_sc_target_nolt * hbt.r2d, 1) (x1, y1) = w.wcs_world2pix(ra_sc_target * hbt.r2d, dec_sc_target * hbt.r2d, 1) dx = x1 - x0 dy = y1 - y0 print(f'Compute backplanes: INRYPL pixel shift = {dx}, {dy}') dx_int = int(round(dx)) dy_int = int(round(dy)) do_roll = True if do_roll: print( f'compute_backplanes: Rolling by {dx_int}, {dy_int} due to INRYPL' ) # Now shift all of the planes that need fixing. The dRA_km and dDec_km are calculated before INRYPL() # is applied, so they do not need to be shifted. I have validated that by plotting them. # # XXX NP.ROLL() is really not ideal. I should use a function that introduces NaN at the edge, not roll it. radius_arr = np.roll(np.roll(radius_arr, dy_int, axis=0), dx_int, axis=1) lon_arr = np.roll(np.roll(lon_arr, dy_int, axis=0), dx_int, axis=1) phase_arr = np.roll(np.roll(phase_arr, dy_int, axis=0), dx_int, axis=1) altitude_arr = np.roll(np.roll(altitude_arr, dy_int, axis=0), dx_int, axis=1) else: print( f'compute_backplanes: Skipping roll due to INRYPL, based on do_roll={do_roll}' ) # Assemble the results into a backplane backplane = { 'RA': ra_arr.astype(float), # return radians 'Dec': dec_arr.astype(float), # return radians 'dRA_km': dra_arr.astype(float), 'dDec_km': ddec_arr.astype(float), 'Radius_eq': radius_arr.astype(float), 'Longitude_eq': lon_arr.astype(float), 'Phase': phase_arr.astype(float), 'Altitude_eq': altitude_arr.astype(float), # 'x' : x_arr.astype(float), # 'y' : y_arr.astype(float), # 'z' : z_arr.astype(float), # } # Assemble a bunch of descriptors, to be put into the FITS headers desc = { 'RA of pixel, radians', 'Dec of pixel, radians', 'Offset from target in target plane, RA direction, km', 'Offset from target in target plane, Dec direction, km', 'Projected equatorial radius, km', 'Projected equatorial longitude, km', 'Sun-target-observer phase angle, radians', 'Altitude above midplane, km', # 'X position of sky plane intercept', # 'Y position of sky plane intercept', # 'Z position of sky plane intercept' } # In the case of Jupiter, add a few extra fields if (name_body.upper() == 'JUPITER'): backplane['Ang_Thebe'] = ang_thebe_arr.astype( float) # Angle to Thebe, in radians backplane['Ang_Metis'] = ang_metis_arr.astype(float) backplane['Ang_Amalthea'] = ang_amalthea_arr.astype(float) backplane['Ang_Adrastea'] = ang_adrastea_arr.astype(float) # If distance to any of the small sats is < 0.3 deg, then delete that entry in the dictionary if (np.amin(ang_thebe_arr) > fov_lorri): del backplane['Ang_Thebe'] else: print("Keeping Thebe".format(np.min(ang_thebe_arr) * hbt.r2d)) if (np.amin(ang_metis_arr) > fov_lorri): del backplane['Ang_Metis'] else: print("Keeping Metis, min = {} deg".format( np.min(ang_metis_arr) * hbt.r2d)) if (np.amin(ang_amalthea_arr) > fov_lorri): del backplane['Ang_Amalthea'] else: print("Keeping Amalthea, min = {} deg".format( np.amin(ang_amalthea_arr) * hbt.r2d)) if (np.amin(ang_adrastea_arr) > fov_lorri): del backplane['Ang_Adrastea'] else: print("Keeping Adrastea".format( np.min(ang_adrastea_arr) * hbt.r2d)) # And return the backplane set return (backplane, desc)
def get_planet_magnitude(object_id, pos_planet, pos_basis): """ Planet and Moon magnitudes """ planet_abs_mag = { 199: -0.42, # OBJECT_ID_MERCURY 299: -4.40, # OBJECT_ID_VENUS 399: -2.96, # OBJECT_ID_EARTH 499: -1.52, # OBJECT_ID_MARS 599: -9.40, # OBJECT_ID_JUPITER 699: -8.68, # OBJECT_ID_SATURN 799: -7.19, # OBJECT_ID_URANUS 899: -6.87, # OBJECT_ID_NEPTUNE 999: -1.00, # OBJECT_ID_PLUTO } # distance between each objects in AU d_planet_basis = spice.vdist(pos_planet, pos_basis) d_planet_basis = spice.convrt(d_planet_basis, "km", "AU") d_planet_sun = spice.vnorm(pos_planet) d_planet_sun = spice.convrt(d_planet_sun, "km", "AU") d_sun_basis = spice.vnorm(pos_basis) d_sun_basis = spice.convrt(d_sun_basis, "km", "AU") if object_id == OBJECT_ID_SUN: return -27.3 + 5.0 * np.log10(d_sun_basis) elif object_id == OBJECT_ID_MOON: return 0.38 elif object_id in [ OBJECT_ID_MERCURY, OBJECT_ID_VENUS, OBJECT_ID_EARTH, OBJECT_ID_MARS, OBJECT_ID_JUPITER, OBJECT_ID_SATURN, OBJECT_ID_URANUS, OBJECT_ID_NEPTUNE, OBJECT_ID_PLUTO, ]: pass else: return None mag = planet_abs_mag[object_id] mag += 5.0 * np.log10(d_planet_basis * d_planet_sun) if object_id <= OBJECT_ID_JUPITER: pa = 0.0 tpa = (d_planet_sun**2 + d_planet_basis**2 - d_sun_basis) / (2.0 * d_planet_sun * d_planet_basis) tpa = np.clip(tpa, -1.0, 1.0) pa = np.degrees(np.arccos(tpa)) if object_id == OBJECT_ID_MERCURY: mag += (0.0380 - 0.000273 * pa + 0.000002 * pa * pa) * pa elif object_id == OBJECT_ID_VENUS: mag += (0.0009 + 0.000239 * pa - 0.00000065 * pa * pa) * pa elif object_id == OBJECT_ID_MARS: mag += 0.016 * pa elif object_id == OBJECT_ID_JUPITER: mag += 0.005 * pa elif object_id == OBJECT_ID_SATURN: mag -= 1.1 * 0.3 return mag
,indent=2 ) import matplotlib.pyplot as plt plt.axhline(0,color='k',linewidth=0.5) plt.axvline(0,color='k',linewidth=0.5) plt.plot(ets,dvs) plt.xlabel('Time, s past TCA') plt.ylabel('Differential dRange/dt, km/s') plt.title('dRange/dt difference\n[Target wrt Instr] - [Target wrt S/C]') plt.show() plt.axhline(8,color='k',linewidth=0.5) plt.axhline(10,color='k',linewidth=0.5) plt.axvline(0,color='k',linewidth=0.5) plt.plot(ets,[sp.vnorm(porx) for porx in porxs],'r',label='Bennu wrt S/C') plt.plot(ets,[sp.vnorm(porxinstr) for porxinstr in porxinstrs],'g',label='Bennu wrt Instr',linewidth=0.65) plt.xlabel('Time, s past TCA') plt.ylabel('Range, km') plt.title('Time vs. Range to target: wrt Instr; wrt S/C') plt.legend(loc='best') plt.show() plt.axhline(8,color='k',linewidth=0.5) plt.axhline(10,color='k',linewidth=0.5) plt.axvline(6,color='k',linewidth=0.5) plt.axvline(0,color='k',linewidth=0.5) plt.plot([porx[0] for porx in porxs] ,[sp.vnorm(porx) for porx in porxs] ,'r',label='Bennu wrt S/C') plt.plot([porxinstr[0] for porxinstr in porxinstrs]