def worker_transform_frame(args): """Worker function for transforming 3D coordinates. Parameters ---------- args: (np.ndarray, np.ndarray, str, str, np.ndarray, np.ndarray, list) Function arguments as tuple. Returns ------- np.ndarray Transformed 3D coordinates. """ (t, data, frame_from, frame_to, frames, frame_indices, kernels) = args if len(heliosat._spice["kernels_loaded"]) == 0: heliosat.spice.spice_init() heliosat.spice.spice_reload(kernels) if frames: for i in range(0, len(t)): data[i] = spiceypy.mxv(frames[int(frame_indices[i])], data[i]) else: for i in range(0, len(t)): data[i] = spiceypy.mxv(spiceypy.pxform(frame_from, frame_to, spiceypy.datetime2et(t[i])), data[i]) return data
def caps_all_anodes(tempdatetime): et = spice.datetime2et(tempdatetime) sclkdp = spice.sce2c( -82, et) # converts an et to a continuous encoded sc clock (ticks) caps_els_anode_vecs = [] for anodenumber, x in enumerate(np.arange(70, -90, -20)): # print(anodenumber, x) rotationmatrix_anode = spice.spiceypy.axisar(np.array( [1, 0, 0]), x * spice.rpd()) # Get angles for different anodes # print("rotationmatrix_anode", rotationmatrix_anode) postanode_rotation = spice.vhat( spice.mxv(rotationmatrix_anode, -spice.spiceypy.getfov( -82821, 20)[2])) # Apply rotation for anodes # print("postanode_rotation", postanode_rotation) # print("caps_els_boresight", caps_els_boresight) cassini_caps_mat = spice.ckgp( -82821, sclkdp, 0, 'CASSINI_CAPS_BASE')[0] # Get actuation angle # print("cassini_caps_mat", cassini_caps_mat) cassini_caps_act_vec = spice.mxv( cassini_caps_mat, postanode_rotation) # Rotate with actuator # print("Actuating frame", cassini_caps_act_vec) CAPS_act_2_titan_cmat = spice.ckgp( -82000, sclkdp, 0, 'IAU_TITAN')[0] # Find matrix to transform to IAU_TITAN frame CAPS_act_2_titan_cmat_transpose = spice.xpose( CAPS_act_2_titan_cmat) # Tranpose matrix rotated_vec = spice.mxv(CAPS_act_2_titan_cmat_transpose, cassini_caps_act_vec) # Apply Matrix # print("rotated_vec ", rotated_vec) caps_els_anode_vecs.append(rotated_vec) return caps_els_anode_vecs
def transform_lonlat(t, lons, lats, frame_from, frame_to): """Transform longitude/latitude direction from one reference frame to another. Parameters ---------- t : datetime.datetime Evaluation datetimes. lons : np.ndarray Longitudes array. lats : np.ndarray Latitudes array. Returns ------- (np.ndarray, np.ndarray) Transformed longitude/latitude's. Raises ------ ValueError If source and target frame are equal. """ if frame_from == frame_to: raise ValueError("source frame and target frame are equal") lons_rad = 2 * np.pi * lons / 360 lats_rad = 2 * np.pi * lats / 360 vecs = np.array([ [np.cos(lon), np.sin(lon), np.sin(lat)] for (lon, lat) in list(zip(lons_rad, lats_rad)) ]) if isinstance(t, datetime.datetime): for i in range(0, len(vecs)): vecs[i] = spiceypy.mxv(spiceypy.pxform(frame_from, frame_to, spiceypy.datetime2et(t)), vecs[i]) elif isinstance(t, np.ndarray) or isinstance(t, list): for i in range(0, len(t)): vecs[i] = spiceypy.mxv(spiceypy.pxform(frame_from, frame_to, spiceypy.datetime2et(t[i])), vecs[i]) else: raise TypeError("variable t is not a datetime or array/list") res = np.array([ [360 * np.arccos(v[0]) / 2 / np.pi, 360 * np.arcsin(v[2]) / 2 / np.pi] for v in vecs ]) return res
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 search_stars(obsinfo, mag_limit=7.0): query = ("SELECT" " CATALOG_NUMBER,RA,DEC,VISUAL_MAGNITUDE,PARLAX,SPECTRAL_TYPE" " FROM HIPPARCOS") nmrows, _error, _errmsg = spice.ekfind(query) stars = [] for row in range(nmrows): ra = spice.ekgd(1, row, 0)[0] * spice.rpd() dec = spice.ekgd(2, row, 0)[0] * spice.rpd() mag = spice.ekgd(3, row, 0)[0] vec = spice.radrec(1.0, ra, dec) tvec = spice.mxv(obsinfo.ref2obsmtx, vec) _tpa, tdist = vec_padist(obsinfo.center, tvec) if tdist < obsinfo.fov.fovmax and mag < mag_limit: parallax = spice.ekgd(4, row, 0)[0] spectral = spice.ekgc(5, row, 0)[0] distance = 1.0 / np.tan(parallax * spice.rpd()) distance = spice.convrt(distance, "AU", "km") pos = tvec * distance vp = viewport_frustum(obsinfo.fov.bounds_rect, obsinfo.width, obsinfo.height, pos) star = { "hip_id": spice.ekgi(0, row, 0)[0], "position": tvec, "ra": ra, "dec": dec, "spectral_type": spectral, "visual_magnitude": mag, "distance": distance, "image_pos": vp[0:2], "color": get_star_color(spectral), } stars.append(star) return stars
def get_geometry_info(obs2refmtx, fov, width, height): cvec = spice.vhat(fov.bounds_rect.center_vec) cvec_ref = spice.mxv(obs2refmtx, cvec) # Azimuth in the upward direction of the screen mvec = spice.vhat(fov.bounds_rect.top_vec) mvec_ref = spice.mxv(obs2refmtx, mvec) pa, dist = vec_padist(cvec_ref, mvec_ref) pos_angle = pa * spice.dpr() # Pixel resolution up to the center of the screen top edge vp = viewport_frustum(fov.bounds_rect, width, height, mvec) angle_res = dist / (height / 2.0 - vp[1]) * spice.dpr() _, ra, dec = spice.recrad(cvec_ref) return pos_angle, angle_res, ra * spice.dpr(), dec * spice.dpr()
def conicaEspacio(Omega=0, i=0, omega=0, figsize=(4, 4)): #Propiedades de la cnica p = 1 e = 0.7 #Visual inicial A = 60 h = 30 #Puntos de la cnica en el sistema natural de referencia fs = np.linspace(0, 360 * GRADOS, 100) rs = p / (1 + e * np.cos(fs)) xs = rs * np.cos(fs) ys = rs * np.sin(fs) zs = np.zeros_like(fs) #Vectores en el sistema original vecrs = np.array([[x, y, z] for x, y, z in zip(xs, ys, zs)]) #Matriz de transformacin Omega *= GRADOS i *= GRADOS omega *= GRADOS #Ntese que en el caso de la cnica, el sistema natural #es el que se encuentra rotado respecto al sistema final #por eso la matriz es la inversa Rtot = spy.eul2m(-Omega, -i, -omega, 3, 1, 3) #Puntos transformados vecrppps = np.array([ spy.mxv(Rtot, [vecrs[i, 0], vecrs[i, 1], vecrs[i, 2]]) for i in range(len(fs)) ]) #Grafico fig = plt.figure(figsize=figsize) ax = fig.add_subplot(111, projection='3d') #Defino desde dnde voy a ver la cnica ax.view_init(h, A) #Grfica de los puntos ax.plot(vecrppps[:, 0], vecrppps[:, 1], vecrppps[:, 2], 'r-', lw=3) #Ejes originales vmax = 3 ax.plot([0, vmax, 0], [0, 0, 0], [0, 0, 0], 'k-') ax.text(vmax, 0, 0, 'x') ax.plot([0, 0, 0], [0, vmax, 0], [0, 0, 0], 'k-') ax.text(0, vmax, 0, 'y') ax.plot([0, 0, 0], [0, 0, 0], [0, 0, vmax], 'k-') ax.text(0, 0, vmax, 'z') #Decoracin xl = ax.set_xlim((-vmax, vmax)) yl = ax.set_ylim((-vmax, vmax)) zl = ax.set_zlim((-vmax, vmax))
def caps_crosstrack(tempdatetime, windspeed): et = spice.datetime2et(tempdatetime) state, ltime = spice.spkezr("CASSINI", et, "IAU_TITAN", "NONE", "titan") ramdir = spice.vhat(state[3:6]) # print("ramdir",ramdir) # Gets Attitude sclkdp = spice.sce2c( -82, et) # converts an et to a continuous encoded sc clock (ticks) ckgp_output = spice.ckgp(-82000, sclkdp, 0, "IAU_TITAN") cmat = ckgp_output[0] spacecraft_axis = np.array([0, 0, 1]) rotated_spacecraft_axis = spice.mxv(cmat, spacecraft_axis) # print("cmat", cmat) # print("rotated spacecraft axis",rotated_spacecraft_axis) ram_unit = spice.mxv(cmat, -ramdir) # Ram Unit in SC coords # print("ram_unit",ram_unit) if windspeed < 0: rotationmatrix = spice.axisar(np.array([0, 0, -1]), 90 * spice.rpd()) if windspeed > 0: rotationmatrix = spice.axisar(np.array([0, 0, -1]), -90 * spice.rpd()) # print(rotationmatrix) crossvec = spice.mxv( rotationmatrix, ram_unit) # Rotate ram unit to find crosstrack velocity vector # print("temp crossvec",crossvec) # print("vsep SC Frame",spice.vsep(ram_unit,crossvec)*spice.dpr()) cmat_t = spice.xpose(cmat) crossvec_titan = spice.mxv(cmat_t, crossvec) # Transform back to IAU Titan Frame # print("crossvec", crossvec) # print("crossvec_titan", crossvec_titan, spice.unorm(crossvec_titan)) # print("vsep titan frame", spice.vsep(ramdir, crossvec_titan) * spice.dpr()) return crossvec_titan
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 caps_ramdirection_azielv(tempdatetime, observ='titan'): """ Returns the azimuth and elevation of the ramdirection """ ram_unit = cassini_ramdirection_SCframe(tempdatetime, observ=observ) sc2CAPS = np.array(([0, -1, 0], [-1, 0, 0], [0, 0, -1])) ram_unit_CAPS = spice.mxv(sc2CAPS, ram_unit) # print(ram_unit_CAPS) ram_unit_azielv = np.array(spice.reclat(ram_unit_CAPS)[1:]) * spice.dpr() return ram_unit_azielv[0], ram_unit_azielv[1]
def rotate_CAPS_SCframe(CAPS_actuation, instrument, anodes=False): """ Returns the CAPS pointing vector after actuation in the SC frame """ naifiddict = {'ims': -82820, 'els': -82821, 'ibs1': -82822, 'ibs2': -82823, 'ibs3': -82824} naifid = naifiddict[instrument] if not anodes: rotationmatrix = spice.spiceypy.axisar(np.array([0, 0, -1]), CAPS_actuation * spice.rpd()) temp = spice.mxv(rotationmatrix, spice.spiceypy.getfov(naifid, 20)[2]) if (instrument == 'ims' or instrument == 'els') and anodes == True: temp = [] rotationmatrix_act = spice.spiceypy.axisar(np.array([0, 0, -1]), CAPS_actuation * spice.rpd()) for anodenumber, x in enumerate(np.arange(70, -90, -20)): rotationmatrix_anode = spice.spiceypy.axisar(np.array([-1, 0, 0]), x * spice.rpd()) postanode_rotation = spice.mxv(rotationmatrix_anode, spice.spiceypy.getfov(naifid, 20)[2]) temp.append(spice.mxv(rotationmatrix_act, postanode_rotation)) return temp
def get_vector(frame_from: str, frame_to: str, vector: np.ndarray, time: datetime.datetime) -> np.ndarray: """ Transform vector from one reference frame to another. :param frame_from: source frame :param frame_to: target frame :param vector: vector in source frame :param time: datetime :return: vector in target frame """ if frame_from == frame_to: return vector times_et = spiceypy.datetime2et(time.replace(tzinfo=None)) return spiceypy.mxv(spiceypy.pxform(frame_from, frame_to, times_et), vector)
def cassini_ramdirection_SCframe(tempdatetime, target='CASSINI', frame='J2000', observ='titan', corrtn='NONE', output=False): et = spice.datetime2et(tempdatetime) state, ltime = spice.spkezr(target, et, frame, corrtn, observ) ramdir = spice.vhat(state[3:6]) # Gets Attitude sclkdp = spice.sce2c(-82, et) # converts an et to a continuous encoded sc clock (ticks) ckgp_output = spice.ckgp(-82000, sclkdp, 0, frame) cmat = ckgp_output[0] ram_unit = spice.mxv(cmat, ramdir) if output: print('ET = {:20.6f}'.format(et)) print('VX = {:20.6f}'.format(ram_unit[0])) print('VY = {:20.6f}'.format(ram_unit[1])) print('VZ = {:20.6f}'.format(ram_unit[2])) return ram_unit
def getSpoint(self): self.bight_obs = [] self.spoing = [] # # calculation of the boresight vector and the intersection point # for et in self.time.window: mat_ins_obs = cspice.pxform(self.frame, self.host.frame, et) bsight_obs = cspice.mxv(mat_ins_obs, self.bsight_ins) spoint, trgepc, srfvec = cspice.sincpt('Ellipsoid', self.target.name, et, self.target.frame, 'NONE', self.host.name, self.host.frame, bsight_obs) self.bsight_obs.append(bsight_obs) self.spoint.append(spoint) return self.spoint
mx = sp.pxform('B1950', 'J2000', et[0]) # SPICE knows about 1950, but not 1900! ra_1950 = t['RA'] dec_1950 = t['Dec'] ra_2000 = [] dec_2000 = [] pt_1950 = [] # Loop over every star and precess it individually. This loop is a real bottleneck. for i in range(num_stars): pt_1900 = sp.radrec(1, ra_1950[i], dec_1950[i]) pt_1950 = sp.mxv(mx, pt_1900) pt_2000 = sp.mxv(mx, pt_1950) # SPICE will not precess 1900 -> 2000. So we apply 1950 -> 2000, do it 2x. d, ra_2000_i, dec_2000_i = sp.recrad(pt_2000) dec_2000.append(dec_2000_i) ra_2000.append(ra_2000_i) hd['RA_2000'] = ra_2000 hd['Dec_2000'] = dec_2000 # Now save this pickle file, so we never have to run this routine again lun = open(file_hd_pickle, 'wb') pickle.dump(hd, lun) lun.close() print("Wrote: " + file_hd_pickle)
def plot_solar_ilum(utc, mk, dsk, observer, target, target_frame, pixels=150): mpl.rcParams['figure.figsize'] = (26.0, 26.0) spiceypy.furnsh(mk) utcstr = utc[10:13] + utc[14:16] + utc[17:19] et = spiceypy.utc2et(utc) spiceypy.furnsh(dsk) nx, ny = (pixels, pixels) # resolution of the image x = np.linspace(-5, 5, nx) y = np.linspace(-5, 5, ny) xv, yv = np.meshgrid(x, y) phase_matrix = np.zeros((nx, ny)) emissn_matrix = np.zeros((nx, ny)) solar_matrix = np.zeros((nx, ny)) isvisible, isiluminated = [], [] r, lt = spiceypy.spkpos(observer, et, 'J2000', 'NONE', target) # # We define a 'Nadir frame' w.r.t. J000 to make it general regardless of # # zN = r zN = zN / np.linalg.norm(zN) xN = np.array( [1, 0, 0]) - np.dot(np.dot([1, 0, 0], zN), zN) / np.linalg.norm(zN)**2 xN = xN / np.linalg.norm(xN) yN = np.cross(zN, xN) yN = yN / np.linalg.norm(yN) RotM = np.linalg.inv(np.array([xN, yN, zN])) spoints = [] for i, x in enumerate(xv): for j, y in enumerate(yv): dpxy = [x[i], y[i], -np.linalg.norm(r) * 1000] ibsight = spiceypy.mxv(RotM, dpxy) # ibsight = [x[i], y[j], -r*1000] try: (spoint, trgepc, srfvec) = spiceypy.sincpt('DSK/UNPRIORITIZED', target, et, target_frame, 'NONE', observer, 'J2000', ibsight) spoints.append(spoint) (trgepc, srfvec, phase, solar, emissn, visiblef, iluminatedf) = spiceypy.illumf('DSK/UNPRIORITIZED', target, 'SUN', et, target_frame, 'NONE', observer, spoint) #check visibility of spoint if visiblef == True: isvisible.append(visiblef) if iluminatedf == True: isiluminated.append(iluminatedf) if solar > np.pi / 2: solar_matrix[i, j] = np.pi - solar else: solar_matrix[i, j] = solar else: solar_matrix[i, j] = np.pi / 2 # not illuminated emissn_matrix[i, j] = emissn phase_matrix[i, j] = phase except: pass emissn_matrix[i, j] = 0 phase_matrix[i, j] = math.pi solar_matrix[i, j] = np.pi / 2 spoints = np.asarray(spoints) fig = plt.figure(figsize=(9, 9)) ax = fig.add_subplot(111, projection='3d') ax.scatter(spoints[:, 0], spoints[:, 1], spoints[:, 2], marker='.') plt.xlabel("x position") plt.ylabel("y position") plt.title('') plt.axis('equal') plt.show() print('total number of points: ', pixels * pixels) print('occulted points: ', pixels * pixels - len(isvisible)) print('not iluminated points: ', pixels * pixels - len(isiluminated)) name = 'solar_matrix' try: os.remove(name) print('modifying png file' + name) except: print('generating png file' + name) plt.imshow(solar_matrix, cmap='viridis_r') plt.show() imsave(name + utcstr + '.png', solar_matrix) return
def compute_backplanes(file, name_target, frame, name_observer, angle1=0, angle2=0, angle3=0): """ Returns a set of backplanes for a single specified image. The image must have WCS coords available in its header. Backplanes include navigation info for every pixel, including RA, Dec, Eq Lon, Phase, etc. The results are returned to memory, and not written to a file. SPICE kernels must be alreaded loaded, and spiceypy running. Parameters ---- file: String. Input filename, for FITS file. frame: String. Reference frame of the target body. 'IAU_JUPITER', 'IAU_MU69', '2014_MU69_SUNFLOWER_ROT', etc. This is the frame that the Radius_eq and Longitude_eq are computed in. name_target: String. Name of the central body. All geometry is referenced relative to this (e.g., radius, azimuth, etc) name_observer: String. Name of the observer. Must be a SPICE body name (e.g., 'New Horizons') Optional Parameters ---- angle{1,2,3}: **NOT REALLY TESTED. THE BETTER WAY TO CHANGE THE ROTATION IS TO USE A DIFFERENT FRAME.** Rotation angles which are applied when defining the plane in space that the backplane will be generated for. These are applied in the order 1, 2, 3. Angles are in radians. Nominal values are 0. This allows the simulation of (e.g.) a ring system inclined relative to the nominal body equatorial plane. For MU69 sunflower rings, the following descriptions are roughly accurate, becuase the +Y axis points sunward, which is *almost* toward the observer. But it is better to experiment and find the appropriate angle that way, than rely on this ad hoc description. These are close for starting with. - `angle1` = Tilt front-back, from face-on. Or rotation angle, if tilted right-left. - `angle2` = Rotation angle, if tilted front-back. - `angle3` = Tilt right-left, from face-on. do_fast: Boolean. If set, generate only an abbreviated set of backplanes. **NOT CURRENTLY IMPLEMENTED.** Output ---- Output is a tuple, consisting of each of the backplanes, and a text description for each one. The size of each of these arrays is the same as the input image. The position of each of these is the plane defined by the target body, and the normal vector to the observer. output = (backplanes, descs) backplanes = (ra, dec, radius_eq, longitude_eq, phase) descs = (desc_ra, desc_dec, desc_radius_eq, desc_longitude_eq, desc_phase) Radius_eq: Radius, in the ring's equatorial plane, in km Longitude_eq: Longitude, in the equatorial plane, in radians (0 .. 2pi) RA: RA of pixel, in radians Dec: Dec of pixel, in dRA: Projected offset in RA direction between center of body (or barycenter) and pixel, in km. dDec: Projected offset in Dec direction between center of body (or barycenter) and pixel, in km. Z: Vertical value of the ring system. With special options selected (TBD), then additional backplanes will be generated -- e.g., a set of planes for each of the Jovian satellites in the image, or sunflower orbit, etc. No new FITS file is written. The only output is the returned tuple. """ if not (frame): raise ValueError('frame undefined') if not (name_target): raise ValueError('name_target undefined') if not (name_observer): raise ValueError('name_observer undefined') name_body = name_target # Sometimes we use one, sometimes the other. Both are identical fov_lorri = 0.3 * hbt.d2r abcorr = 'LT' do_satellites = False # Flag: Do we create an additional backplane for each of Jupiter's small sats? # Open the FITS file w = WCS( file ) # Warning: I have gotten a segfault here before if passing a FITS file with no WCS info. hdulist = fits.open(file) et = float(hdulist[0].header['SPCSCET']) # ET of mid-exposure, on s/c n_dx = int(hdulist[0].header['NAXIS1'] ) # Pixel dimensions of image. Both LORRI and MVIC have this. n_dy = int(hdulist[0].header['NAXIS2']) hdulist.close() # Setup the output arrays lon_arr = np.zeros( (n_dy, n_dx)) # Longitude of pixel (defined with recpgr) lat_arr = np.zeros( (n_dy, n_dx)) # Latitude of pixel (which is zero, so meaningless) radius_arr = np.zeros((n_dy, n_dx)) # Radius, in km altitude_arr = np.zeros((n_dy, n_dx)) # Altitude above midplane, in km ra_arr = np.zeros((n_dy, n_dx)) # RA of pixel dec_arr = np.zeros((n_dy, n_dx)) # Dec of pixel dra_arr = np.zeros( (n_dy, n_dx) ) # dRA of pixel: Distance in sky plane between pixel and body, in km. ddec_arr = np.zeros( (n_dy, n_dx) ) # dDec of pixel: Distance in sky plane between pixel and body, in km. phase_arr = np.zeros((n_dy, n_dx)) # Phase angle x_arr = np.zeros( (n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords y_arr = np.zeros( (n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords z_arr = np.zeros( (n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords # ============================================================================= # Do the backplane, in the general case. # This is a long routine, because we deal with all the satellites, the J-ring, etc. # ============================================================================= if (True): # Look up body parameters, used for PGRREC(). (num, radii) = sp.bodvrd(name_target, 'RADII', 3) r_e = radii[0] r_p = radii[2] flat = (r_e - r_p) / r_e # Define a SPICE 'plane' along the plane of the ring. # Do this in coordinate frame of the body (IAU_JUPITER, 2014_MU69_SUNFLOWER_ROT, etc). # ============================================================================= # Set up the Jupiter system specifics # ============================================================================= if (name_target.upper() == 'JUPITER'): plane_target_eq = sp.nvp2pl( [0, 0, 1], [0, 0, 0 ]) # nvp2pl: Normal Vec + Point to Plane. Jupiter north pole? # For Jupiter only, define a few more output arrays for the final backplane set ang_metis_arr = np.zeros( (n_dy, n_dx)) # Angle from pixel to body, in radians ang_adrastea_arr = ang_metis_arr.copy() ang_thebe_arr = ang_metis_arr.copy() ang_amalthea_arr = ang_metis_arr.copy() # ============================================================================= # Set up the MU69 specifics # ============================================================================= if ('MU69' in name_target.upper()): # Define a plane, which is the plane of sunflower rings (ie, X-Z plane in Sunflower frame) # If additional angles are passed, then create an Euler matrix which will do additional angles of rotation. # This is defined in the 'MU69_SUNFLOWER' frame vec_plane = [0, 1, 0] # Use +Y (anti-sun dir), which is normal to XZ. # vec_plane = [0, 0, 1] # Use +Y (anti-sun dir), which is normal to XZ. plane_target_eq = sp.nvp2pl( vec_plane, [0, 0, 0]) # "Normal Vec + Point to Plane". 0,0,0 = origin. # XXX NB: This plane in in body coords, not J2K coords. This is what we want, because the # target intercept calculation is also done in body coords. # ============================================================================= # Set up the various output planes and arrays necessary for computation # ============================================================================= # Get xformation matrix from J2K to target system coords. I can use this for points *or* vectors. mx_j2k_frame = sp.pxform('J2000', frame, et) # from, to, et # Get vec from body to s/c, in both body frame, and J2K. # NB: The suffix _j2k indicates j2K frame. _frame indicates the frame of target (IAU_JUP, MU69_SUNFLOWER, etc) (st_target_sc_frame, lt) = sp.spkezr(name_observer, et, frame, abcorr, name_target) (st_sc_target_frame, lt) = sp.spkezr(name_target, et, frame, abcorr, name_observer) (st_target_sc_j2k, lt) = sp.spkezr(name_observer, et, 'J2000', abcorr, name_target) (st_sc_target_j2k, lt) = sp.spkezr(name_target, et, 'J2000', abcorr, name_observer) vec_target_sc_frame = st_target_sc_frame[0:3] vec_sc_target_frame = st_sc_target_frame[0:3] vec_target_sc_j2k = st_target_sc_j2k[0:3] vec_sc_target_j2k = st_sc_target_j2k[0:3] dist_target_sc = sp.vnorm( vec_target_sc_j2k) # Get target distance, in km # vec_sc_target_frame = -vec_target_sc_frame # ACTUALLY THIS IS NOT TRUE!! ONLY TRUE IF ABCORR=NONE. # Name this vector a 'point'. INRYPL requires a point argument. pt_target_sc_frame = vec_target_sc_frame # Look up RA and Dec of target (from sc), in J2K (_, ra_sc_target, dec_sc_target) = sp.recrad(vec_sc_target_j2k) # Get vector from target to sun. We use this later for phase angle. (st_target_sun_frame, lt) = sp.spkezr('Sun', et, frame, abcorr, name_target) # From body to Sun, in body frame vec_target_sun_frame = st_target_sun_frame[0:3] # Create a 2D array of RA and Dec points # These are made by WCS, so they are guaranteed to be right. xs = range(n_dx) ys = range(n_dy) (i_x_2d, i_y_2d) = np.meshgrid(xs, ys) (ra_arr, dec_arr) = w.wcs_pix2world(i_x_2d, i_y_2d, False) # Returns in degrees ra_arr *= hbt.d2r # Convert to radians dec_arr *= hbt.d2r # Compute the projected distance from MU69, in the sky plane, in km, for each pixel. # dist_target_sc is the distance to MU69, and we use this to convert from radians, to km. # 16-Oct-2018. I had been computing this erroneously. It should be *cosdec, not /cosdec. dra_arr = (ra_arr - ra_sc_target) * dist_target_sc * np.cos(dec_arr) ddec_arr = (dec_arr - dec_sc_target) * dist_target_sc # Convert to km # ============================================================================= # Compute position for additional Jupiter bodies, as needed # ============================================================================= if (name_target.upper() == 'JUPITER'): vec_metis_j2k, lt = sp.spkezr('Metis', et, 'J2000', abcorr, 'New Horizons') vec_adrastea_j2k, lt = sp.spkezr('Adrastea', et, 'J2000', abcorr, 'New Horizons') vec_thebe_j2k, lt = sp.spkezr('Thebe', et, 'J2000', abcorr, 'New Horizons') vec_amalthea_j2k, lt = sp.spkezr('Amalthea', et, 'J2000', abcorr, 'New Horizons') vec_metis_j2k = np.array(vec_metis_j2k[0:3]) vec_thebe_j2k = np.array(vec_thebe_j2k[0:3]) vec_adrastea_j2k = np.array(vec_adrastea_j2k[0:3]) vec_amalthea_j2k = np.array(vec_amalthea_j2k[0:3]) # ============================================================================= # Loop over pixels in the output image # ============================================================================= for i_x in xs: for i_y in ys: # Look up the vector direction of this single pixel, which is defined by an RA and Dec # Vector is thru pixel to ring, in J2K. # RA and Dec grids are made by WCS, so they are guaranteed to be right. vec_pix_j2k = sp.radrec(1., ra_arr[i_y, i_x], dec_arr[i_y, i_x]) # Convert vector along the pixel direction, from J2K into the target body frame vec_pix_frame = sp.mxv(mx_j2k_frame, vec_pix_j2k) # And calculate the intercept point between this vector, and the ring plane. # All these are in body coordinates. # plane_target_eq is defined as the body's equatorial plane (its XZ for MU69). # ** Some question as to whether we should shoot this vector at the ring plane, or the sky plane. # Ring plane is normally the one we want. But, for the case of edge-on rings, the eq's break down. # So, we should use the sky plane instead. Testing shows that for the case of MU69 Eq's break down # for edge-on rings... there is always an ambiguity. # ** For testing, try intersecting the sky plane instead of the ring plane. # ** Confirmed: Using sky plane gives identical results in case of face-on rings. # And it gives meaningful results in case of edge-on rings, where ring plane did not. # However, for normal rings (e.g., Jupiter), we should continue using the ring plane, not sky plane. do_sky_plane = True # For ORT4, where we want to use euler angles, need to set this to False if do_sky_plane and ('MU69' in name_target): plane_sky_frame = sp.nvp2pl( vec_sc_target_frame, [0, 0, 0]) # Frame normal to s/c vec, cntrd on MU69 (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_sky_frame) # pt_intersect_frame is the point where the ray hits the skyplane, in the coordinate frame # of the target body. else: # Calc intersect into equator of target plane (ie, ring plane) (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_target_eq) # pt, vec, plane # Swap axes in target frame if needed. # In the case of MU69 (both sunflower and tunacan), the frame is defined s.t. the ring # is in the XZ plane, not XY. This is strange (but correct). # I bet MU69 is the only ring like this. Swap it so that Z means 'vertical, out of plane' -- # that is, put it into normal XYZ rectangular coords, so we can use RECLAT etc on it. if ( 'MU69' in name_target ): # Was 0 2 1. But this makes tunacan radius look in wrong dir. # 201 looks same # 210 similar # 201 similar # 102, 120 similar. # ** None of these change orientation of 'radius' backplane. OK. pt_intersect_frame = np.array([ pt_intersect_frame[0], pt_intersect_frame[2], pt_intersect_frame[1] ]) # Get the radius and azimuth of the intersect, in the ring plane # Q: Why for the TUNACAN is the radius zero here along horizontal (see plot)? # A: Ahh, it is not zero. It is just that the 'projected radius' of a ring that is nearly edge-on # can be huge! Basically, if we try to calc the intersection with that plane, it will give screwy # answers, because the plane is so close to edge-on that intersection could be a long way # from body itself. # Instead, I really want to take the tangent sky plane, intersect that, and then calc the # position of that (in xyz, radius, longitude, etc). # Since that plane is fixed, I don't see a disadvantage to doing that. # We want the 'radius' to be the radius in the equatorial plane -- that is, sqrt(x^2 + y^2). # We don't want it to be the 'SPICE radius', which is the distance. # (For MU69 equatorial plane is nominally XZ, but we have already changed that above to XY.) _radius_3d, lon, lat = sp.reclat(pt_intersect_frame) radius_eq = sp.vnorm( [pt_intersect_frame[0], pt_intersect_frame[1], 0]) # radius_eq = sp.vnorm([pt_intersect_frame[0], pt_intersect_frame[1], pt_intersect_frame[2]]) # Get the vertical position (altitude) altitude = pt_intersect_frame[2] # Calculate the phase angle: angle between s/c-to-ring, and ring-to-sun vec_ring_sun_frame = -pt_intersect_frame + vec_target_sun_frame angle_phase = sp.vsep(-vec_pix_frame, vec_ring_sun_frame) # Save various derived quantities radius_arr[i_y, i_x] = radius_eq lon_arr[i_y, i_x] = lon phase_arr[i_y, i_x] = angle_phase altitude_arr[i_y, i_x] = altitude # Save these just for debugging x_arr[i_y, i_x] = pt_intersect_frame[0] y_arr[i_y, i_x] = pt_intersect_frame[1] z_arr[i_y, i_x] = pt_intersect_frame[2] # Now calc angular separation between this pixel, and the satellites in our list # Since these are huge arrays, cast into floats to make sure they are not doubles. if (name_body.upper() == 'JUPITER'): ang_thebe_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_thebe_j2k) ang_adrastea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_adrastea_j2k) ang_metis_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_metis_j2k) ang_amalthea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_amalthea_j2k) # Now, fix a bug. The issue is that SP.INRYPL uses the actual location of the bodies (no aberration), # while their position is calculated (as it should be) with abcorr=LT. This causes a small error in the # positions based on the INRYPL calculation. This should probably be fixed above, but it was not # obvious how. So, instead, I am fixing it here, by doing a small manual offset. # Calculate the shift required, by measuring the position of MU69 with abcorr=NONE, and comparing it to # the existing calculation, that uses abcorr=LT. This is brute force, but it works. For MU69 approach, # it is 0.75 LORRI 4x4 pixels (ie, 3 1X1 pixels). This is bafflingly huge (I mean, we are headed # straight toward MU69, and it takes a month to move a pixel, and RTLT is only a few minutes). But I have # confirmed the math and the magnitude, and it works. (st_sc_target_j2k_nolt, _) = sp.spkezr(name_target, et, 'J2000', 'NONE', name_observer) vec_sc_target_j2k_nolt = st_sc_target_j2k_nolt[0:3] (_, ra_sc_target_nolt, dec_sc_target_nolt) = sp.recrad(vec_sc_target_j2k_nolt) (x0, y0) = w.wcs_world2pix(ra_sc_target_nolt * hbt.r2d, dec_sc_target_nolt * hbt.r2d, 1) (x1, y1) = w.wcs_world2pix(ra_sc_target * hbt.r2d, dec_sc_target * hbt.r2d, 1) dx = x1 - x0 dy = y1 - y0 print(f'Compute backplanes: INRYPL pixel shift = {dx}, {dy}') dx_int = int(round(dx)) dy_int = int(round(dy)) do_roll = True if do_roll: print( f'compute_backplanes: Rolling by {dx_int}, {dy_int} due to INRYPL' ) # Now shift all of the planes that need fixing. The dRA_km and dDec_km are calculated before INRYPL() # is applied, so they do not need to be shifted. I have validated that by plotting them. # # XXX NP.ROLL() is really not ideal. I should use a function that introduces NaN at the edge, not roll it. radius_arr = np.roll(np.roll(radius_arr, dy_int, axis=0), dx_int, axis=1) lon_arr = np.roll(np.roll(lon_arr, dy_int, axis=0), dx_int, axis=1) phase_arr = np.roll(np.roll(phase_arr, dy_int, axis=0), dx_int, axis=1) altitude_arr = np.roll(np.roll(altitude_arr, dy_int, axis=0), dx_int, axis=1) else: print( f'compute_backplanes: Skipping roll due to INRYPL, based on do_roll={do_roll}' ) # Assemble the results into a backplane backplane = { 'RA': ra_arr.astype(float), # return radians 'Dec': dec_arr.astype(float), # return radians 'dRA_km': dra_arr.astype(float), 'dDec_km': ddec_arr.astype(float), 'Radius_eq': radius_arr.astype(float), 'Longitude_eq': lon_arr.astype(float), 'Phase': phase_arr.astype(float), 'Altitude_eq': altitude_arr.astype(float), # 'x' : x_arr.astype(float), # 'y' : y_arr.astype(float), # 'z' : z_arr.astype(float), # } # Assemble a bunch of descriptors, to be put into the FITS headers desc = { 'RA of pixel, radians', 'Dec of pixel, radians', 'Offset from target in target plane, RA direction, km', 'Offset from target in target plane, Dec direction, km', 'Projected equatorial radius, km', 'Projected equatorial longitude, km', 'Sun-target-observer phase angle, radians', 'Altitude above midplane, km', # 'X position of sky plane intercept', # 'Y position of sky plane intercept', # 'Z position of sky plane intercept' } # In the case of Jupiter, add a few extra fields if (name_body.upper() == 'JUPITER'): backplane['Ang_Thebe'] = ang_thebe_arr.astype( float) # Angle to Thebe, in radians backplane['Ang_Metis'] = ang_metis_arr.astype(float) backplane['Ang_Amalthea'] = ang_amalthea_arr.astype(float) backplane['Ang_Adrastea'] = ang_adrastea_arr.astype(float) # If distance to any of the small sats is < 0.3 deg, then delete that entry in the dictionary if (np.amin(ang_thebe_arr) > fov_lorri): del backplane['Ang_Thebe'] else: print("Keeping Thebe".format(np.min(ang_thebe_arr) * hbt.r2d)) if (np.amin(ang_metis_arr) > fov_lorri): del backplane['Ang_Metis'] else: print("Keeping Metis, min = {} deg".format( np.min(ang_metis_arr) * hbt.r2d)) if (np.amin(ang_amalthea_arr) > fov_lorri): del backplane['Ang_Amalthea'] else: print("Keeping Amalthea, min = {} deg".format( np.amin(ang_amalthea_arr) * hbt.r2d)) if (np.amin(ang_adrastea_arr) > fov_lorri): del backplane['Ang_Adrastea'] else: print("Keeping Adrastea".format( np.min(ang_adrastea_arr) * hbt.r2d)) # And return the backplane set return (backplane, desc)
def get_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
def transform_frame(t, data, frame_from, frame_to, frame_cadence=None): """Transform 3D coordinates from one reference frame to another. Parameters ---------- t : list[datetime.datetime] Evaluation datetimes. data : np.ndarray 3D vector array. frame_from : str Source refernce frame. frame_to : str Target reference frame. frame_cadence: float, optional Evaluate frame transformation matrix every "frame_cadence" seconds instead of at very time point (significant speed up), by default None. Returns ------- np.ndarray Transformed vector array in target reference frame. Raises ------ ValueError If data array has the wrong dimensions (must be 2d or 3d) or source and target frame are equal. """ if frame_from == frame_to: raise ValueError("source frame and target frame are equal") # convert timestamps to python datetimes if required if not isinstance(t[0], datetime.datetime): t = [datetime.datetime.fromtimestamp(_t) for _t in t] if frame_cadence: frames = int((t[-1] - t[0]).total_seconds() // frame_cadence) frame_indices = [np.floor(_) for _ in np.linspace(0, frames, len(t), endpoint=False)] time_indices = np.linspace(0, len(t), frames, endpoint=False) frames = [spiceypy.pxform(frame_from, frame_to, spiceypy.datetime2et(t[int(i)])) for i in time_indices] else: frames = None frame_indices = None if data.ndim == 2: if frame_cadence: for i in range(0, len(t)): data[i] = spiceypy.mxv(frames[int(frame_indices[i])], data[i]) else: for i in range(0, len(t)): data[i] = spiceypy.mxv(spiceypy.pxform(frame_from, frame_to, spiceypy.datetime2et(t[i])), data[i]) return data elif data.ndim == 3: max_workers = min(multiprocessing.cpu_count() * 2, data.shape[1]) kernels = heliosat._spice["kernels_loaded"] with ProcessPoolExecutor(max_workers=max_workers) as executor: args = [(t, data[:, i], frame_from, frame_to, frames, frame_indices, kernels) for i in range(data.shape[1])] futures = executor.map(worker_transform_frame, args) result = np.array([_ for _ in futures]) return np.swapaxes(result, 0, 1) else: raise ValueError("data array can only be 2 or 3 dimensional")
# Get the distance from Jupiter center to image center vec_nh_center = sp.radrec(1, radec_center[0][0], radec_center[0][1]) # Vector from NH, to center of LORRI frame ang_jup_center = sp.vsep(vec_nh_jup, vec_nh_center) # Ang Sep btwn Jup and LORRI, radians dist_jup_center_rj = ang_jup_center * sp.vnorm(vec_nh_jup) / rj_km # Convert from radians into RJ width_lorri = 0.3*hbt.d2r # LORRI full width, radians dist_jup_center_rj_range = np.array([ang_jup_center-width_lorri/2, ang_jup_center+width_lorri/2]) * \ sp.vnorm(vec_nh_jup) / rj_km # Finally, we have min and max dist # in the central row of array, in rj range_rj_arr.append(sp.vnorm(vec_nh_jup)/rj_km) # Calc the elevation angle (aka sub-obs lat on Jup) mx = sp.pxform('J2000', 'IAU_JUPITER', t_i['ET']) vec_nh_jup_jup = sp.mxv(mx, vec_nh_jup) (dist, lon, lat) = sp.recrad(-vec_nh_jup_jup) angle_elev_arr.append(lat * hbt.r2d) # Save the sub-obs latitude, in degrees # If we are on the LHS limb, then we need to reverse this, and negate it. if (is_limb_left): dist_jup_center_rj_range = -1 * dist_jup_center_rj_range[::-1] dist_proj_rj_arr.append((dist_jup_center_rj_range)) # Get the distance above the ring plane -- that is, distance from image center to ring plane, projected. # To do this: # - Project a vector from s/c along central LORRI pixel. # - Make a SPICE 'line' from s/c along this vector
def cassini_titan_test(flyby, anodes=False): times = [] states = [] lons, lats, alts = [], [], [] crossvecs_lonlatalts = [] crossvecs_lonlatalts_spicenormal = [] cmats = [] vecs = [] anode_vecs = [] anode_seps = [[], [], [], [], [], [], [], []] anodes1, anodes8 = [], [] crossvecs = [] angularseparations = [] beamanodes = [] spiceplanenormals = [] windsdf = pd.read_csv("crosswinds_full.csv", index_col=0, parse_dates=True) tempdf = windsdf[windsdf['Flyby'] == flyby] for tempdatetime, negwindspeed, poswindspeed in zip( pd.to_datetime(tempdf['Bulk Time']), tempdf["Negative crosstrack velocity"], tempdf["Positive crosstrack velocity"]): print("---------") print(tempdatetime) times.append(tempdatetime) beamanodes.append(np.mean(ELS_ramanodes(tempdatetime)) + 1) states.append(cassini_phase( tempdatetime.strftime('%Y-%m-%dT%H:%M:%S'))) # print(states[-1]) lon, lat, alt = spice.recpgr("TITAN", states[-1][:3], spice.bodvrd("TITAN", 'RADII', 3)[1][0], 1.44e-4) lons.append(lon * spice.dpr()) lats.append(lat * spice.dpr()) alts.append(alt) # vecs.append(cassini_act_2_titan(tempdatetime)) crossvec = caps_crosstrack(tempdatetime, np.mean([negwindspeed, poswindspeed])) print("crossvec", crossvec) testspicenormal, anode1, anode8 = caps_crosstrack_spice( tempdatetime, np.mean([negwindspeed, poswindspeed])) anodes1.append(anode1) anodes8.append(anode8) spiceplanenormals.append(testspicenormal) print("test spice normal", testspicenormal) jacobian = spice.dpgrdr("TITAN", states[-1][0], states[-1][1], states[-1][2], spice.bodvrd('TITAN', 'RADII', 3)[1][0], 1.44e-4) # print("jacobian", jacobian) crossvec_lonlatalt = spice.mxv(jacobian, spice.vhat(crossvec)) crossvec_lonlatalt_spicenormal = spice.mxv(jacobian, testspicenormal) # print("recpgr", lon, lat, alt) # print("crossvec latlon", crossvec_lonlatalt) # print("crossvec latlon vhat", spice.vhat(crossvec_latlon)) crossvecs.append(crossvec) crossvecs_lonlatalts.append(crossvec_lonlatalt) crossvecs_lonlatalts_spicenormal.append(crossvec_lonlatalt_spicenormal) # print("Time", tempdatetime) # print("position", states[-1][:3]) # print("velocity", spice.vhat(states[-1][3:])) # print("direction", spice.vhat(vecs[-1])) # if anodes: # anode_vecs.append(caps_all_anodes(tempdatetime)) # print("anode vecs 1 & 8", anode_vecs[-1][0], anode_vecs[-1][7]) # # spiceplanenormal = spice.psv2pl(states[-1][:3],anode_vecs[-1][0],anode_vecs[-1][7]) # # print("SPICE NORMAL", spice.pl2nvp(spiceplanenormal)) # # # # spiceplanenormals.append(-1*spice.pl2nvp(spiceplanenormal)[0]) # # print("Crossvec", crossvec) # for anodecounter, i in enumerate(anode_vecs[-1]): # # print(anodecounter,anode_vecs[-1][anodecounter]) # anode_seps[anodecounter].append( # spice.vsep(spice.vhat(states[-1][3:]), spice.vhat(anode_vecs[-1][anodecounter])) * spice.dpr()) # print("anodeseps",anode_seps) # print("Angular Separation", spice.vsep(spice.vhat(states[-1][3:]), spice.vhat(vecs[-1])) * spice.dpr()) x, y, z, u, v, w = [], [], [], [], [], [] for i in states: x.append(i[0]) y.append(i[1]) z.append(i[2]) # CAPS direction for i in vecs: u.append(i[0]) v.append(i[1]) w.append(i[2]) # Crosstrack u2, v2, w2 = [], [], [] for j in crossvecs: u2.append(j[0]) v2.append(j[1]) w2.append(j[2]) # SPICE plane normal u3, v3, w3 = [], [], [] for j in spiceplanenormals: u3.append(j[0]) v3.append(j[1]) w3.append(j[2]) # Ram Direction u1, v1, w1 = [], [], [] for i in states: u1.append(i[3]) v1.append(i[4]) w1.append(i[5]) fig = plt.figure() u = np.linspace(0, 2 * np.pi, 50) v = np.linspace(0, np.pi, 50) x_sphere = 2574.7 * np.outer(np.cos(u), np.sin(v)) y_sphere = 2574.7 * np.outer(np.sin(u), np.sin(v)) z_sphere = 2574.7 * np.outer(np.ones(np.size(u)), np.cos(v)) ax = fig.add_subplot(111, projection='3d') # Plot the surface # ax.plot_wireframe(x_sphere, y_sphere, z_sphere, color='b') # ax.plot(x, y, z, alpha=0.5, color='k') if anodes: for timecounter, (i, j) in enumerate(zip(anodes1, anodes8)): X = x[timecounter] Y = y[timecounter] Z = z[timecounter] # print(i) # for anodecounter, j in enumerate(i): # if anodecounter in [0, 7]: # ax.quiver(X, Y, Z, j[0], j[1], j[2], length=20, color='C' + str(anodecounter)) # print(timecounter, i, j) ax.quiver(X, Y, Z, i[0], i[1], i[2], length=30, color='C1') ax.quiver(X, Y, Z, j[0], j[1], j[2], length=30, color='C2') ax.quiver(x, y, z, u2, v2, w2, length=30, color='m') ax.quiver(x, y, z, u1, v1, w1, length=5, color='k') ax.quiver(x, y, z, u3, v3, w3, length=30, color='r') ax.set_xlabel("X") ax.set_ylabel("Y") ax.set_zlabel("Z") ax.set_xlim(min(x), max(x)) ax.set_ylim(min(y), max(y)) ax.set_zlim(min(z), max(z)) dlat, dlon = [], [] for i in crossvecs_lonlatalts: dlat.append(i[1]) dlon.append(i[0]) dlat_spicenormal, dlon_spicenormal = [], [] for i in crossvecs_lonlatalts_spicenormal: dlat_spicenormal.append(i[1]) dlon_spicenormal.append(i[0]) fig2, ax2 = plt.subplots() ax2.plot(lons, lats) ax2.quiver(lons, lats, dlon, dlat) ax2.quiver(lons, lats, dlon_spicenormal, dlat_spicenormal, color='r') ax2.set_xlabel("Longitude") ax2.set_ylabel("Latitude") ax2.grid()
def caps_crosstrack_spice(tempdatetime, windspeed): et = spice.datetime2et(tempdatetime) sclkdp = spice.sce2c( -82, et) # converts an et to a continuous encoded sc clock (ticks) state, ltime = spice.spkezr("CASSINI", et, "IAU_TITAN", "NONE", "titan") ramdir = spice.vhat(state[3:6]) # print("ramdir",ramdir) # Gets Attitude sclkdp = spice.sce2c( -82, et) # converts an et to a continuous encoded sc clock (ticks) ckgp_output = spice.ckgp(-82000, sclkdp, 0, "IAU_TITAN") cmat = ckgp_output[0] print("cmat", cmat) ram_unit = spice.mxv(cmat, ramdir) # Ram Unit in SC coords # print("ram_unit", ram_unit) anglediff = spice.vsepg( ram_unit[:2], np.array([0, 1, 0]), 2) # Find azimuthal angle between normal boresight and ram direction # print("anglediff", anglediff * spice.dpr()) cassini_ram_mat = spice.rotate(-anglediff, 3) # print("cassini_ram_mat", cassini_ram_mat) # Rotates rotational axis with actuation # cassini_caps_mat = spice.ckgp(-82821, sclkdp, 0, 'CASSINI_CAPS_BASE')[0] # Rotation matrix of actuation # print("cassini_caps_mat", cassini_caps_mat) anode_rotational_axis = spice.mxv(cassini_ram_mat, np.array([1, 0, 0])) # Rotate with actuator print("Rotational Axis", anode_rotational_axis) rotationmatrix_1 = spice.spiceypy.axisar(anode_rotational_axis, -70 * spice.rpd()) rotationmatrix_2 = spice.spiceypy.axisar(anode_rotational_axis, 70 * spice.rpd()) ram_unit_rotated1 = spice.mxv(rotationmatrix_1, ram_unit) ram_unit_rotated2 = spice.mxv(rotationmatrix_2, ram_unit) scframe_spiceplane = spice.psv2pl([0, 0, 0], ram_unit_rotated1, ram_unit_rotated2) print("ram_unit", ram_unit, ram_unit_rotated1, ram_unit_rotated2) print("SC frame spice normal", spice.psv2pl([0, 0, 0], ram_unit_rotated1, ram_unit_rotated2)) cmat_t = spice.xpose(cmat) ram_unit_rotated1_titan = spice.mxv( cmat_t, ram_unit_rotated1) # Transform back to IAU Titan Frame ram_unit_rotated2_titan = spice.mxv( cmat_t, ram_unit_rotated2) # Transform back to IAU Titan Frame spiceplanenormal = spice.mxv(cmat_t, spice.pl2nvp(scframe_spiceplane)[0]) # Old method finding normal in titan frame # spiceplane = spice.psv2pl(state[:3], ram_unit_rotated1_titan, ram_unit_rotated2_titan) # spiceplanenormal = spice.pl2nvp(spiceplane)[0] print("SPICE NORMAL", spiceplanenormal) # print("Spice normal, sc frame", scframe_spicenormal_titan) if windspeed > 0: spiceplanenormal = -1 * spiceplanenormal print("spice plane fipped", windspeed, spiceplanenormal) print("vsep titan frame", spice.vsep(ramdir, spiceplanenormal) * spice.dpr()) return spiceplanenormal, ram_unit_rotated1_titan, ram_unit_rotated2_titan
def __Geometry(self, boresight=''): #if self.geometry_flag is True and \ # self.time.window.all() == self.previous_tw.all(): # return distance = [] altitude = [] boresight_latitude = [] boresight_longitude = [] latitude = [] longitude = [] subpoint_xyz = [] subpoint_pgc = [] subpoint_pcc = [] zaxis_target_angle = [] myaxis_target_angle = [] yaxis_target_angle = [] xaxis_target_angle = [] beta_angle = [] qs, qx, qy, qz = [], [], [] ,[] x, y, z = [],[],[] tar = self.target time = self.time for et in time.window: try: # # Compute the distance # ptarg, lt = spiceypy.spkpos(tar.name, et, tar.frame, time.abcorr, self.name) x.append(ptarg[0]) y.append(ptarg[1]) z.append(ptarg[2]) vout, vmag = spiceypy.unorm(ptarg) distance.append(vmag) # # Compute the geometric sub-observer point. # if tar.frame == 'MARSIAU': tar_frame = 'IAU_MARS' else: tar_frame = tar.frame spoint, trgepc, srfvec = spiceypy.subpnt(tar.method, tar.name, et, tar_frame, time.abcorr, self.name) subpoint_xyz.append(spoint) # # Compute the observer's altitude from SPOINT. # dist = spiceypy.vnorm(srfvec) altitude.append(dist) # # Convert the sub-observer point's rectangular coordinates to # planetographic longitude, latitude and altitude. # spglon, spglat, spgalt = spiceypy.recpgr(tar.name, spoint, tar.radii_equ, tar.flat) # # Convert radians to degrees. # spglon *= spiceypy.dpr() spglat *= spiceypy.dpr() subpoint_pgc.append([spglon, spglat, spgalt]) # # Convert sub-observer point's rectangular coordinates to # planetocentric radius, longitude, and latitude. # spcrad, spclon, spclat = spiceypy.reclat(spoint) # # Convert radians to degrees. # spclon *= spiceypy.dpr() spclat *= spiceypy.dpr() subpoint_pcc.append([spclon, spclat, spcrad]) latitude.append(spclat) #TODO: Remove with list extraction longitude.append(spclon) # TODO: Remove with list extraction # # Compute the geometric sub-boresight point. # if tar.frame == 'MARSIAU': tar_frame = 'IAU_MARS' else: tar_frame = tar.frame if boresight: try: id = spiceypy.bodn2c(boresight) (shape,framen, bsight, n, bounds) = spiceypy.getfov(id, 80) mat = spiceypy.pxform(framen,tar_frame,et) except: framen = boresight bsight = 0,0,1 else: bsight = self.name try: if tar.method == 'INTERCEPT/ELLIPSOID': method = 'ELLIPSOID' else: method = tar.method spoint, trgepc, srfvec = spiceypy.sincpt(method, tar.name, et, tar_frame, time.abcorr, self.name, framen, bsight) # # Convert the sub-observer point's rectangular coordinates to # planetographic longitude, latitude and altitude. # spglon, spglat, spgalt = spiceypy.recpgr(tar.name, spoint, tar.radii_equ, tar.flat) # # Convert radians to degrees. # spglon *= spiceypy.dpr() spglat *= spiceypy.dpr() # # Convert sub-observer point's rectangular coordinates to # planetocentric radius, longitude, and latitude. # spcrad, spclon, spclat = spiceypy.reclat(spoint) # # Convert radians to degrees. # spclon *= spiceypy.dpr() spclat *= spiceypy.dpr() boresight_latitude.append(spclat) boresight_longitude.append(spclon) except: pass # # Compute the angle between the observer's S/C axis and the # geometric sub-observer point # obs_tar, ltime = spiceypy.spkpos(tar.name, et, 'J2000', time.abcorr, self.name) obs_zaxis = [0, 0, 1] obs_myaxis = [0, -1, 0] obs_yaxis = [0, 1, 0] obs_xaxis = [1, 0, 0] # # We need to account for when there is no CK attitude available. # try: matrix = spiceypy.pxform(self.frame, 'J2000', et) z_vecout = spiceypy.mxv(matrix, obs_zaxis) zax_target_angle = spiceypy.vsep(z_vecout, obs_tar) zax_target_angle *= spiceypy.dpr() zaxis_target_angle.append(zax_target_angle) my_vecout = spiceypy.mxv(matrix, obs_myaxis) myax_target_angle = spiceypy.vsep(my_vecout, obs_tar) myax_target_angle *= spiceypy.dpr() myaxis_target_angle.append(myax_target_angle) y_vecout = spiceypy.mxv(matrix, obs_myaxis) yax_target_angle = spiceypy.vsep(y_vecout, obs_tar) yax_target_angle *= spiceypy.dpr() yaxis_target_angle.append(yax_target_angle) x_vecout = spiceypy.mxv(matrix, obs_myaxis) xax_target_angle = spiceypy.vsep(x_vecout, obs_tar) xax_target_angle *= spiceypy.dpr() xaxis_target_angle.append(xax_target_angle) quat = spiceypy.m2q(spiceypy.invert(matrix)) qs.append(quat[0]) qx.append(-1*quat[1]) qy.append(-1*quat[2]) qz.append(-1*quat[3]) except: zaxis_target_angle.append(0.0) myaxis_target_angle.append(0.0) yaxis_target_angle.append(0.0) xaxis_target_angle.append(0.0) qs.append(0.0) qx.append(0.0) qy.append(0.0) qz.append(0.0) beta_angle.append(spiops.beta_angle(self.name, self.target.name, et)) except: boresight_latitude = 0 boresight_longitude = 0 distance = 0 altitude = 0 latitude = 0 longitude = 0 subpoint_xyz = [0,0,0] subpoint_pgc = [0,0,0] subpoint_pcc = [0,0,0] zaxis_target_angle = 0 myaxis_target_angle = 0 yaxis_target_angle = 0 xaxis_target_angle = 0 beta_angle = 0 (qx, qy, qz, qs) = 0, 0, 0, 0 (x, y, z) = 0, 0, 0 self.boresight_latitude = boresight_latitude self.boresight_longitude = boresight_longitude self.distance = distance self.altitude = altitude self.latitude = latitude self.longitude = longitude self.subpoint_xyz = subpoint_xyz self.subpoint_pgc = subpoint_pgc self.subpoint_pcc = subpoint_pcc self.zaxis_target_angle = zaxis_target_angle self.myaxis_target_angle = myaxis_target_angle self.yaxis_target_angle = yaxis_target_angle self.xaxis_target_angle = xaxis_target_angle self.beta_angle = beta_angle self.quaternions = [qx, qy, qz, qs] self.trajectory = [x,y,z] self.geometry_flag = True self.previous_tw = self.time.window return
def test_frame_mu69_sunflower(): """ This is just a quick test routine to check that MU69 Sunflower frame is more-or-less working. This program prints the RA/Dec of MU69's Y and Z axes, under a variety of different conditions. No options. Things to verify here: - That the Z RA/Dec point toward the orbit pole RA/Dec in all cases. Small deviations << 1 deg allowed - That the Y RA/Dec point toward the Sun always for _ROT Frames. (ie, Y RA/Dec changes with time.) - That the Y RA/Dec point toward the Sun for the _INERT frames on 1 Jan 2015, and slowly move after / before that, at roughly 1 deg/year. (ie, Y RA/Dec is pretty much fixed) 16-Jan-2018. HBT verified that output values look pretty good. """ tms = ['kernels_sunflower.tm'] # Define the metakernel, which in turn calls the sunflower .tf frame frames = ['2014_MU69_SUNFLOWER_ROT', '2014_MU69_SUNFLOWER_INERT'] utcs = ['1 Jan 2005 00:00:00', '1 Jan 2015 00:00:00'] frame_j2k = 'J2000' # Get values from Simon Porter. See email from MRS ~16-Jan-2018. # These RA/Dec values are also put into the .tf file which I have made. ra_mu69_sp = 272.426110231801*hbt.d2r dec_mu69_sp = 68.831520928192*hbt.d2r # Define the MU69 Z axis. We will rotate this, and it should point in specified direction. z_mu69 = [0, 0, 1] # MU69 +Z axis. It should point to the specified RA/Dec y_mu69 = [0, 1, 0] # MU69 +Y axis. It should point away from the Sun print("Simon Porter pole position:") print("RA = {}, Dec = {}".format(ra_mu69_sp*hbt.r2d, dec_mu69_sp*hbt.r2d)) print("---") # Loop over input parameters. For each combination, do a calculation, and print the output. for tm in tms: for frame in frames: for utc in utcs: sp.furnsh(tm) et = sp.utc2et(utc) mx = sp.pxform(frame, frame_j2k, et) z_mu69_j2k = sp.mxv(mx, z_mu69) (_, ra_z, dec_z) = sp.recrad(z_mu69_j2k) y_mu69_j2k = sp.mxv(mx, y_mu69) (_, ra_y, dec_y) = sp.recrad(y_mu69_j2k) print("Metakernel: {}".format(tm)) print("UTC: {}".format(utc)) print("Frame: {}".format(frame)) print("Matrix: \n{}".format(mx)) print("Y RA = {}, Dec = {}".format(ra_y*hbt.r2d, dec_y*hbt.r2d)) print("Z RA = {}, Dec = {}".format(ra_z*hbt.r2d, dec_z*hbt.r2d)) print("\n---\n")
def compute_backplanes(file, name_target, frame, name_observer, angle1=0, angle2=0, angle3=0): """ Returns a set of backplanes for a single specified image. The image must have WCS coords available in its header. Backplanes include navigation info for every pixel, including RA, Dec, Eq Lon, Phase, etc. The results are returned to memory, and not written to a file. SPICE kernels must be alreaded loaded, and spiceypy running. Parameters ---- file: String. Input filename, for FITS file. frame: String. Reference frame of the target body. 'IAU_JUPITER', 'IAU_MU69', '2014_MU69_SUNFLOWER_ROT', etc. This is the frame that the Radius_eq and Longitude_eq are computed in. name_target: String. Name of the central body. All geometry is referenced relative to this (e.g., radius, azimuth, etc) name_observer: String. Name of the observer. Must be a SPICE body name (e.g., 'New Horizons') Optional Parameters ---- angle{1,2,3}: **NOT REALLY TESTED. THE BETTER WAY TO CHANGE THE ROTATION IS TO USE A DIFFERENT FRAME.** Rotation angles which are applied when defining the plane in space that the backplane will be generated for. These are applied in the order 1, 2, 3. Angles are in radians. Nominal values are 0. This allows the simulation of (e.g.) a ring system inclined relative to the nominal body equatorial plane. For MU69 sunflower rings, the following descriptions are roughly accurate, becuase the +Y axis points sunward, which is *almost* toward the observer. But it is better to experiment and find the appropriate angle that way, than rely on this ad hoc description. These are close for starting with. - `angle1` = Tilt front-back, from face-on. Or rotation angle, if tilted right-left. - `angle2` = Rotation angle, if tilted front-back. - `angle3` = Tilt right-left, from face-on. do_fast: Boolean. If set, generate only an abbreviated set of backplanes. **NOT CURRENTLY IMPLEMENTED.** Output ---- Output is a tuple, consisting of each of the backplanes, and a text description for each one. The size of each of these arrays is the same as the input image. The position of each of these is the plane defined by the target body, and the normal vector to the observer. output = (backplanes, descs) backplanes = (ra, dec, radius_eq, longitude_eq, phase) descs = (desc_ra, desc_dec, desc_radius_eq, desc_longitude_eq, desc_phase) Radius_eq: Radius, in the ring's equatorial plane, in km Longitude_eq: Longitude, in the equatorial plane, in radians (0 .. 2pi) RA: RA of pixel, in radians Dec: Dec of pixel, in dRA: Projected offset in RA direction between center of body (or barycenter) and pixel, in km. dDec: Projected offset in Dec direction between center of body (or barycenter) and pixel, in km. Z: Vertical value of the ring system. With special options selected (TBD), then additional backplanes will be generated -- e.g., a set of planes for each of the Jovian satellites in the image, or sunflower orbit, etc. No new FITS file is written. The only output is the returned tuple. """ if not(frame): raise ValueError('frame undefined') if not(name_target): raise ValueError('name_target undefined') if not(name_observer): raise ValueError('name_observer undefined') name_body = name_target # Sometimes we use one, sometimes the other. Both are identical fov_lorri = 0.3 * hbt.d2r abcorr = 'LT' do_satellites = False # Flag: Do we create an additional backplane for each of Jupiter's small sats? # Open the FITS file w = WCS(file) # Warning: I have gotten a segfault here before if passing a FITS file with no WCS info. hdulist = fits.open(file) et = float(hdulist[0].header['SPCSCET']) # ET of mid-exposure, on s/c n_dx = int(hdulist[0].header['NAXIS1']) # Pixel dimensions of image. Both LORRI and MVIC have this. n_dy = int(hdulist[0].header['NAXIS2']) hdulist.close() # Setup the output arrays lon_arr = np.zeros((n_dy, n_dx)) # Longitude of pixel (defined with recpgr) lat_arr = np.zeros((n_dy, n_dx)) # Latitude of pixel (which is zero, so meaningless) radius_arr = np.zeros((n_dy, n_dx)) # Radius, in km altitude_arr= np.zeros((n_dy, n_dx)) # Altitude above midplane, in km ra_arr = np.zeros((n_dy, n_dx)) # RA of pixel dec_arr = np.zeros((n_dy, n_dx)) # Dec of pixel dra_arr = np.zeros((n_dy, n_dx)) # dRA of pixel: Distance in sky plane between pixel and body, in km. ddec_arr = np.zeros((n_dy, n_dx)) # dDec of pixel: Distance in sky plane between pixel and body, in km. phase_arr = np.zeros((n_dy, n_dx)) # Phase angle x_arr = np.zeros((n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords y_arr = np.zeros((n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords z_arr = np.zeros((n_dy, n_dx)) # Intersection of sky plane: X pos in bdoy coords # ============================================================================= # Do the backplane, in the general case. # This is a long routine, because we deal with all the satellites, the J-ring, etc. # ============================================================================= if (True): # Look up body parameters, used for PGRREC(). (num, radii) = sp.bodvrd(name_target, 'RADII', 3) r_e = radii[0] r_p = radii[2] flat = (r_e - r_p) / r_e # Define a SPICE 'plane' along the plane of the ring. # Do this in coordinate frame of the body (IAU_JUPITER, 2014_MU69_SUNFLOWER_ROT, etc). # ============================================================================= # Set up the Jupiter system specifics # ============================================================================= if (name_target.upper() == 'JUPITER'): plane_target_eq = sp.nvp2pl([0,0,1], [0,0,0]) # nvp2pl: Normal Vec + Point to Plane. Jupiter north pole? # For Jupiter only, define a few more output arrays for the final backplane set ang_metis_arr = np.zeros((n_dy, n_dx)) # Angle from pixel to body, in radians ang_adrastea_arr = ang_metis_arr.copy() ang_thebe_arr = ang_metis_arr.copy() ang_amalthea_arr = ang_metis_arr.copy() # ============================================================================= # Set up the MU69 specifics # ============================================================================= if ('MU69' in name_target.upper()): # Define a plane, which is the plane of sunflower rings (ie, X-Z plane in Sunflower frame) # If additional angles are passed, then create an Euler matrix which will do additional angles of rotation. # This is defined in the 'MU69_SUNFLOWER' frame vec_plane = [0, 1, 0] # Use +Y (anti-sun dir), which is normal to XZ. # vec_plane = [0, 0, 1] # Use +Y (anti-sun dir), which is normal to XZ. plane_target_eq = sp.nvp2pl(vec_plane, [0,0,0]) # "Normal Vec + Point to Plane". 0,0,0 = origin. # XXX NB: This plane in in body coords, not J2K coords. This is what we want, because the # target intercept calculation is also done in body coords. # ============================================================================= # Set up the various output planes and arrays necessary for computation # ============================================================================= # Get xformation matrix from J2K to target system coords. I can use this for points *or* vectors. mx_j2k_frame = sp.pxform('J2000', frame, et) # from, to, et # Get vec from body to s/c, in both body frame, and J2K. # NB: The suffix _j2k indicates j2K frame. _frame indicates the frame of target (IAU_JUP, MU69_SUNFLOWER, etc) (st_target_sc_frame, lt) = sp.spkezr(name_observer, et, frame, abcorr, name_target) (st_sc_target_frame, lt) = sp.spkezr(name_target, et, frame, abcorr, name_observer) (st_target_sc_j2k, lt) = sp.spkezr(name_observer, et, 'J2000', abcorr, name_target) (st_sc_target_j2k, lt) = sp.spkezr(name_target, et, 'J2000', abcorr, name_observer) vec_target_sc_frame = st_target_sc_frame[0:3] vec_sc_target_frame = st_sc_target_frame[0:3] vec_target_sc_j2k = st_target_sc_j2k[0:3] vec_sc_target_j2k = st_sc_target_j2k[0:3] dist_target_sc = sp.vnorm(vec_target_sc_j2k) # Get target distance, in km # vec_sc_target_frame = -vec_target_sc_frame # ACTUALLY THIS IS NOT TRUE!! ONLY TRUE IF ABCORR=NONE. # Name this vector a 'point'. INRYPL requires a point argument. pt_target_sc_frame = vec_target_sc_frame # Look up RA and Dec of target (from sc), in J2K (_, ra_sc_target, dec_sc_target) = sp.recrad(vec_sc_target_j2k) # Get vector from target to sun. We use this later for phase angle. (st_target_sun_frame, lt) = sp.spkezr('Sun', et, frame, abcorr, name_target) # From body to Sun, in body frame vec_target_sun_frame = st_target_sun_frame[0:3] # Create a 2D array of RA and Dec points # These are made by WCS, so they are guaranteed to be right. xs = range(n_dx) ys = range(n_dy) (i_x_2d, i_y_2d) = np.meshgrid(xs, ys) (ra_arr, dec_arr) = w.wcs_pix2world(i_x_2d, i_y_2d, False) # Returns in degrees ra_arr *= hbt.d2r # Convert to radians dec_arr *= hbt.d2r # Compute the projected distance from MU69, in the sky plane, in km, for each pixel. # dist_target_sc is the distance to MU69, and we use this to convert from radians, to km. # 16-Oct-2018. I had been computing this erroneously. It should be *cosdec, not /cosdec. dra_arr = (ra_arr - ra_sc_target) * dist_target_sc * np.cos(dec_arr) ddec_arr = (dec_arr - dec_sc_target) * dist_target_sc # Convert to km # ============================================================================= # Compute position for additional Jupiter bodies, as needed # ============================================================================= if (name_target.upper() == 'JUPITER'): vec_metis_j2k,lt = sp.spkezr('Metis', et, 'J2000', abcorr, 'New Horizons') vec_adrastea_j2k,lt = sp.spkezr('Adrastea', et, 'J2000', abcorr, 'New Horizons') vec_thebe_j2k,lt = sp.spkezr('Thebe', et, 'J2000', abcorr, 'New Horizons') vec_amalthea_j2k,lt = sp.spkezr('Amalthea', et, 'J2000', abcorr, 'New Horizons') vec_metis_j2k = np.array(vec_metis_j2k[0:3]) vec_thebe_j2k = np.array(vec_thebe_j2k[0:3]) vec_adrastea_j2k = np.array(vec_adrastea_j2k[0:3]) vec_amalthea_j2k = np.array(vec_amalthea_j2k[0:3]) # ============================================================================= # Loop over pixels in the output image # ============================================================================= for i_x in xs: for i_y in ys: # Look up the vector direction of this single pixel, which is defined by an RA and Dec # Vector is thru pixel to ring, in J2K. # RA and Dec grids are made by WCS, so they are guaranteed to be right. vec_pix_j2k = sp.radrec(1., ra_arr[i_y, i_x], dec_arr[i_y, i_x]) # Convert vector along the pixel direction, from J2K into the target body frame vec_pix_frame = sp.mxv(mx_j2k_frame, vec_pix_j2k) # And calculate the intercept point between this vector, and the ring plane. # All these are in body coordinates. # plane_target_eq is defined as the body's equatorial plane (its XZ for MU69). # ** Some question as to whether we should shoot this vector at the ring plane, or the sky plane. # Ring plane is normally the one we want. But, for the case of edge-on rings, the eq's break down. # So, we should use the sky plane instead. Testing shows that for the case of MU69 Eq's break down # for edge-on rings... there is always an ambiguity. # ** For testing, try intersecting the sky plane instead of the ring plane. # ** Confirmed: Using sky plane gives identical results in case of face-on rings. # And it gives meaningful results in case of edge-on rings, where ring plane did not. # However, for normal rings (e.g., Jupiter), we should continue using the ring plane, not sky plane. do_sky_plane = True # For ORT4, where we want to use euler angles, need to set this to False if do_sky_plane and ('MU69' in name_target): plane_sky_frame = sp.nvp2pl(vec_sc_target_frame, [0,0,0]) # Frame normal to s/c vec, cntrd on MU69 (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_sky_frame) # pt_intersect_frame is the point where the ray hits the skyplane, in the coordinate frame # of the target body. else: # Calc intersect into equator of target plane (ie, ring plane) (npts, pt_intersect_frame) = sp.inrypl(pt_target_sc_frame, vec_pix_frame, plane_target_eq) # pt, vec, plane # Swap axes in target frame if needed. # In the case of MU69 (both sunflower and tunacan), the frame is defined s.t. the ring # is in the XZ plane, not XY. This is strange (but correct). # I bet MU69 is the only ring like this. Swap it so that Z means 'vertical, out of plane' -- # that is, put it into normal XYZ rectangular coords, so we can use RECLAT etc on it. if ('MU69' in name_target): # Was 0 2 1. But this makes tunacan radius look in wrong dir. # 201 looks same # 210 similar # 201 similar # 102, 120 similar. # ** None of these change orientation of 'radius' backplane. OK. pt_intersect_frame = np.array([pt_intersect_frame[0], pt_intersect_frame[2], pt_intersect_frame[1]]) # Get the radius and azimuth of the intersect, in the ring plane # Q: Why for the TUNACAN is the radius zero here along horizontal (see plot)? # A: Ahh, it is not zero. It is just that the 'projected radius' of a ring that is nearly edge-on # can be huge! Basically, if we try to calc the intersection with that plane, it will give screwy # answers, because the plane is so close to edge-on that intersection could be a long way # from body itself. # Instead, I really want to take the tangent sky plane, intersect that, and then calc the # position of that (in xyz, radius, longitude, etc). # Since that plane is fixed, I don't see a disadvantage to doing that. # We want the 'radius' to be the radius in the equatorial plane -- that is, sqrt(x^2 + y^2). # We don't want it to be the 'SPICE radius', which is the distance. # (For MU69 equatorial plane is nominally XZ, but we have already changed that above to XY.) _radius_3d, lon, lat = sp.reclat(pt_intersect_frame) radius_eq = sp.vnorm([pt_intersect_frame[0], pt_intersect_frame[1], 0]) # radius_eq = sp.vnorm([pt_intersect_frame[0], pt_intersect_frame[1], pt_intersect_frame[2]]) # Get the vertical position (altitude) altitude = pt_intersect_frame[2] # Calculate the phase angle: angle between s/c-to-ring, and ring-to-sun vec_ring_sun_frame = -pt_intersect_frame + vec_target_sun_frame angle_phase = sp.vsep(-vec_pix_frame, vec_ring_sun_frame) # Save various derived quantities radius_arr[i_y, i_x] = radius_eq lon_arr[i_y, i_x] = lon phase_arr[i_y, i_x] = angle_phase altitude_arr[i_y, i_x] = altitude # Save these just for debugging x_arr[i_y, i_x] = pt_intersect_frame[0] y_arr[i_y, i_x] = pt_intersect_frame[1] z_arr[i_y, i_x] = pt_intersect_frame[2] # Now calc angular separation between this pixel, and the satellites in our list # Since these are huge arrays, cast into floats to make sure they are not doubles. if (name_body.upper() == 'JUPITER'): ang_thebe_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_thebe_j2k) ang_adrastea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_adrastea_j2k) ang_metis_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_metis_j2k) ang_amalthea_arr[i_y, i_x] = sp.vsep(vec_pix_j2k, vec_amalthea_j2k) # Now, fix a bug. The issue is that SP.INRYPL uses the actual location of the bodies (no aberration), # while their position is calculated (as it should be) with abcorr=LT. This causes a small error in the # positions based on the INRYPL calculation. This should probably be fixed above, but it was not # obvious how. So, instead, I am fixing it here, by doing a small manual offset. # Calculate the shift required, by measuring the position of MU69 with abcorr=NONE, and comparing it to # the existing calculation, that uses abcorr=LT. This is brute force, but it works. For MU69 approach, # it is 0.75 LORRI 4x4 pixels (ie, 3 1X1 pixels). This is bafflingly huge (I mean, we are headed # straight toward MU69, and it takes a month to move a pixel, and RTLT is only a few minutes). But I have # confirmed the math and the magnitude, and it works. (st_sc_target_j2k_nolt, _) = sp.spkezr(name_target, et, 'J2000', 'NONE', name_observer) vec_sc_target_j2k_nolt = st_sc_target_j2k_nolt[0:3] (_, ra_sc_target_nolt, dec_sc_target_nolt) = sp.recrad(vec_sc_target_j2k_nolt) (x0,y0) = w.wcs_world2pix(ra_sc_target_nolt*hbt.r2d, dec_sc_target_nolt*hbt.r2d, 1) (x1,y1) = w.wcs_world2pix(ra_sc_target *hbt.r2d, dec_sc_target *hbt.r2d, 1) dx = x1-x0 dy = y1-y0 print(f'Compute backplanes: INRYPL pixel shift = {dx}, {dy}') dx_int = int(round(dx)) dy_int = int(round(dy)) do_roll = True if do_roll: print(f'compute_backplanes: Rolling by {dx_int}, {dy_int} due to INRYPL') # Now shift all of the planes that need fixing. The dRA_km and dDec_km are calculated before INRYPL() # is applied, so they do not need to be shifted. I have validated that by plotting them. # # XXX NP.ROLL() is really not ideal. I should use a function that introduces NaN at the edge, not roll it. radius_arr = np.roll(np.roll(radius_arr, dy_int, axis=0), dx_int, axis=1) lon_arr = np.roll(np.roll(lon_arr, dy_int, axis=0), dx_int, axis=1) phase_arr = np.roll(np.roll(phase_arr, dy_int, axis=0), dx_int, axis=1) altitude_arr = np.roll(np.roll(altitude_arr, dy_int, axis=0), dx_int, axis=1) else: print(f'compute_backplanes: Skipping roll due to INRYPL, based on do_roll={do_roll}') # Assemble the results into a backplane backplane = { 'RA' : ra_arr.astype(float), # return radians 'Dec' : dec_arr.astype(float), # return radians 'dRA_km' : dra_arr.astype(float), 'dDec_km' : ddec_arr.astype(float), 'Radius_eq' : radius_arr.astype(float), 'Longitude_eq' : lon_arr.astype(float), 'Phase' : phase_arr.astype(float), 'Altitude_eq' : altitude_arr.astype(float), # 'x' : x_arr.astype(float), # 'y' : y_arr.astype(float), # 'z' : z_arr.astype(float), # } # Assemble a bunch of descriptors, to be put into the FITS headers desc = { 'RA of pixel, radians', 'Dec of pixel, radians', 'Offset from target in target plane, RA direction, km', 'Offset from target in target plane, Dec direction, km', 'Projected equatorial radius, km', 'Projected equatorial longitude, km', 'Sun-target-observer phase angle, radians', 'Altitude above midplane, km', # 'X position of sky plane intercept', # 'Y position of sky plane intercept', # 'Z position of sky plane intercept' } # In the case of Jupiter, add a few extra fields if (name_body.upper() == 'JUPITER'): backplane['Ang_Thebe'] = ang_thebe_arr.astype(float) # Angle to Thebe, in radians backplane['Ang_Metis'] = ang_metis_arr.astype(float) backplane['Ang_Amalthea'] = ang_amalthea_arr.astype(float) backplane['Ang_Adrastea'] = ang_adrastea_arr.astype(float) # If distance to any of the small sats is < 0.3 deg, then delete that entry in the dictionary if (np.amin(ang_thebe_arr) > fov_lorri): del backplane['Ang_Thebe'] else: print("Keeping Thebe".format(np.min(ang_thebe_arr) * hbt.r2d)) if (np.amin(ang_metis_arr) > fov_lorri): del backplane['Ang_Metis'] else: print("Keeping Metis, min = {} deg".format(np.min(ang_metis_arr) * hbt.r2d)) if (np.amin(ang_amalthea_arr) > fov_lorri): del backplane['Ang_Amalthea'] else: print("Keeping Amalthea, min = {} deg".format(np.amin(ang_amalthea_arr) * hbt.r2d)) if (np.amin(ang_adrastea_arr) > fov_lorri): del backplane['Ang_Adrastea'] else: print("Keeping Adrastea".format(np.min(ang_adrastea_arr) * hbt.r2d)) # And return the backplane set return (backplane, desc)
def get_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
def __init__(self,fovraws,ralohi=(),declohi=() ,obs_pos=None,obs_vel=None,obs_year=None ): """Convert FOV definition in external inertial reference frame to an FOV definition in a local reference frame; also determine RA,Dec limits of FOV for use in star catalog lookup. For polygonal FOVs, set up FOV as vectors in a local reference frame, with a matrix to rotate vectors from the external to the local frame. The local reference frame (reffrm) will have +Z along the average vertices' direction, and will be rotated such the the +X axis is not parallel to any of the edges of the polygon from the FOV projected onto the Z=+1 plane. Arguments fovraws - sequence either of vector and cone and half-angle for circular FOV, or of a pair of vectors for RA,Dec box, or of three or more vectors for a polygonal FOV. A vector is a sequence either of two values, RA,Dec, or of three values, X,Y,Z. obs_pos - Observer position, solar system barycentric, 3-vector, km - For parallax correction obs_vel - Observer velocity, solar system barycentric, 3-vector, km/s - For proper motion correction obs_year - Observer time, y past 2015.5 (Gaia DR2 epoch) - For stellar aberration correction """ ### Get count of items in FOV sequence; ensure it is 2 or more ### and ralohi and declohi are empty, or that fovraws is empty ### and ralohi and declohi have 2 values each (self.fovraws ,self.ralohi ,self.declohi ,self.obs_pos ,self.obs_vel ,self.obs_year ,)= fovraws,list(ralohi),list(declohi),obs_pos,obs_vel,obs_year self.L = len(fovraws) assert (1<self.L and not (self.ralohi+self.declohi) ) or (0==self.L and 2==len(self.ralohi) and 2==len(self.declohi) ), 'Invalid vertices in FOV' ################################ ### Initialize: FOV RA,Dec pairs; FOV type (assume polygon); FOV ### vector triples; list of RA,Dec boxes self.radecdegs = list() self.fovtype = 1<self.L and FOV.POLYGONTYPE or FOV.RADECBOXTYPE self.uvfovxyzs,fovsum = list(),sp.vpack(0.,0.,0.) self.radec_boxes = list() rdba = self.radec_boxes.append ### Shorthand to append box to list ################################ ### Parse list of vertices: ### - [list,float] => Circle (cone) ### - [list,list] => RA,Dec box ### - [list,list,list,...] => Polygon for vertex in fovraws: ### For second of two vertices ... if 1==len(self.radecdegs) and 2==self.L: ### Two-vertex items are either a conic FOV, or an [RA,Dec] box try: ### If second item in list is a float, then it's a half-angle ### of the cone self.hangdeg = float(vertex) assert self.hangdeg < 90.0,'Cone half-angle is not less than 90degrees' assert self.hangdeg > 0.0,'Cone half-angle is not greater than 0degrees' self.hangrad = self.hangdeg * rpd self.min_cosine = math.cos(self.hangrad) self.uv_cone_axis = self.uvfovxyzs[0] self.fovtype = FOV.CIRCLETYPE break except AssertionError as e: raise except: ### If the above fails, then it's the second corner of the box self.fovtype = FOV.RADECBOXTYPE ### Parse one vertex ra,dec,uvxyz = parse_inertial(vertex) ### Append RA,Dec and unit vector XYZ onto their resepective lists self.radecdegs.append((ra,dec,)) self.uvfovxyzs.append(uvxyz) fovsum = sp.vadd(fovsum,uvxyz) ################################ ### Calculate RA,DEC limits as list of [ralo,rahi,declo,dechi] boxes ### - .radec_boxes is a list; rdba is .radec_boxes.append ### - List will have multiple RA,Dec boxes if FOV crosses the Prime ### Meridian (PM) an even number of times. if self.fovtype == FOV.RADECBOXTYPE: ### RA,DEC box FOV: calculate limits; handle PM crossing if 2==self.L: ras,decs = zip(*self.radecdegs) ralo,rahi = sorted(ras) declo,dechi = sorted(decs) if 180 > (rahi-ralo): rdba([ralo,rahi,declo,dechi]) else: rdba([0.0,ralo,declo,dechi]) rdba([rahi,360.0,declo,dechi]) else: if self.ralohi[1] > self.ralohi[0]: rdba(self.ralohi+self.declohi) else: rdba([self.ralohi[0],360.0]+self.declohi) rdba([0.0,self.ralohi[1]]+self.declohi) elif self.fovtype == FOV.CIRCLETYPE: ### Circular FOV: DEC limits determine RA limits; handle PM Xing ra,dec = self.radecdegs[0] fovdeclo = dec - self.hangdeg fovdechi = dec + self.hangdeg if fovdeclo < -90.0 or fovdechi > 90.0: ### A pole is in the FOV; use full RA range fovralo,fovrahi = 0.0,360.0 fovdeclo,fovdechi = max([fovdeclo,-90.0]),min([fovdechi,+90.0]) elif fovdeclo == -90.0 or fovdechi == 90.0: ### A pole is on the FOV circumference; RA range is 180 degrees fovralo,fovrahi = ra-90.0,ra+90.0 else: ### The FOV excludes the poles; calculate the RA range, using ### the formula validated in script validate_delta_ra_formula.py tanhang,tandec = math.tan(self.hangrad),math.tan(dec*rpd) sinhang,cosdec = math.sin(self.hangrad),math.cos(dec*rpd) coshang = math.cos(self.hangrad) T = sinhang / math.sqrt(1.0 - ((tanhang*tandec)**2)) deltara = dpr * math.atan(T / (cosdec * coshang)) fovralo,fovrahi = ra-deltara,ra+deltara ### Ensure RA limits are within range [0:360] (N.B. inclusive) if fovralo < 0.0: fovralo += 360.0 if fovrahi > 360.0: fovrahi -= 360.0 if fovralo <= fovrahi: ### RA lo <= RA hi: no PM crosssing rdba([fovralo,fovrahi,fovdeclo,fovdechi]) else: ### RA hi < RA hi: there is a PM crosssing rdba([0.0,fovrahi,fovdeclo,fovdechi]) rdba([fovralo,360.,fovdeclo,fovdechi]) else: assert self.fovtype == FOV.POLYGONTYPE ### Polygonal FOV: build frame where all vertices will be ### projected onto the plane Z=1 ### .uvavg: unit vector = mean of all vertices, will be +Z self.uvavg = sp.vhat(fovsum) ### Create rotation matrix to FOV frame: +Z is mean of vertices' ### directions (.uvavg); +X will be a direction that is not ### parallel to any side of the polygon ### - Start with temporary matrix with +Z as defined above; +X ### toward vertex at largest angle from .uvavg vother = min([(sp.vdot(self.uvavg,v),list(v),) for v in self.uvfovxyzs])[1] tmpmtx = sp.twovec(self.uvavg,3,vother,1) ### - Rotate all vectors to that frame; scale Z components to 1.0 vtmps = list() for v in self.uvfovxyzs: ### - Ensure all vertices are in the same hemisphere assert 0.0 < sp.vdot(self.uvavg,v),'All vertices are not in the same hemisphere' vtmp = sp.mxv(tmpmtx,v) vtmps.append(sp.vscl(1.0/vtmp[2],vtmp)) ### Find largest azimuth gap between any two sides: that azimuth ### will be direction of +X in the final rotation matrix ### - Get azimuths of all sides of polygon, in range [-PI:PI] azimuths,vlast = list(),vtmps[-1] for v in self.uvfovxyzs: azimuths.append(numpy.arctan((v[1]-vlast[1])/(v[0]-vlast[0]))) vlast = v ### - Sort angles and add [least angle plus PI] to end of list azimuths.sort() azimuths.append(azimuths[0]+sp.pi()) ### - Find largest delta-azimuth and its index dazimuths = [hi-lo for hi,lo in zip(azimuths[1:],azimuths[:-1])] maxdaz = max(dazimuths) imaxdaz = dazimuths.index(maxdaz) ### - Calculate azimuth from to mean of that delta-azimuth, meanaz = azimuths[imaxdaz] + (maxdaz / 2.0) ### Final matrix: add rotation of tmpmtx around +Z by that angle self.mtxtofov = sp.mxm(sp.rotate(meanaz,3),tmpmtx) ### Apply final rotation matrix, store results in .uvlclxyzs tmpmtx = sp.twovec(self.uvavg,3,vother,1) self.uvlclxyzs = [self.rotate_to_local(v) for v in self.uvfovxyzs] ### Calculate upper and lower RA and Dec limits, with PM crossings los,his = list(),list() ### - Create [[RA,Dec],[X,Y,Z]] pairs list; ensure last is off PM pairs = list(zip(self.radecdegs,self.uvfovxyzs)) pop_count = 0 while pairs[-1][0][0] == 0.0: pop_count += 1 assert pop_count < self.L,'All vertices are on the Prime Meridian' pairs.append(pairs.pop(0)) ### Count PM crossings self.crossing_count = 0 lastra = pairs[-1][0][0] zero_count = 0 for (ra,dec,),xyz in pairs: if ra == 0.0: zero_count += 1 if lastra > 180.0: ra = 360.0 if 180 < abs(ra-lastra): self.crossing_count += 1 lastra = ra if 0==self.crossing_count or 1==(1&self.crossing_count): ### If there are either no, or an odd number, of PM crossings, ### then use the pairs as-is for a single FOV subfovs = [pairs] if self.crossing_count: ### - For odd crossing count, one pole or the other must be ### in the FOV; init full RA range, that pole for Dec ranges ralo,rahi = 0.0,360.0 if sp.vdot(self.uvavg,[0,0,1]) > 0.0: declo = dechi = +90.0 else : declo = dechi = -90.0 else: ### - For zero crossing count, initialize inverted ranges ralo,rahi = 360.0,0.0 declo,dechi = +90.0,-90.0 subranges = [[ralo,rahi,declo,dechi]] else: ### If there are an even, non-zero number of PM crossings, break ### them into two sub-FOVs, one on either side of the PM eastfov,westfov = list(),list() if zero_count: ### If there are any zero RA values, rotate the pairs to ### ensure a zero-RA pair is the first, so it and the non-zero ### last pair will be assigned to the correct side of the PM while pairs[0][0][0]!=0.0: pairs.append(pairs.pop(0)) else: ### If there are no zero RA values, rotate the pairs to ensure ### a crossing occurs between the last and first pair, so the ### corresponding zero crossing will be assigned to the ### correct side of the PM while abs(pairs[0][0][0]-pairs[-1][0][0])<180: pairs.append(pairs.pop(0)) ### Write vertices into the two sub-FOVs ### - Set last-vertex values for first item in pairs (lastra,lastdec,),lastxyz = pairs[-1] for pair in pairs: ### - Loop over vertex pairs ((RA,DEC,),Cartesian_Vector) (ra,dec,),xyz = pair if ra == 0.0: ### - When RA=0, the previous RA determines if it's 0 ar 360 if lastra >= 180.0: ra = 360.0 westfov.append([(ra,dec,),xyz]) iswest = True else: eastfov.append(pair) iswest = False elif abs(lastra-ra) >= 180.0: ### - When the change in RA>=180, the PM is being crossed ### - Find the mid-vector where the PM is crossed k1 = -xyz[1] / (lastxyz[1]-xyz[1]) midxyz = sp.vhat(sp.vlcom(1.0-k1,xyz,k1,lastxyz)) middec = dpr * sp.recrad(midxyz)[2] ### - Add that mid-vector, with RA=360, to the west FOV westfov.append([(360.0,middec,),midxyz]) ### - Determine if vector is west iswest = ra >= 180.0 ### - Add that mid-vector, with RA=0, to the east FOV ... if (ra > 0.0) and (not iswest): ### - ... only if the ra is not already 0, as it will be ### added in the next step eastfov.append([(0.0,middec,),midxyz]) ### Add the vector to either east or west FOV if iswest: westfov.append(pair) else : eastfov.append(pair) else: ### PM was not crossed, add vector to same FOV, as last time if iswest: westfov.append(pair) else : eastfov.append(pair) ### - Set last-vertex values for next item in pairs (lastra,lastdec,),lastxyz = (ra,dec,),xyz ### - Create subfovs list of east and west FOVs; set subranges subfovs = [eastfov,westfov] subranges = [[360.0,0.0,90.0,-90.0],[360.0,0.0,90.0,-90.0]] ### To here, we have list of FOV(s) and list of range(s); use them ### to determine RA,DEC box(es) to use for database query while subfovs: ### Get sub-FOV, sub-range; set last vertex's XYZ subfov,(ralo,rahi,declo,dechi,) = subfovs.pop(),subranges.pop() lastxyz = subfov[-1][-1] for pair in subfov: ### Each element of subfov comprises (RA,Dec) and vertex XYZ ### - xyz is a unit vector (ra,dec,),xyz = pair ### - Adjust RA limits as needed from RA of vertex if ra > rahi: rahi = ra elif ra < ralo: ralo = ra ### - Set Dec extrema from DEC of vertex maxdec = mindec = dec ### - Calculate Dec extrema from lastxyz to xyz ### -- Normal to plane of lastxyz and syz sidenormal = sp.vcrss(lastxyz,xyz) ### -- Z-rates along great circle at lastxyz and at xyz lastdz = sp.vcrss(sidenormal,lastxyz)[2] dz = sp.vcrss(sidenormal,xyz)[2] if 0.0 > (lastdz*dz): ### -- If sign of Z-rates differs, there should be an ### extreme value between lastxyz and xyz ### --- Get vector perpendicular to side normal on equator ### --- Use that to calculate the unit vector at Dec extreme equinox = sp.vcrss([0,0,1],sidenormal) vtoextremez = sp.ucrss(sidenormal,equinox) ### --- Cosine of angle between lastxyz and xyz mindot = sp.vdot(lastxyz,xyz) for none in [None,None]: ### --- Two cases: vtoextremez and -vtoextremez ### - Angles from vtoextremez to lastxyz and to xyz ### must be less than angle between lastxyz and xyz ### so cosines of those angles must be greater lastxyzdot = sp.vdot(lastxyz,vtoextremez) xyzdot = sp.vdot(xyz,vtoextremez) if lastxyzdot>mindot and xyzdot>mindot: ### --- Adjust maxdec and mindec as needed try : extremedec = dpr * math.asin(vtoextremez[2]) except: extremedec = dpr * sp.recrad(vtoextremez)[2] if extremedec > maxdec: maxdec = extremedec elif extremedec < mindec: mindec = extremedec break ### --- Invert vtoextremez for next pass vtoextremez = sp.vminus(vtoextremez) ### - Adjust Dec limits as needed from Dec extrema of side if maxdec > dechi: dechi = maxdec if mindec < declo: declo = mindec lastxyz = xyz ### Append calculated RA,Dec box(es) rdba((ralo,rahi,declo,dechi,)) ### Put None in .localxyzs, in .v_for_stellar_aberr, and in ### .v_for_parallax; if no stellar aberration or parallax is ### explicitly applied to define it later, then .localxyzs will be ### calculated on the fly self.localxyzs = None self.v_for_stellar_aberr = None self.v_for_parallax = None
4332.59 * 86400.0 * 1e3), 'Uranus': make_planet('IAU_URANUS', 'USO', len(t), 30688.5 * 86400.0 * 1e3), 'Saturn': make_planet('IAU_SATURN', 'KSO', len(t), 10759.22 * 86400.0 * 1e3) } # Do the calculations. print('[ikuchi-build] Calculating pole positions') for i in range(len(t)): if i % 1000 == 0: print('[ikuchi-build] Calculating pole positions: {:04}/{:04}'.format( i + 1, len(t))) for p in data: m = spiceypy.pxform(data[p]['s'], data[p]['d'], t[i]) v = spiceypy.mxv(m, [0, 0, 1]) data[p]['v'][i, :] = v data[p]['roty'][i] = np.arccos(v[2]) * 180 / np.pi data[p]['rotz'][i] = np.arctan2(v[1], v[0]) * 180 / np.pi # Now we have a set of angles that are modulo 2pi, but to fit them we need # to remove the modulo 2pi and have a monotonic set of angles, e.g., 0- 2*pi*n. # We find all the points where the angle changes and then add an offset to that # rotation. We keep increasing that offset by 2pi for each rotation to # get a monotonic change. print( '[ikuchi-build] Adjusting rotation angles to be monotonic (invert modulo 2pi)' ) for p in data: rages = [] st = 0
def rotate_to_local(self,vxyz): """Utility to rotation from inertial frame to local frame""" return sp.mxv(self.mtxtofov,vxyz)