def relative_position(obj, reference, ettemp, frame='J2000'): kernels = load_kernels() if isinstance(ettemp, Time): et = spice.str2et(ettemp.isot) elif isinstance(ettemp, float): et = ettemp else: et = [ spice.str2et(e.isot) if type(e) == type(Time.now()) else e for e in ettemp ] abcor = 'LT+S' if isinstance(et, float): posvel, lt = spice.spkezr(obj, et, frame, abcor, reference) pos, vel = posvel[:3], posvel[3:] else: pos = np.zeros((len(et), 3)) vel = np.zeros((len(et), 3)) for i, t in enumerate(et): posvel, lt = spice.spkezr(obj, t, frame, abcor, reference) pos[i, :] = np.array(posvel[:3]) vel[i, :] = np.array(posvel[3:]) pos *= u.km vel *= u.km / u.s for k in kernels: spice.unload(k) return pos, vel
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 Calculate_SCoords(run): #setup import spiceypy as spice import numpy as np import math spice.furnsh("./MetDat/MoonMetdat.txt") #import ~_setup.txt and SPK (~.bsp) file slines = [] with open(run + '_setup.txt') as f: slines = f.read().splitlines() spice.furnsh(run + '.bsp') #get TLE_SPK_OBJ_ID and et (time, seconds past J2000) from TLE File obj_id = slines[5].split('=')[1] et = float(slines[28].split('=')[1]) print '' #read out date as yyyy mmm dd hr:min:sec.millisecond print '\n', spice.et2utc(et, 'C', 3) #Calculate sub-observer point and distance state = spice.spkezr(obj_id, et, "MOON_PA", "LT+S", "Moon") s_obs = spice.reclat(state[0][0:3]) print '\nSub-Observer Point:' print ' Sat-Moon distance: ', s_obs[0], 'km' print ' Satellite sub-long: ', s_obs[1] * 180 / math.pi, 'deg' print ' Satellite sub-lat: ', s_obs[2] * 180 / math.pi, 'deg' #Calculate sub-Earth point and distance state = spice.spkezr("Earth", et, "MOON_PA", "LT+S", "Moon") s_eat = spice.reclat(state[0][0:3]) print '\nSub-Earth Point:' print ' Earth-Moon distance: ', s_eat[0], 'km' print ' Earth sub-long: ', s_eat[1] * 180 / math.pi, 'deg' print ' Earth sub-lat: ', s_eat[2] * 180 / math.pi, 'deg' #Calculate sub-Sun point and distance state = spice.spkezr("Sun", et, "MOON_PA", "LT+S", "Moon") s_sun = spice.reclat(state[0][0:3]) print '\nSub-Sun Point:' print ' Sun-Moon distance: ', s_sun[0], 'km' print ' Sun sub-long: ', 90 - s_sun[1] * 180 / math.pi, 'deg' print ' Sun sub-lat: ', s_sun[2] * 180 / math.pi, 'deg\n' #Writes selenographic coordiantes to a file named 'run'+_spoints.txt with open(run + '_spoints.txt', 'w') as f: f.write('' + '\n#Sub-Observer Point:' + '\n\t Sat-Moon distance: ' + str(s_obs[0]) + '\n\t slong: ' + str(s_obs[1] * 180 / math.pi) + '\n\t slat: ' + str(s_obs[2] * 180 / math.pi) + '\n\n#Sub-Earth Point:' + '\n\t Earth-Moon distance: ' + str(s_eat[0]) + '\n\t slong: ' + str(s_eat[1] * 180 / math.pi) + '\n\t slat: ' + str(s_eat[2] * 180 / math.pi) + '\n\n#Sub-Sun Point:' + '\n\t Sun-Moon distance: ' + str(s_sun[0]) + '\n\t slong: ' + str(90 - s_sun[1] * 180 / math.pi) + '\n\t slat: ' + str(s_sun[2] * 180 / math.pi)) return [slines[21].split('=')[1][1:], et, s_obs, s_eat, s_sun]
def _get_body_barycentric_acc(self, body, t): #Uses numerical differentiation to estimate body acceleration interval = 86400 v1 = spice.spkezr(body, t - interval, 'J2000', 'NONE', 'solar system barycenter')[0][3:6] * 1000 v2 = spice.spkezr(body, t + interval, 'J2000', 'NONE', 'solar system barycenter')[0][3:6] * 1000 return (v2 - v1) / (2 * interval)
def PlotJourneyCentralBodyOrbits(self, JourneyAxes, PlotOptions): if PlotOptions.PlotCentralBody != 'Journey central body': import spiceypy journey_central_body = self.central_body if journey_central_body in ['Mars','Jupiter','Uranus','Neptune','Pluto']: journey_central_body = journey_central_body + ' Barycenter' if journey_central_body != PlotOptions.PlotCentralBody: body_state, LT = spiceypy.spkezr(journey_central_body, (self.missionevents[0].JulianDate - 2451545.0) * 86400.0, 'J2000', "None", PlotOptions.PlotCentralBody) r = np.linalg.norm(body_state[0:3]) v = np.linalg.norm(body_state[3:6]) mu = {'Mercury': 22031.780000, 'Venus': 324858.592000, 'Earth': 398600.435436, 'Mars': 42828.375214, 'Jupiter Barycenter': 126712764.800000, 'Saturn Barycenter': 37940585.200000, 'Uranus Barycenter': 5794548.600000, 'Neptune Barycenter': 6836527.100580, 'Pluto Barycenter': 977.000000, 'Sun': 132712440041.939400, 'Moon': 4902.800066, 'Earth-Moon Barycenter': 403503.235502} elements = spiceypy.oscltx(body_state, (self.missionevents[0].JulianDate - 2451545.0) * 86400.0, mu[PlotOptions.PlotCentralBody]) a = elements[-2] #a = r / (2.0 - r*v*v) if a > 0.0: T = 2*math.pi *math.sqrt(a**3 / mu[PlotOptions.PlotCentralBody]) else: T = 2*math.pi * self.TU t0 = (self.missionevents[0].JulianDate - 2451545.0) * 86400.0 tf = t0 + T X = [] Y = [] Z = [] for epoch in np.linspace(t0, tf, num=100): body_state, LT = spiceypy.spkezr(journey_central_body, epoch, 'J2000', "None", PlotOptions.PlotCentralBody) X.append(body_state[0]) Y.append(body_state[1]) Z.append(body_state[2]) JourneyAxes.plot(X, Y, Z, lw=1, c='0.75')
def tess_earth_vector(time): """Return the cartesian position of TESS relative to the Earth.""" # Load the SPICE data ephemFiles = glob.glob(path + "TESS_EPH_PRE_LONG_2018*.bsp") tlsFile = path + "tess2018338154046-41240_naif0012.tls" solarSysFile = path + "tess2018338154429-41241_de430.bsp" for ephFil in ephemFiles: spice.furnsh(ephFil) spice.furnsh(tlsFile) spice.furnsh(solarSysFile) # JD time range allTJD = time + TJD0 nT = len(allTJD) allET = np.zeros((nT, ), dtype=np.float) for i, t in enumerate(allTJD): allET[i] = spice.unitim(t, "JDTDB", "ET") # Calculate positions of TESS, the Earth, and the Sun # Note that our reference frame is that of `starry`, # where `y` points *north*. # This is just the rotation {x, y, z} --> {z, x, y} # relative to the J200 mean equatorial coordinates. tess = np.zeros((3, len(allET))) for i, et in enumerate(allET): outTuple = spice.spkezr("Mgs Simulation", et, "J2000", "NONE", "Earth") tess[0, i] = outTuple[0][1] * REARTH tess[1, i] = outTuple[0][2] * REARTH tess[2, i] = outTuple[0][0] * REARTH return tess
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 Location(et, ingress, sv, when): Coords = np.ones(3) [tgopos, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.target) [mexpos, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.obs) [states, _] = spice.spkezr(sv.target, et - when, sv.fframe, 'NONE', sv.obs) sc2scvector = states[0:3] velocity = states[3:6] relativespeed = np.linalg.norm(velocity) # e9 because we are converting from km to m (SPICE outputs km, but constants in m) veldopp = (relativespeed / constants.c) * 437.1e9 displacement = np.linalg.norm(sc2scvector) sc2scunitvector = np.true_divide(sc2scvector, displacement) # Extract the triaxial dimensions of Mars marsrad = spice.bodvrd(sv.front, 'RADII', 3) # For the ray that connects MEX and TGO, find the point on this ray that is closest to the Martian surface [nearestpoint, alt] = spice.npedln(marsrad[1][0], marsrad[1][1], marsrad[1][2], tgopos, sc2scunitvector) # THERE IS MORE SETTINGS ON THIS [radius, lon, lat] = spice.reclat(nearestpoint) # Rad -> Deg , frame inversion required (hence the negative 180) lon = 180 - (lon * (-180 / math.pi)) lat = lat * (-180 / math.pi) MexNadirTGOAngle = spice.vsep(-mexpos, -sc2scvector) MexNadirTGOAngle = MexNadirTGOAngle * (180 / math.pi) # produce a string of the date and time, because an ephemeris time is not human-readable date_time = spice.timout(et, 'MM-DD HR:MN:SC') ingress_date_time = spice.timout(ingress, 'MM-DD HR:MN:SC') return lon, lat, displacement, nearestpoint, alt, relativespeed, date_time, ingress_date_time, veldopp, MexNadirTGOAngle
def bodyPosition(body,et): import spiceypy as sp x,tl=sp.spkezr(body,et,"J2000","NONE","EARTH") R,alpha,dec=sp.recrad(x[:3]) return R,alpha,dec
def spacecraft_direction(self): """ Returns the x axis of the first velocity vector relative to the spacecraft. This indicates of the craft is moving forwards or backwards. From LROC Frame Kernel: lro_frames_2014049_v01.tf "+X axis is in the direction of the velocity vector half the year. The other half of the year, the +X axis is opposite the velocity vector" Hence we rotate the first velocity vector into the sensor reference frame, but the X component of that vector is inverted compared to the spacecraft so a +X indicates backwards and -X indicates forwards The returned velocity is also slightly off from the spacecraft velocity due to the sensor being attached to the craft with wax. Returns ------- direction : double X value of the first velocity relative to the sensor """ frame_chain = self.frame_chain lro_bus_id = spice.bods2c('LRO_SC_BUS') time = self.ephemeris_start_time state, _ = spice.spkezr(self.spacecraft_name, time, 'J2000', 'None', self.target_name) position = state[:3] velocity = state[3:] rotation = frame_chain.compute_rotation(1, lro_bus_id) rotated_velocity = spice.mxv(rotation._rots.as_matrix()[0], velocity) return rotated_velocity[0]
def get_target_xyz(t): """ Returns the vectors of the Horizons body at a certain time t. Arguments: t: days Julian date of observation Returns: xyz: numpy array A position vector of the observed object uvw: numpy array An instantaneous velocity vector of the observed object radec: numpy array The right ascension and declination of the observed object """ state, lighttime = spice.spkezr(name, t, 'J2000', cor, loc) pos, lighttime = spice.spkpos(name, t, 'J2000', cor, loc) range, ra, dec = spice.recrad(pos) xyz = np.array([state[0], state[1], state[2] ]) / 149597870.7 #6.68459e-9 uvw = np.array([state[3], state[4], state[5] ]) / 149597870.7 * 24. * 3600. #*6.68459e-9 radec = np.array([ra, dec]) return xyz, uvw, radec * 180 / np.pi
def nightlystates(self,timerange): """ Generate asteroid state at every midnight within the input time range Expects integer MJD dates (midnight). Generates geocentric coordinates (range, RA, DEC, rangerate, dRA, dDec) for the asteroid at each midnight between, and including, start and end. Parameters ---------- timerange: 2 element float numpy array [Starting time, Ending time] (MJD) """ # Converting from MJD to JD and computing number of days including endpoints start=int(timerange[0])+shared.mjd2jd stop =int(timerange[-1])+shared.mjd2jd ndays=int(stop-start)+1 # Loading SPK try: sp.furnsh(self.spkname) except: sys.exit("Cannot load %s" %(self.spkname)) # Initializing state variables self.nightlyt=np.zeros(ndays) self.nightlyr=np.zeros(ndays) self.nightlydr=np.zeros(ndays) self.nightlyra=np.zeros(ndays) self.nightlydra=np.zeros(ndays) self.nightlydec=np.zeros(ndays) self.nightlyddec=np.zeros(ndays) counter = -1 for i in np.linspace(start,stop,num=ndays): counter += 1 # Converting from UTC to ET timeet=sp.str2et('JD '+repr(i)) # Finding direction of asteroids from geocenter # Finding states wrt station adds 4 days for 1e6 objects asteroidsvec=sp.spkezr(str(self.spiceid),timeet,"J2000","LT","Earth") # Units are km and km/s # Converting from Rectangular to Latitudinal coordinates. # tmp is a 6 element vector (R, Long, Lat, Dr, Dlong, Dlat) Units are radians and radians/s tmp=sp.xfmsta(asteroidsvec[0][0:6],"RECTANGULAR","LATITUDINAL"," ") self.nightlyt[counter] = i self.nightlyr[counter] = tmp[0] self.nightlydr[counter] = tmp[3] self.nightlyra[counter] = tmp[1] self.nightlydra[counter] = tmp[4] self.nightlydec[counter] = tmp[2] self.nightlyddec[counter]= tmp[5]
def sun_position(self): """ Returns a tuple with information detailing the sun position at the time of the image. Expects center_ephemeris_time to be defined. This must be a floating point number containing the average of the start and end ephemeris time. Expects reference frame to be defined. This must be a sring containing the name of the target reference frame. Expects target_name to be defined. This must be a string containing the name of the target body. Returns ------- : (sun_positions, sun_velocities) a tuple containing a list of sun positions, a list of sun velocities """ times = [self.center_ephemeris_time] positions = [] velocities = [] for time in times: sun_state, _ = spice.spkezr("SUN", time, self.reference_frame, 'LT+S', self.target_name) positions.append(sun_state[:3]) velocities.append(sun_state[3:6]) positions = 1000 * np.asarray(positions) velocities = 1000 * np.asarray(velocities) return positions, velocities, times
def _compute_ephemerides(self): """ Helper function to pull position and velocity in one pass so that the results can then be cached in the associated properties. """ eph = np.empty((self.number_of_ephemerides, 3)) eph_rates = np.empty(eph.shape) current_et = self.starting_ephemeris_time for i in range(self.number_of_ephemerides): state, _ = spice.spkezr( self.spacecraft_name, current_et, self.reference_frame, 'NONE', self.target_name, ) # If this is the sensor, insufficient, if this is the spacecraft, it works? Huh? eph[i] = state[:3] eph_rates[i] = state[3:] current_et += getattr(self, 'dt_ephemeris', 0) # By default, spice works in km eph *= 1000 eph_rates *= 1000 self._sensor_velocity = eph_rates self._sensor_position = eph
def position_velocity(self, jd, of='TESS', relative_to='EARTH'): """ Returns position and velocity of TESS for the given timestamps as geocentric XYZ-coordinates in kilometers. Parameters: jd (ndarray): Time in Julian Days where position of TESS should be calculated. of (string, optional): Object for which to calculate position for. Default='TESS'. relative_to (string, optional): Object for which to calculate position relative to. Default='EARTH'. Returns: ndarray: Position and velocity of TESS as geocentric XYZ-coordinates in kilometers. .. codeauthor:: Rasmus Handberg <*****@*****.**> """ # Convert JD to Ephemeris Time: jd = np.atleast_1d(jd) times = [spiceypy.unitim(j, 'JDTDB', 'ET') for j in jd] # Get state of spacecraft (position and velocity): try: pos_vel, lt = spiceypy.spkezr(of, times, 'J2000', 'NONE', relative_to) pos_vel = np.asarray(pos_vel) except SpiceyError as e: if 'SPICE(SPKINSUFFDATA)' in e.value: raise InadequateSpiceException( "Inadequate SPICE kernels available") else: raise return pos_vel
def velocityUlysses(date, RF="HCI"): """ Get Ulysses' eigen-velocity from SPICE and transform it to RTN-coordinates :param date: datetime object :param RF: etspice.ReferenceFrame :return: eigen-velocity in RTN-coordinates """ from spiceypy import spkezr, datetime2et [x, y, z, vx, vy, vz] = spkezr('ULYSSES', datetime2et(date), RF, 'None', 'SUN')[0] # cart in km(/s) r_sph = cart2spher(np.array([x, y, z])) v_sph = cart2spher(np.array( [vx, vy, vz])) #does not really make sense but is needed for the rtn-function if RF in ["HCI", "HCI_B", "HCI_J"]: # solar equatorial coord. sys. pass elif RF in ["ECLIPJ2000", "ECLIPB1950"]: # ecliptic coord. sys. t_epoch = datetime.datetime( 2000, 1, 1, 12) if RF == "ECLIPJ2000" else datetime.datetime( 2000, 1, 1, 12) + datetime.timedelta(days=(2433282.42345905 - 2451545.0)) r_sph = hc_to_hg(r_sph, ang_ascnode=calc_delta(t_epoch)) v_sph = hc_to_hg(v_sph, ang_ascnode=calc_delta(t_epoch)) vR, vT, vN = hg_to_rtn(v_sph, r_sph) return (np.array([vR, vT, vN]))
def sensor_position(self): """ Returns a tuple with information detailing the position of the sensor at the time of the image. Expects ephemeris_time to be defined. This must be a floating point number containing the ephemeris time. Expects spacecraft_name to be defined. This must be a string containing the name of the spacecraft containing the sensor. Expects reference_frame to be defined. This must be a sring containing the name of the target reference frame. Expects target_name to be defined. This must be a string containing the name of the target body. Returns ------- : (positions, velocities, times) a tuple containing a list of positions, a list of velocities, and a list of times """ if not hasattr(self, '_position'): ephem = self.ephemeris_time pos = [] vel = [] for time in ephem: state, _ = spice.spkezr( self.spacecraft_name, time, self.reference_frame, 'NONE', self.target_name, ) pos.append(state[:3]) vel.append(state[3:]) # By default, spice works in km self._position = [p * 1000 for p in pos] self._velocity = [v * 1000 for v in vel] return self._position, self._velocity, self.ephemeris_time
def force_function(t, y): # Get the derivative vector dy = np.zeros(y.shape) # Populate the first elements of dy with the last elements of y dy[:3] = y[3:] # Get the force on the s/c from Mercury dy[3:] -= MERCURY_MU / MERCURY_MU * y[:3] / (np.linalg.norm(y[:3])**3) # Get the force on the s/c d/t the Sun sun_vector = get_sun_vector(634798080 + t * time_scale) sun_vector /= MERCURY_RADIUS dy[3:] -= SUN_MU / MERCURY_MU * sun_vector[:3] / (np.linalg.norm( sun_vector[:3])**3) # Difference vector diff_vec = y - sun_vector dy[3:] -= SUN_MU / MERCURY_MU * diff_vec[:3] / (np.linalg.norm( diff_vec[:3])**3) for pname in ("Venus", "Jupiter barycenter", "Saturn barycenter"): p_vector, _ = spice.spkezr(pname, 634798080 + t * time_scale, "J2000", "NONE", "Mercury") p_vector /= MERCURY_RADIUS diff_vec = y - p_vector dy[3:] -= P_MUS[pname] / MERCURY_MU * ( p_vector[:3] / (np.linalg.norm(p_vector[:3])**3) + diff_vec[:3] / (np.linalg.norm(diff_vec[:3])**3)) return dy
def n_body_equation(self, t, y): """ n体问题微分方程 y_dot = f(t, y) 见英文版教材P117 :self.PARAM t: 对应tdb时刻 :self.PARAM y: 对应时刻人造卫星的状态向量[x, y, z, vx, vy, vz] :return: y_dot """ y_dot = np.empty((6, )) y_dot[:3] = y[3:] r_mex = y[:3] r_central = spice.spkezr(self.PARAM["Central_Body"], t, self.PARAM["Ref_Frame"], 'None', self.PARAM["Ref_Body"])[0][:3] r_central_mex = r_mex - r_central GM_central = spice.bodvrd(self.PARAM["Central_Body"], "GM", 1)[1][0] a_central = -GM_central * r_central_mex / norm(r_central_mex)**3 perturbations = sum([ self.perturbation(name, r_mex, t) for name in self.PARAM["Perturbation_Bodies"] ]) y_dot[3:] = a_central + perturbations if self.PARAM["Radiation"]: radiation_pressure = self.solar_radiation_pressure(t, y) y_dot[3:] += radiation_pressure return y_dot # v, a
def solar_radiation_pressure(self, t, y): """ 计算太阳辐射光压 见英文版教材P79 3.75 :self.PARAM t: TDB时刻 :self.PARAM y: 对应时刻人造卫星的状态向量[x, y, z, vx, vy, vz] :return: 太阳辐射光压加速度 """ r_mex = y[:3] r_sun = spice.spkezr("Sun", t, self.PARAM["Ref_Frame"], 'None', self.PARAM["Ref_Body"])[0][:3] r_sun_mex = r_mex - r_sun AU_km = spice.gdpool("AU", 0, 1)[0] v = min([ self.body_shadow_function(r_mex, name, t) for name in self.PARAM["Occulting_Bodies"] ]) CR, A, m = [ self.PARAM[i] for i in ["CR", "Surface_Area", "Satellite_Mass"] ] a = v * self.P_Solar * CR * A / m * AU_km**2 * r_sun_mex / norm( r_sun_mex)**3 return a * 1e-3 # km * s^-2 pass
def sun_position(self): sun_state, _ = spice.spkezr("SUN", self.center_ephemeris_time, self.reference_frame, 'NONE', self.target_name) return [sun_state[:4].tolist()], [sun_state[3:6].tolist() ], [self.center_ephemeris_time]
def cassini_titan_altlatlon(tempdatetime): et = spice.datetime2et(tempdatetime) state, ltime = spice.spkezr('CASSINI', et, 'IAU_TITAN', 'NONE', 'TITAN') lon, lat, alt = spice.recpgr('TITAN', state[:3], spice.bodvrd('TITAN', 'RADII', 3)[1][0], 2.64e-4) return alt, lat * spice.dpr(), lon * spice.dpr()
def sun_velocity(self): sun_state, lt = spice.spkezr("SUN", self.center_ephemeris_time, self.reference_frame, 'NONE', self.label['TARGET_NAME']) return [sun_state[3:6].tolist()]
def sun_position(self): sun_state, _ = spice.spkezr("SUN", self.center_ephemeris_time, self.reference_frame, 'NONE', self.label['TARGET_NAME']) return [sun_state[:3].tolist()]
def __state_from_spice(self, x): ''' Routine for actually getting the state vector from spice. ''' spiceid, epoch = x ephemeris_time = self.__compute_ephemeris_time(epoch) state, _ = spice.spkezr(spiceid, ephemeris_time, self.frame, 'none', self.origin) return state
def test(): print("which kernels would you like to load?") meta = input() KERNELS_TO_LOAD = ('meta.tm') print(spice.tkvrsn('TOOLKIT')) #help(spice.furnsh) spice.furnsh(KERNELS_TO_LOAD) print("Load successful") print("Enter time:") time = input() et = spice.str2et(time) #et=spice.str2et( '2012-12-03T12:23:52' ) #help(spice.spkcpo) target0 = 'DAWN' frame0 = 'DAWN_SPACECRAFT' corrtn0 = 'NONE' observ0 = 'DAWN' state0 = [6] state0, ltime0 = spice.spkezr(target0, et, frame0, corrtn0, observ0) print("target:" + target0) print("positions:" + "\nx: " + str(state0[0])) print("y: " + str(state0[1])) print("z: " + str(state0[2])) target1 = 'VESTA' frame1 = 'DAWN_SPACECRAFT' corrtn1 = 'NONE' observ1 = 'DAWN' state1, ltime1 = spice.spkezr(target1, et, frame1, corrtn1, observ1) print("target:" + target1) print("positions:" + "\nx: " + str(state1[0])) print("y: " + str(state1[1])) print("z: " + str(state1[2])) mat = spice.pxform('DAWN_SPACECRAFT', 'IAU_VESTA', et) print("orientation") print(mat) fig = plt.figure(figsize=(9, 9)) ax = fig.add_subplot(111, projection='3d') ax.text(state0[0], state0[1], state0[2], target0) ax.text(state1[0], state1[1], state1[2], target1) ax.set_xlabel('X axis') ax.set_ylabel('Y axis') ax.set_zlabel('Z axis') plt.show()
def perturbation(name, r_mex, t): """ 计算摄动天体加速度 :param name: 摄动天体名称 :param r_mex: 人造卫星(MEX)的位置向量 :param t: TDB时刻 :return: 对应天体摄动加速度加速度[ax, ay, az] """ r_body = spice.spkezr(name, t, PARAM["Ref_Frame"], 'None', PARAM["Ref_Body"])[0][:3] r_cent = spice.spkezr(PARAM["Central_Body"], t, PARAM["Ref_Frame"], 'None', PARAM["Ref_Body"])[0][:3] GM_body = spice.bodvrd(name, "GM", 1)[1][0] r_body_mex = r_mex - r_body r_cent_body = r_body - r_cent a = -GM_body * (r_cent_body / norm(r_cent_body)**3 + r_body_mex / norm(r_body_mex)**3) return a
def getsta(target, TIME, mode="LT+S", observer="SOLAR SYSTEM BARYCENTER"): # https://spiceypy.readthedocs.io/en/master/remote_sensing.html # # Local parameters # METAKR = 'getsta.tm' # # Load the kernels that this program requires. We # will need a leapseconds kernel to convert input # UTC time strings into ET. We also will need the # necessary SPK files with coverage for the bodies # in which we are interested. # spiceypy.furnsh(METAKR) # #Prompt the user for the input time string. # utctim = parseDate(TIME) #print( 'Converting UTC Time: {:s}'.format(utctim) ) # #Convert utctim to ET. # et = spiceypy.str2et(utctim) #print( ' ET seconds past J2000: {:16.3f}'.format(et) ) # # Compute the apparent state of target as seem from # observer in the J2000 frame. All of the ephemeris # readers return states in units of kilometers and # kilometers per second. # [state, ltime] = spiceypy.spkezr(target, et, 'J2000', mode, observer) #print( ' Apparent state of MARS BARYCENTER (4) as seen ' #'from SSB (0) in the J2000\n' #' frame (km, km/s):' ) #print( ' X = {:16.3f}'.format(state[0]) ) #print( ' Y = {:16.3f}'.format(state[1]) ) #print( ' Z = {:16.3f}'.format(state[2]) ) #print( ' VX = {:16.3f}'.format(state[3]) ) #print( ' VY = {:16.3f}'.format(state[4]) ) #print( ' VZ = {:16.3f}'.format(state[5]) ) spiceypy.unload(METAKR) return state
def body_shadow_function(self, r_mex, name, t): """ 计算阴影函数(shadow function)见英文教材P81 3.4.2 :self.PARAM r_mex: 人造卫星(MEX)的位置向量 :self.PARAM name: 遮挡天体的名称 :self.PARAM t: TDB时刻 :return: 阴影函数v """ r_body = spice.spkezr(name, t, self.PARAM["Ref_Frame"], 'None', self.PARAM["Ref_Body"])[0][:3] r_sun = spice.spkezr("Sun", t, self.PARAM["Ref_Frame"], 'None', self.PARAM["Ref_Body"])[0][:3] r_body_mex = r_mex - r_body r_mex_sun = r_sun - r_mex R_body = spice.bodvrd(name, "RADII", 3)[1][0] RS = spice.bodvrd("Sun", "RADII", 3)[1][0] a = asin(RS / norm(r_mex_sun)) b = asin(R_body / norm(r_body_mex)) c = self.agl_between(-1 * r_body_mex, r_mex_sun) v = self.shadow_function(a, b, c) return v
def ChgFrame(state, fromFrame, toFrame, dt): ''' :param: state vector, as array of floats or numpy array. :param: fromFrame: spice frame name, as string :param: toFrame: spice frame name, as string :param: dt: float representing the date and time in J2000. :return: a numpy array ''' _load_kernels_() if isinstance(dt, str): dt = spice.str2et(dt) dcm = DCM(fromFrame, toFrame, dt) position = dcm.dot(state[:3]) velocity = dcm.dot(state[3:]) stateRotd = np.array(list(position) + list(velocity)) # Find the target body name if fromFrame.startswith('IAU_'): # From planetocentric to heliocentric target = fromFrame[4:] if target.lower() in BARYCENTER_FRAMES: # Switch to barycenter, as per https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/aareadme_de430-de431.txt target += '_barycenter' origin = spice.spkezr(target, dt, toFrame, 'None', 'Sun')[0] return stateRotd + origin elif fromFrame.endswith('J2000'): # From heliocentric to planetocentric # Works for EclipJ2000 and J2000 target = toFrame[4:] if target.lower() in BARYCENTER_FRAMES: # Switch to barycenter, as per https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/aareadme_de430-de431.txt target += '_barycenter' origin = spice.spkezr(target, dt, fromFrame, 'None', 'Sun')[0] # Note that we perform the rotation because we asked for the origin in the *fromFrame* not in the *toFrame* like above. position = position - dcm.dot(origin[:3]) velocity = velocity - dcm.dot(origin[3:]) return np.array(list(position) + list(velocity))
def generate_positions(self, times, observing_body, frame, abcorr=None): """ Generate positions from a spice kernel. Parameters ---------- times : time like An object that can be parsed by `~astropy.time.Time`. observing_body : str or int The observing body. Output position vectors are given relative to the position of this body. See https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/naif_ids.html for a list of bodies. frame : str The coordinate system to return the positions in. See https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/frames.html for a list of frames. abcorr : str, optional By default no aberration correciton is performed. See the documentaiton at https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/spkezr_c.html for allowable values and their effects. """ times = time.Time(times) # Spice needs a funny set of times fmt = '%Y %b %d, %H:%M:%S' spice_times = [sp.str2et(time.strftime(fmt)) for time in times] self._abcorr = str(abcorr) # Do the calculation pos_vel, lightTimes = sp.spkezr( self.target.name, spice_times, frame, self._abcorr, observing_body) positions = np.array(pos_vel)[:, :3] * u.km velocities = np.array(pos_vel)[:, 3:] * u.km / u.s self.light_times=np.array(lightTimes) self._frame = frame self._times = time.Time(times) self._velocities = velocities self._x = positions[:, 0] self._y = positions[:, 1] self._z = positions[:, 2] self._vx = velocities[:, 0] self._vy = velocities[:, 1] self._vz = velocities[:, 2] self._generated = True self._observing_body = Body(observing_body)
def calc_ephemeris(target, ets, frame, observer): ''' Convenience wrapper for spkezr and spkgeo ''' if type(target) == str: return array(spice.spkezr(target, ets, frame, 'NONE', observer)[0]) else: n_states = len(ets) states = zeros((n_states, 6)) for n in range(n_states): states[n] = spice.spkgeo(target, ets[n], frame, observer)[0] return states
def ram_angles(time): """Return the angle between SC-Y and the ram direction (0. = Y to ram)""" p = spiceypy.spkezr('MAVEN', time, 'IAU_MARS', 'NONE', 'MARS')[0][3:] r = spiceypy.pxform('IAU_MARS', 'MAVEN_SPACECRAFT', time) a = spiceypy.mxv(r, p) e = np.arctan( np.sqrt(a[1]**2. + a[2]**2.) / a[0]) * 180./np.pi f = np.arctan( np.sqrt(a[0]**2. + a[2]**2.) / a[1]) * 180./np.pi g = np.arctan( np.sqrt(a[0]**2. + a[1]**2.) / a[2]) * 180./np.pi if e < 0.: e = e + 180. if f < 0.: f = f + 180. if g < 0.: g = g + 180. return np.array((e,f,g))
def fetch_state(self, target, epoch): target = self.fromName(target) try: state, lt = spiceypy.spkezr(str(target), self.get_et(epoch), self.frame, self.correction, str(self.center)) except spiceypy.utils.exceptions.SpiceSPKINSUFFDATA: print( f"Insufficient SPICE data exception was raised for {target} at {epoch}", flush=True) state = [None] * 6 return {k: v for k, v in zip(SpiceProvider.STATE_COLUMNS, state)}
sp.furnsh('kernels_kem_ats.tm') utc_start = "2019 1 Jan 06:40:00" utc_end = "2019 1 Jan 07:20:00" et_start = sp.utc2et(utc_start) et_end = sp.utc2et(utc_end) et_mid = (et_start + et_end)/2 num_dt = 500 et = hbt.frange(et_start, et_end, num_dt) name_target = 'MU69' name_observer = 'New Horizons' dist = np.zeros(num_dt) phase = np.zeros(num_dt) for i,et_i in enumerate(et): (state,_) = sp.spkezr(name_target, et_i, 'J2000', 'LT+S', name_observer) dist[i] = np.sqrt(np.sum(state[0:3]**2)) plt.plot(et - et_mid, dist) plt.xlabel('Dist [km]') plt.ylabel('Seconds from c/a') plt.show() print("dist[0] = {}".format(dist))
print(f'{i} {stack.t["filename"][i]}') # plt.imshow(stretch(stack.image_single(i)), origin='lower') # plt.title(f"i={i}, et = {stack.t['et'][i]}") plt.show() #%%% # Align the stack. # The values for the shifts (in pixels) are calculated based # on putting MU69's ra/dec in the center of each frame. # This assumes that MU69 does not move within the stack itself. # ra_mu69 = 274.73344 # Value for 2-Nov-2018. Matches plot_img_wcs.py # dec_mu69 = -20.86170 et = stack.t['et'][0] (st, _) = sp.spkezr('MU69', et, 'J2000', 'LT', 'New Horizons') (_, ra, dec) = sp.recrad(st[0:3]) radec_mu69 = (ra, dec) stack.align(method = 'WCS', center = radec_mu69) # Pass position in radians #%%% # Flatten the stack stack.padding = 65 zoom = 4 self = stack # Do the flattening
def nh_ort_find_ring_pole(): file_superstack = '/Users/throop/Data/ORT4/superstack_ORT4_z4_mean_wcs_sm_hbt.fits' file_tm = 'kernels_kem_prime.tm' sp.unload(file_tm) sp.furnsh(file_tm) f = fits.open(file_superstack) img = f[0].data # plt.imshow(stretch(img)) # plt.show() wcs = WCS(file) num_pts = 200 ra_pole = 275 * hbt.d2r # dec_pole = -56 * hbt.d2r dec_pole = 13 * hbt.d2r radius_ring = 9000 # Radius in km vec_pole_j2k = sp.radrec(1, ra_pole, dec_pole) et = float(f[0].header['SPCSCET']) utc = sp.et2utc(et, 'C', 0) # Get position from NH to UT (st, lt) = sp.spkezr('MU69', et, 'J2000', 'LT', 'New Horizons') vec_nh_ut = st[0:3] # Get position from Sun, to NH (st, lt) = sp.spkezr('New Horizons', et, 'J2000', 'LT', 'Sun') vec_sun_nh = st[0:3] vec_sun_ut = vec_sun_nh + vec_nh_ut # Define a 'ring plane', based on a pole vector, and a point # This ring plane should be in J2K space -- that is, centered on Sun. plane_ring = sp.nvp2pl(vec_pole_j2k, vec_sun_ut) # Pole position is variable. Point is UT in J2K. # Get the point and spanning vectors that define this plane # XXX for some reason, these values from pl2psv do not depend on value of vec_pol_j2k (pt_pl, vec1_pl, vec2_pl) = sp.pl2psv(plane_ring) # Now take a bunch of linear combinations of these spanning vectors # Plot UT's position on the plot (st, lt) = sp.spkezr('MU69', et, 'J2000', 'LT', 'New Horizons') (_, ra, dec) = sp.recrad(vec_nh_ut) (x, y) = wcs.wcs_world2pix(ra*hbt.r2d, dec*hbt.r2d, 0) # plt.plot(x, y, marker = 'o', ms = 10, alpha=0.3, color='purple') # Set an offset to the WCS values, in case UT is not in the right position (ie, not centered properly) dy = 0 # Large value moves up dx = 0 # Draw the ring image plt.imshow(stretch(img), origin='lower') # Calculate and draw all of the ring points for i in range(num_pts): angle_azimuth = 2*math.pi * (i / num_pts) # Put in range 0 .. 2 pi vec_i = vec1_pl * math.sin(angle_azimuth) + vec2_pl * math.cos(angle_azimuth) vec_i = vec_i * radius_ring # Now get the point in space, J2K pt_ring_i_j2k = vec_i + vec_sun_ut vec_sun_ring_i = pt_ring_i_j2k vec_nh_ring_i = vec_sun_ring_i- vec_sun_nh # (_, ra_i, dec_i) = sp.recrad(vec_nh_ring_i) (x, y) = wcs.wcs_world2pix(ra_i*hbt.r2d, dec_i*hbt.r2d, 0) plt.plot(x+dx, y+dy, marker = 'o', ms = 1, color='red', alpha = 0.15) # print(f'{i}, {ra_i*hbt.r2d}, {dec_i*hbt.r2d}, {x}, {y}') plt.title(f'ORT4 Superstack, Ring Pole = ({ra_pole*hbt.r2d},{dec_pole*hbt.r2d}) deg') plt.show() return
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()
def align(self, method = 'wcs', center = None, body = None): """ Take a loaded stack, and set the shift amounts for each image. Does not actually perform the shifts, but updates internal shift settings for each image. The shifting only happens when image is flattened. Updates the internal shift_x_pix, shift_y_pix, accessible thru self.t[<number>]['shift_pix_x']. These pixel shifts are raw, and do not reflect any zoom. Optional arguments ---- method: 'wcs' : Center based on full WCS object. This should usually be used. 'wcs_simple' : Center based on RA / Dec positions in WCS. Does not use full WCS -- just CRVAL. 'body' : Center based on a given body name. Not yet implemented. center: Tuple or string specifying the center to use. For instance, center = (ra_radians, dec_radians) """ if (method.upper() == 'WCS_SIMPLE'): # Do not use this! # If 'radec' is passed, then set the shifts s.t. the right amount is applied to put # specified RA/Dec in the center. # Extract the ra and dec in radians. These specify the desired RA and dec. (ra, dec) = center shift_x_pix = self.t['shift_x_pix'] shift_y_pix = self.t['shift_y_pix'] ra_center = self.t['ra_center'] # RA of each individual image dec_center = self.t['dec_center'] num_images = self.size[0] for i in range(num_images): shift_x_pix[i] = (ra_center[i] - ra)/self.pixscale_x shift_y_pix[i] = (dec_center[i] - dec)/self.pixscale_y # Save these values back to the table, where they live self.t['shift_pix_x'] = shift_x_pix self.t['shift_pix_y'] = shift_y_pix if (method.upper() == 'WCS'): # This is the normal method # If 'WCS' is passed, do the same as 'radec', but use the whole WCS object explicitly. # This should give very nearly the same results. (ra, dec) = center num_images = self.size[0] shift_x_pix = self.t['shift_x_pix'] shift_y_pix = self.t['shift_y_pix'] for i in range(num_images): wcs = self.t['wcs'][i] # Get WCS for this image (pos_pix_x, pos_pix_y) = wcs.wcs_world2pix(ra*hbt.r2d, dec*hbt.r2d, 0) shift_x_pix[i] = self.dx_pix/2 - pos_pix_x shift_y_pix[i] = self.dy_pix/2 - pos_pix_y # Save these values back to the table, where they live self.t['shift_pix_x'] = shift_x_pix self.t['shift_pix_y'] = shift_y_pix if (method.upper() == 'BODY'): num_images = self.size[0] shift_x_pix = self.t['shift_x_pix'] shift_y_pix = self.t['shift_y_pix'] for i in range(num_images): hdu = fits.open(self.t['filename'][i]) et_i = hdu['PRIMARY'].header['SPCSCET'] hdu.close() self.t['et'][i] = et_i # This was badly populated -- here we fix it abcorr = 'LT' frame = 'J2000' name_observer = 'New Horizons' (st,lt) = sp.spkezr(body, et_i, frame, abcorr, name_observer) vec_obs_targ = st[0:3] (_, ra, dec) = sp.recrad(vec_obs_targ) # get ra dec in radians # print(f'For image {}') wcs = self.t['wcs'][i] # Get WCS for this image (pos_pix_x, pos_pix_y) = wcs.wcs_world2pix(ra*hbt.r2d, dec*hbt.r2d, 0) # degrees → pixels shift_x_pix[i] = self.dx_pix/2 - pos_pix_x shift_y_pix[i] = self.dy_pix/2 - pos_pix_y # Save these values back to the table, where they live self.t['shift_pix_x'] = shift_x_pix self.t['shift_pix_y'] = shift_y_pix
# Set up the times for each exposure #============================================================================== 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))
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))
fwhm = 4 # Works well for MVIC framing hdulist = fits.open(file) im = hdulist['PRIMARY'].data w = WCS(file) # Look up the WCS coordinates for this frame center = w.wcs.crval # degrees et = hdulist['PRIMARY'].header['SPCSCET'] file_tm = '/Users/throop/gv/dev/gv_kernels_new_horizons.txt' sp.furnsh(file_tm) (st_sun,lt) = sp.spkezr('Sun', et, 'J2000', 'LT', 'New Horizons') (st_pluto,lt) = sp.spkezr('Pluto', et, 'J2000', 'LT', 'New Horizons') ang_sep = sp.vsep(st_sun[0:3], st_pluto[0:3]) ang_phase_deg = (math.pi - ang_sep)*hbt.r2d # Generate the pickle filename file_stars_pkl = dir_out + sequence + '_stars_deg' + repr(radius) + '.pkl' #============================================================================== # Get stars from a star catalog #============================================================================== try: lun = open(file_stars_pkl, 'rb')
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()
def get_fits_info_from_files_lorri(path, file_tm = "/Users/throop/gv/dev/gv_kernels_new_horizons.txt", pattern=''): "Populate an astropy table with info from the headers of a list of LORRI files." import numpy as np import spiceypy as sp import glob import astropy from astropy.io import fits from astropy.table import Table import astropy.table import math import hbt # For testing: # file = '/Users/throop/Data/NH_Jring/data/jupiter/level2/lor/all/lor_0035020322_0x630_sci_1.fit' # 119 deg phase as per gv # file = '/Users/throop/Data/NH_Jring/data/jupiter/level2/lor/all/lor_0034599122_0x630_sci_1.fit' # 7 deg phase, inbound # t = hbt.get_fits_info_from_files_lorri(file) # Flags: Do we do all of the files? Or just a truncated subset of them, for testing purposes? DO_TRUNCATED = False NUM_TRUNC = 100 # We should work to standardize this, perhaps allowing different versions of this function # for different instruments. d2r = np.pi /180. r2d = 1. / d2r sp.furnsh(file_tm) # *** If path ends with .fit or .fits, then it is a file not a path. Don't expand it, but read it as a single file. if (('.fits' in path) or ('.fit' in path)): file_list = path files = [file_list] else: dir_data = path #dir_data = '/Users/throop/data/NH_Jring/data/jupiter/level2/lor/all' # Start up SPICE # Get the full list of files # List only the files that match an (optional) user-supplied pattern, such as '_opnav' file_list = glob.glob(dir_data + '/*' + pattern + '.fit') files = np.array(file_list) indices = np.argsort(file_list) files = files[indices] # Read the JD from each file. Then sort the files based on JD. jd = [] for file in files: hdulist = fits.open(file) jd.append(hdulist[0].header['MET']) hdulist.close() fits_met = [] # new list (same as array) fits_startmet= [] fits_stopmet = [] fits_exptime = [] # starting time of exposure fits_target = [] fits_reqdesc = [] fits_reqcomm = [] # New 9-Oct-2018 fits_reqid = [] # New 9-Oct-2018 fits_spcinst0= [] fits_spcutcjd= [] fits_naxis1= [] fits_naxis2 = [] fits_sformat = [] # Data format -- '1x1' or '4x4' fits_spctscx = [] # sc - target, dx fits_spctscy = [] # dy fits_spctscz = [] # dz fits_spctcb = [] # target name fits_spctnaz = [] # Pole angle between target and instrument (i.e., boresight rotation angle) fits_rsolar = [] # (DN/s)/(erg/cm^2/s/Ang/sr), Solar spectrum. Use for resolved sources. if (DO_TRUNCATED): files = files[0:NUM_TRUNC] #files_short = np.array(files) #for i in range(files.size): # files_short = files[i].split('/')[-1] # Get just the filename itself # Set up one iteration variable so we don't need to create it over and over num_obs = np.size(files) i_obs = np.arange(num_obs) print("Read " + repr(np.size(files)) + " files.") for file in files: print("Reading file " + file) hdulist = fits.open(file) header = hdulist[0].header keys = header.keys() fits_met.append(header['MET']) fits_exptime.append(header['EXPTIME']) fits_startmet.append(header['STARTMET']) fits_stopmet.append(header['STOPMET']) fits_target.append(header['TARGET']) fits_reqdesc.append(header['REQDESC']) fits_reqcomm.append(header['REQCOMM']) fits_reqid.append(header['REQID']) fits_spcinst0.append(header['SPCINST0']) fits_spcutcjd.append( (header['SPCUTCJD'])[3:]) # Remove the 'JD ' from before number fits_naxis1.append(header['NAXIS1']) fits_naxis2.append(header['NAXIS2']) fits_spctscx.append(header['SPCTSCX']) fits_spctscy.append(header['SPCTSCY']) fits_spctscz.append(header['SPCTSCZ']) fits_spctnaz.append(header['SPCTNAZ']) fits_sformat.append(header['SFORMAT']) fits_rsolar.append(header['RSOLAR']) # NB: This will be in the level-2 FITS, but not level 1 hdulist.close() # Close the FITS file #print object #print "done" # Calculate distance to Jupiter in each of these # Calc phase angle (to Jupiter) # Eventually build backplanes: phase, RA/Dec, etc. # Eventually Superimpose a ring on top of these # ** Not too hard. I already have a routine to create RA/Dec of ring borders. # Eventually overlay stars # Q: Will there be enough there? # Eventually repoint based on stars # ** Before I allow repointing, I should search a star catalog and plot them. # Convert some things to numpy arrays. Is there any disadvantage to this? met = np.array(fits_met) jd = np.array(fits_spcutcjd, dtype='d') # 'f' was rounding to one decimal place... naxis1 = np.array(fits_naxis1) naxis2 = np.array(fits_naxis2) target = np.array(fits_target) # np.array can use string arrays as easily as float arrays instrument = np.array(fits_spcinst0) dx_targ = np.array(fits_spctscx) dy_targ = np.array(fits_spctscy) dz_targ = np.array(fits_spctscz) desc = np.array(fits_reqdesc) reqid = np.array(fits_reqid) reqcomm = np.array(fits_reqcomm) met0 = np.array(fits_startmet) met1 = np.array(fits_stopmet) exptime = np.array(fits_exptime) rotation = np.array(fits_spctnaz) sformat = np.array(fits_sformat) rotation = np.rint(rotation).astype(int) # Turn rotation into integer. I only want this to be 0, 90, 180, 270... rsolar = np.array(fits_rsolar) files_short = np.zeros(num_obs, dtype = 'U60') # Now do some geometric calculations and create new values for a few fields dist_targ = np.sqrt(dx_targ**2 + dy_targ**2 + dz_targ**2) phase = np.zeros(num_obs) utc = np.zeros(num_obs, dtype = 'U30') et = np.zeros(num_obs) subsclat = np.zeros(num_obs) # Sub-sc latitude subsclon = np.zeros(num_obs) # Sub-sc longitude name_observer = 'New Horizons' frame = 'J2000' abcorr = 'LT+S' # Note that using light time corrections alone ("LT") is # generally not a good way to obtain an approximation to an # apparent target vector: since light time and stellar # aberration corrections often partially cancel each other, # it may be more accurate to use no correction at all than to # use light time alone. # Fix the MET. The 'MET' field in fits header is actually not the midtime, but the time of the first packet. # I am going to replace it with the midtime. # *** No, don't do that. The actual MET field is used for timestamping -- keep it as integer. # met = (met0 + met1) / 2. # Loop over all images for i in i_obs: # Get the ET and UTC, from the JD. These are all times *on s/c*, which is what we want et[i] = sp.utc2et('JD ' + repr(jd[i])) utc[i] = sp.et2utc(et[i], 'C', 2) # Calculate Sun-Jupiter-NH phase angle for each image (st_jup_sc, ltime) = sp.spkezr('Jupiter', et[i], frame, abcorr, 'New Horizons') #obs, targ (st_sun_jup, ltime) = sp.spkezr('Sun', et[i], frame, abcorr, 'Jupiter') ang_scat = sp.vsep(st_sun_jup[0:3], st_jup_sc[0:3]) phase[i] = math.pi - ang_scat # phase[i] = ang_scat files_short[i] = files[i].split('/')[-1] # Calc sub-sc lon/lat mx = sp.pxform(frame,'IAU_JUPITER', et[i]) st_jup_sc_iau_jup = sp.mxv(mx, st_jup_sc[0:3]) (radius,subsclon[i],subsclat[i]) = sp.reclat(st_jup_sc[0:3]) # Radians (radius,subsclon[i],subsclat[i]) = sp.reclat(st_jup_sc_iau_jup) # Radians # Stuff all of these into a Table t = Table([i_obs, met, utc, et, jd, files, files_short, naxis1, naxis2, target, instrument, dx_targ, dy_targ, dz_targ, reqid, met0, met1, exptime, phase, subsclat, subsclon, naxis1, naxis2, rotation, sformat, rsolar, desc, reqcomm], names = ('#', 'MET', 'UTC', 'ET', 'JD', 'Filename', 'Shortname', 'N1', 'N2', 'Target', 'Inst', 'dx', 'dy', 'dz', 'ReqID', 'MET Start', 'MET End', 'Exptime', 'Phase', 'Sub-SC Lat', 'Sub-SC Lon', 'dx_pix', 'dy_pix', 'Rotation', 'Format', 'RSolar', 'Desc', 'Comment')) # Define units for a few of the columns t['Exptime'].unit = 's' t['Sub-SC Lat'].unit = 'degrees' # Create a dxyz_targ column, from dx dy dz. Easy! t['dxyz'] = np.sqrt(t['dx']**2 + t['dy']**2 + t['dz']**2) # Distance, in km return t
EPHEMERIDES_FILE = "data/de432s.bsp" if __name__ == "__main__": # Script expects first argument to be a UTC timestamp if len(sys.argv) > 1: input_utc = sys.argv[1] else: input_utc = "2000-01-01T00:00:00" # default for testing # Load the necessary data files sp.furnsh([LEAP_SECOND_FILE, EPHEMERIDES_FILE]) # Convert UTC to ephemeris time epoch = sp.utc2et(input_utc) # State (position and velocity in cartesian coordinates) # of EARTH as seen from SUN in the ecliptic J2000 coordinate frame. state, lt, = sp.spkezr("EARTH", epoch, "ECLIPJ2000", "NONE", "SUN") # Show the output print("Input time = {}".format(input_utc)) print("") print("# Position of the Earth in the heliocentric ecliptic (J2000) frame") print("X = {} km".format(state[0])) print("Y = {} km".format(state[1])) print("Z = {} km".format(state[2])) print("") print("# Velocity of the Earth in the same frame") print("dX = {} km/s".format(state[3])) print("dY = {} km/s".format(state[4])) print("dZ = {} km/s".format(state[5])) # As a sanity check, print the speed speed = (state[3]**2 + state[4]**2 + state[5]**2)**.5 print("")
def plot_backplanes_fits(file): """ This file takes an image, and plots all of the backplanes for it. """ # Start up SPICE file_kernel = '/Users/throop/git/NH_rings/kernels_kem.tm' sp.furnsh(file_kernel) hdulist = fits.open(file) # Loop over all of the planes, and plot each one i=1 fig = plt.subplots() for hdu in hdulist: plt.subplot(3,4,i) plt.imshow(hdu.data) plt.title("{} / {}".format(i-1, hdu.name)) i+=1 plt.show() stretch_percent = 90 stretch = astropy.visualization.PercentileInterval(stretch_percent) # Look up position of MU69 in pixels. et = hdu[0].header['SPCSCET'] utc = sp.et2utc(et, 'C', 0) abcorr = 'LT' frame = 'J2000' name_target = 'MU69' name_observer = 'New Horizons' w = WCS(file_new) (st,lt) = sp.spkezr(name_target, et, frame, abcorr, name_observer) vec_obs_mu69 = st[0:3] (_, ra, dec) = sp.recrad(vec_obs_mu69) (pos_pix_x, pos_pix_y) = w.wcs_world2pix(ra*hbt.r2d, dec*hbt.r2d, 0) # Plot the image itself hbt.figsize((10,10)) plt.imshow(stretch(hdu[0].data)) # Plot one of the planes plt.imshow(stretch(hdu['Longitude_eq'].data), alpha=0.5, cmap=plt.cm.Reds_r) # Plot the ring radius_ring = 100_000 # This needs to be adjusted for different distances. radius_arr = hdu['Radius_eq'].data radius_good = np.logical_and(radius_arr > radius_ring*0.95, radius_arr < radius_ring*1.05) plt.imshow(radius_good, alpha=0.3) # Plot MU69 plt.plot(pos_pix_x, pos_pix_y, ms=10, marker = 'o', color='green') plt.title("{}, {}".format(os.path.basename(file_new), utc)) plt.show() # Close the file hdu.close()
def plot_img_wcs(img, wcs, ra=274.73344, dec=-20.86170, markersize=5, alpha=0.5, color='red', title=None, do_zoom_center = False, do_show=True, width=None, name_observer='New Horizons', name_target = 'MU69', et = None, do_stretch=True, do_inhibit_axes=False, do_colorbar=False, do_plot_fiducial=True, label_axis=None, # vmin=None, vmax=None, scale_km=None, **kwargs): # Default RA, Dec above is for MU69 on approach # (274.73344, -20.86170) = 2-Nov-2018. But it changes v slowly, so should be OK. import matplotlib.pyplot as plt # pyplot import astropy.visualization import numpy as np """ Just plots an image, and uses WCS to plot a specific RA/Dec on it. By default, the RA/Dec is the inbound asymptote for NH MU69. This RA/Dec should be good to within 1 pixel for 15-Aug .. 15-Dec 2018. This is a one-off func; not in a class. **kwargs are passed to imshow(), not to plot(). Parameters ----- do_zoom_center: Flag. Zoom in on just the center region, or show the entire plot. width: If set, crop the plot to the specified width in pixels, centered at the plot center. Plot is assumed to be square (height=width). If not set, the entire plot is shown. do_show: If False, suppress the final plt.show(). This allows other planes to be plotted on top later if requested. do_stretch: [Default True]. Boolean. If set, apply a stretch to the outuput image before plotting. do_inhibit_axes: Boolean. If set, do not print the x and y values on the respective axes. do_plot_fiducial: Boolean. If set, plot little red dots on the edges, showing the original image center location. ra: RA to center on [degrees] dec: Declination to center on [degrees] name_target: name_observer: et: If *all* of these parameters are supplied, then the RA and Dec are taken from a call to SPKEZR, rather than from the supplied RA/Dec. scale_km: pixel scale in km/pix. If this is passed, then xy axes are labeled in km from center. If this is 'None', then xy axes are labeled in pixels. label_axis: Text to label the axes with (e.g., 'pixels' or 'km from MU69'). If None, then no label is plotted. """ stretch_percent = 90 stretch = astropy.visualization.PercentileInterval(stretch_percent) # PI(90) scales to 5th..95th %ile. # Set the width and height appropriately if not width: # If no width passed, then just use the default. width = np.shape(img)[0] hw = int(width/2) ycenter = int((np.shape(img)[0])/2) # Vertical xcenter = int((np.shape(img)[0])/2) # Horizontal x0 = xcenter-hw # center positions, in pixels x1 = xcenter+hw y0 = ycenter-hw y1 = ycenter+hw # Crop the image if needed img_crop = img[x0:x1, y0:y1] ycenter_crop = int((np.shape(img_crop)[0])/2) # Vertical xcenter_crop = int((np.shape(img_crop)[0])/2) # Horizontal # Stretch the image vertically. Must do this after the crop, not before! vmax = np.percentile(img_crop, 95) vmin = np.percentile(img_crop, 5) img_crop_stretch = np.clip(img_crop, a_min=vmin, a_max=vmax) # If requested, calculate the x and y axes, in km extent = None # Set ehe default value size = np.shape(img_crop_stretch) if scale_km is not None: extent = [-size[0]/2 * scale_km, size[0]/2 * scale_km, -size[1]/2 * scale_km, size[1]/2 * scale_km] # print(f'extent = {extent}') else: extent = [0, size[0], 0, size[1] ] # Do the actual imshow() if not(do_stretch): fig = plt.imshow(img_crop, origin='lower', extent=extent, **kwargs) else: fig = plt.imshow(img_crop_stretch, origin='lower', extent=extent, **kwargs) # If requested, inhibit printing values on the x and y axes if do_inhibit_axes: plt.gca().get_xaxis().set_visible(False) plt.gca().get_yaxis().set_visible(False) # If requested, use SPICE to determine the position to plot if name_target and name_observer and et: abcorr = 'LT' frame = 'J2000' (st, lt) = sp.spkezr(name_target, et, frame, abcorr, name_observer) vec = st[0:3] (_, ra, dec) = sp.recrad(vec) ra *= hbt.r2d dec *= hbt.r2d (x, y) = wcs.wcs_world2pix(ra, dec, 0) # The '0' is the 'origin': 1 for FITS, where [1,1] is origin. # 0 for numpy, where [0,0] is origin. # (Though if I go into DS9, it says that LL pixel is 0,0.) # I assume that I should use 0 here # Definition of 'origin', which is third argument of wcs_world2pix: # "Here, origin is the coordinate in the upper left corner of the image. In FITS and Fortran standards, # this is 1. In Numpy and C standards this is 0." # print(f'Position xy: {x}, {y}') # plt.xlim((xcenter-hw, xcenter+hw)) # plt.ylim((ycenter-hw, ycenter+hw)) # Make some markers on the edge, so we can tell if MU69 is actually properly centered, or not. if do_plot_fiducial: if scale_km is not None: # Mark center # plt.plot(x, y, marker = 'o', markersize = markersize, alpha=alpha, color=color) # Mark four edges plt.plot([0*scale_km, 0*scale_km, hw*scale_km, -hw*scale_km], [-hw*scale_km, hw*scale_km, 0,0], marker = 'o', color='red', ls='None') else: # Mark center # Mark four edges # plt.plot(0, 0, marker = 'o', markersize = markersize, alpha=alpha, color=color) plt.plot([ycenter_crop, ycenter_crop, ycenter_crop+hw, ycenter_crop-hw], [xcenter_crop-hw, xcenter_crop+hw, xcenter_crop, xcenter_crop], marker = 'o', color='red', ls='None') # Label axes, if requested if label_axis is not None: # if scale_km is not None: # text = 'km' # else: # text = 'pix' plt.xlabel(label_axis) plt.ylabel(label_axis) plt.title(title) # Plot a colorbar, if requested if do_colorbar: plt.colorbar(fig, format='%.0e') # Display the whole thing if do_show: plt.show()
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)
# ###################################################################### # SPICE RELATED ROUTINES # ###################################################################### # Ephemeris time: et=sp.str2et("01/01/2015 00:00:00.000 UTC") etini=sp.str2et("01/01/%s 00:00:00.000 UTC"%yini) jdini=et2jd(etini) etend=sp.str2et("01/01/%s 00:00:00.000 UTC"%yend) print "Number of days:",(etend-etini)/DAY det=1.0*DAY/6 det=1*HOUR astronomy=[] et=etini while True: # STATE VECTOR MOON AND SUN rmoon=sp.spkezr("MOON",et,"J2000","NONE","399") rsun=sp.spkezr("SUN",et,"J2000","NONE","399") Rm,vm=normX(rmoon) Rs,vs=normX(rsun) V=Dmoon*(Rmoon/Rm)**3+Dsun*(Rsun/Rs)**3 # JULIAN DAY jd=et2jd(et) astronomy+=[[jd,Rm/Rmoon,Rs/Rsun,V]] if et>etend:break et+=det astronomy=numpy.array(astronomy) numpy.savetxt("astronomy-cycles-%s_%s.data"%(yini,yend),astronomy) jds=astronomy[:,0] Rms=astronomy[:,1]
#pos = (300, 700) pos = (100, 200) # y, x in normal imshow() coordinates. #dist_target = 0.01*u.au dist_solar = 43.2*u.au # MU69 dist at encounter: 43.2 AU, from KEM Wiki page do_psf = True # Flag: Do we convolve result with NH LORRI PSF?26.7 + 0.65 dt_obs = -22*u.day # Time relative to MU69 C/A utc_ca = '2019 1 Jan 05:33:00' et_ca = sp.utc2et(utc_ca) et_obs = et_ca + dt_obs.to('s').value utc_obs = sp.et2utc(et_obs, 'C', 0) utc_obs_human = 'K{:+}d'.format(dt_obs.to('day').value) vec,lt = sp.spkezr('2014 MU69', et_obs, 'J2000', 'LT', 'New Horizons') vec_sc_targ = vec[0:3] dist_target = np.sqrt(np.sum(vec_sc_targ**2))*u.km.to('AU')*u.au arr = nh_make_simulated_image_lorri(do_ring=True, dist_ring_smoothing = 1000*u.km, iof_ring = iof_ring, a_ring = (5000*u.km, 10000*u.km), exptime = exptime, mode = mode, pos = pos, dist_solar = dist_solar, dist_target = dist_target, do_mu69 = False, do_psf = True)
# Get the RA / Dec for the four corner points if t_i['Format'] == '1X1': radec_corners = w.wcs_pix2world([[0,0],[0,1024],[1023,1023],[1023,0],[0,0]],0) * hbt.d2r # Radians radec_center = w.wcs_pix2world([[511,511]],0) * hbt.d2r # Radians arr *= 16 # For vertical scaling, all of the 1x1's must be scaled up by ~16x to match 4x4's. if t_i['Format'] == '4X4': radec_corners = w.wcs_pix2world([[0,0],[0,256],[256,256],[256,0],[0,0]],0) * hbt.d2r # Radians radec_center = w.wcs_pix2world([[128,128]],0) * hbt.d2r # Radians # Get RA / Dec for Jupiter (vec,lt) = sp.spkezr('Jupiter', t_i['ET'], 'J2000', abcorr, 'New Horizons') (_, ra_jup, dec_jup) = sp.recrad(vec[0:3]) # Radians vec_nh_jup = vec[0:3] # Get vectors Sun-Jup, and Sun-NH (vec,lt) = sp.spkezr('Jupiter', t_i['ET'], 'J2000', abcorr, 'Sun') vec_sun_jup = vec[0:3] # Radians vec_sun_nh = vec_sun_jup - vec_nh_jup # Plot the boxes 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
# Calculate the ring location #============================================================================== # Get an array of points along the ring ra_ring1, dec_ring1 = hbt.get_pos_ring(et, name_body='Jupiter', radius=122000, units='radec', wcs=w) ra_ring2, dec_ring2 = hbt.get_pos_ring(et, name_body='Jupiter', radius=129000, units='radec', wcs=w) # Return as radians x_ring1, y_ring1 = w.wcs_world2pix(ra_ring1*r2d, dec_ring1*r2d, 0) # Convert to pixels x_ring2, y_ring2 = w.wcs_world2pix(ra_ring2*r2d, dec_ring2*r2d, 0) # Convert to pixels # Get position of Metis, in pixels (vec6,lt) = sp.spkezr('Metis', et, 'J2000', 'LT+S', 'New Horizons') (junk, ra_metis, dec_metis) = sp.recrad(vec6[0:3]) # Look up velocity of NH, for stellar aberration abcorr = 'LT+S' frame = 'J2000' st,ltime = sp.spkezr('New Horizons', et, frame, abcorr, 'Sun') # Get velocity of NH vel_sun_nh_j2k = st[3:6] # Correct stellar RA/Dec for stellar aberration radec_stars_cat = np.transpose(np.array((ra_stars_cat, dec_stars_cat))) radec_stars_cat_abcorr = hbt.correct_stellab(radec_stars_cat, vel_sun_nh_j2k) # Store as radians # Convert ring RA/Dec for stellar aberration
#planes = pickle.load(lun) #lun.close() #radius = planes['Radius_eq'] radius = hdulist['Radius_eq'].data longitude = hdulist['Longitude_eq'].data et = hdulist['PRIMARY'].header['SPCSCET'] # Get the ET from the file utc = sp.et2utc(et, 'C', 0) w = WCS(file) # Calculate the sub-observer latitude (ie, ring tilt angle) (vec, lt) = sp.spkezr('New Horizons', et, 'IAU_PLUTO', 'LT', 'Pluto') vec = vec[0:3] (junk, lon_obs, lat_obs) = sp.reclat(vec) # Get latitude, in radians #============================================================================== # Make a plot of the image + backplanes #============================================================================== plt.set_cmap('Greys_r') hbt.figsize((9,6)) # This gets ignored sometimes here. I'm not sure why?? plt.subplot(1,3,1) plt.imshow(stretch(im)) plt.title(sequence) plt.gca().get_xaxis().set_visible(False)
et_limits_arr = [] for utc in utc_limits_arr: et_limits_arr.append(sp.utc2et(utc)) num_et = 100 et_arr = hbt.frange(et_limits_arr[0], et_limits_arr[1], num_et) phase_arr = [] utc_arr = [] for et in et_arr: (st, lt) = sp.spkezr('MU69', et, 'J2000', 'LT+S', 'New Horizons') vec_sc_mu69 = st[0:3] (st, lt) = sp.spkezr('MU69', et, 'J2000', 'LT+S', 'Sun') vec_sun_mu69 = st[0:3] ang_phase = sp.vsep(-vec_sc_mu69, -vec_sun_mu69) phase_arr.append(ang_phase) utc_arr.append(sp.et2utc(et, 'C', 0)) print(f'Phase angle = {ang_phase*hbt.r2d} deg at {utc_arr[-1]}') phase_arr = np.array(phase_arr) et_arr = np.array(et_arr)
et_ca = sp.utc2et(utc_ca) hour = 3600 minute = 60 dt = -12 * hour # What is the offset of our observation, from KBO C/A. K-1h, K+2d, etc. 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?
encounter_phase = 'Inbound' if (et_start < et_ca) else 'Outbound' # Set up the ET array if DO_TIMES_OPNAV: et = et_opnav else: et = hbt.frange(et_start, et_end, num_dt) index_asymptote = 0 if (encounter_phase == 'Outbound') else 1 # We make one call to SPICE to look up the asymptote position # Most SPICE calls are later, but we need to do this one now, ahead of order. (st,lt) = sp.spkezr('MU69', et[index_asymptote], 'J2000', 'LT', 'New Horizons') (junk, ra_asymptote, dec_asymptote) = sp.recrad(st[0:3]) crval = np.array([ra_asymptote, dec_asymptote]) * hbt.r2d radius_search = 0.15 # degrees # We only use these catalog for very fine searches, so narrow is OK. DO_PLOT_GSC = False # Goes to about v=12 if DO_PLOT_GSC: name_cat = u'The HST Guide Star Catalog, Version 1.1 (Lasker+ 1992) 1' # works, but 1' errors; investigating stars = conesearch.conesearch(crval, radius_search, cache=False, catalog_db = name_cat) ra_stars = np.array(stars.array['RAJ2000'])*hbt.d2r # Convert to radians dec_stars = np.array(stars.array['DEJ2000'])*hbt.d2r # Convert to radians mag_stars = np.array(stars.array['Pmag']) if DO_PLOT_USNO: