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 cassini_surfintercerpt(utc, output=False): target = 'TITAN' fixref = 'IAU_TITAN' dref = 'IAU_TITAN' method = 'ELLIPSOID' abcorr = 'NONE' obsrvr = 'CASSINI' state = cassini_phase(utc) dvec = spice.vhat(-state[:3]) et = spice.str2et(utc) [point, trgepc, srfvec] = spice.sincpt(method, target, et, fixref, abcorr, obsrvr, dref, dvec) temp = spice.reclat(point) radius = temp[0] colat = 90 + spice.convrt(temp[1], "RADIANS", "DEGREES") lon = np.mod(spice.convrt(temp[2], "RADIANS", "DEGREES"), 360) if output: print("Distance to Intercept Point", spice.vnorm(srfvec)) print("Radius", radius) print("Colatitude", colat) print("Longtitude", lon) return point, [radius, colat, lon], spice.vnorm(srfvec)
def 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 point_towards_sun(self, pixel_res=0.5): """ Compute the solar azimuth. Pixel resolution is required to stay within one pixel of the origin point """ # Check if surface point spoint was set if not self.spoint_set: raise SPointNotSetError # Get the difference vector poB=subsolar-origin with its tail at origin # and its head at the subsolar point poB = spice.vsub(self.subsolar, self.spoint) # get pixel scale in km/pixel and then divide by 2 to insure to stay # within a pixel of the origin point scale = (pixel_res / 1000.0) / 2.0 # the difference vector cuts through the body, # we need the tangent vector # to the surface at the origin point. vperp receives the perpendicular # component of the poB towards the spoint vector hpoB = spice.vperp(poB, self.spoint) # unitize the tangent vector and then scale it to within a pixel of the # origin point upoB = spice.vhat(hpoB) spoB = spice.vscl(scale, upoB) # Compute the new point in body fixed. This point will be within a # pixel of the origin but in the same direction as the requested la/lon # of the point of interest, i.e. the subsolar point nB = spice.vadd(self.spoint, spoB) coords = Coords.fromtuple(spice.reclat(nB)) return coords.dlon, coords.dlat
def point_towards_sun(self, pixel_res=0.5): """ Compute the solar azimuth. Pixel resolution is required to stay within one pixel of the origin point """ # Check if surface point spoint was set if not self.spoint_set: raise SPointNotSetError # Get the difference vector poB=subsolar-origin with its tail at origin # and its head at the subsolar point poB = spice.vsub(self.subsolar, self.spoint) # get pixel scale in km/pixel and then divide by 2 to insure to stay # within a pixel of the origin point scale = (pixel_res / 1000.0) / 2.0 # the difference vector cuts through the body, # we need the tangent vector # to the surface at the origin point. vperp receives the perpendicular # component of the poB towards the spoint vector hpoB = spice.vperp(poB, self.spoint) # unitize the tangent vector and then scale it to within a pixel of the # origin point upoB = spice.vhat(hpoB) spoB = spice.vscl(scale, upoB) # Compute the new point in body fixed. This point will be within a # pixel of the origin but in the same direction as the requested la/lon # of the point of interest, i.e. the subsolar point nB = spice.vadd(self.spoint, spoB) coords = SurfaceCoords.fromtuple(spice.reclat(nB)) return coords.dlon, coords.dlat
def subsolar(self): # normalize surface point vector: uuB = spice.vhat(self.center_to_sun) # receive subsolar point in IAU_MARS rectangular coords # the *self.radii unpacks the Radii object into 3 arguments. v_subsolar = spice.surfpt((0, 0, 0), uuB, *self.radii) return v_subsolar
def _get_subsolar(self): # normalize surface point vector: uuB = spice.vhat(self.center_to_sun) # receive subsolar point in IAU_MARS rectangular coords # the *self.radii unpacks the Radii object into 3 arguments. v_subsolar = spice.surfpt((0, 0, 0), uuB, *self.radii) return v_subsolar
def parse_inertial(input_vertex, return_radec=True): """Parse input vertex as either RA,Dec or XYZ; Return RA,DEC (optional) and unit vector """ if 2==len(input_vertex): ### Vertex has two items: assume they are RA and Dec ra,dec = map(float,input_vertex) assert ra<=360.0 and ra>=0.0,'RA ({0}) is out of range [0,360)'.format(ra) assert dec<=90.0 and dec>=-90.0,'RA ({0}) is out of range [-90,+90]'.format(dec) uvxyz = sp.radrec(1.0,ra*rpd,dec*rpd) else: ### Vertex has three items: assume they are XYZ assert 3==len(input_vertex),'XYZ input vector [{0}] for vertex does not have 3 elements'.format(str(input_vertex)) uvxyz = sp.vhat(sp.vpack(*map(float,input_vertex))) if return_radec: ra,dec = sp.vsclg(dpr,sp.recrad(uvxyz)[1:],2) if return_radec: return ra,dec,uvxyz return uvxyz
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 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 dooneRandom(): ### Random normal unit vector unitNormal = sp.vhat([r.uniform(-1, 1) for i in xrange(3)]) ### Random positive semi-axes' lengths semi_axes = [abs(r.uniform(.5, 100)) for i in xrange(3)] ### Solve for surface point with that surface normal xyz = norm2surfpt(semi_axes, unitNormal) ### Use spiceypy.surfnm() to calculate surface unit normal vector, at ### solved-for surface point vector xyz, to compare to input unit normal ### vector, and calculate error magnitude, errMag surfnm = sp.surfnm(semi_axes[0], semi_axes[1], semi_axes[2], xyz) errVec = sp.vsub(surfnm, unitNormal) errMag = sp.vnorm(errVec) if doLog: pprint.pprint(dict(errVec=errVec, errMag=errMag)) ###return error magnitude return errMag
def caps_crosstrack_latlon(time, negwindspeed, poswindspeed, anodes=False): anode_vecs = [] anode_seps = [[], [], [], [], [], [], [], []] beamanodes = [] # print(time) beamanodes.append(np.mean(ELS_ramanodes(time)) + 1) state = cassini_phase(time.strftime('%Y-%m-%dT%H:%M:%S')) # crossvec = caps_crosstrack(time, windspeed) * abs(windspeed) * 1e-3 #Old Method crossvec, anode1, anode8 = caps_crosstrack_spice( time, np.mean([negwindspeed, poswindspeed])) # SPICE Plane method crossvec_neg = crossvec * 1e-3 crossvec_pos = crossvec * 1e-3 print("crossvec", crossvec_neg, crossvec_pos) newstate_neg = list(state[:3]) + list(crossvec_neg) newstate_pos = list(state[:3]) + list(crossvec_pos) transformed_state_neg = spice.xfmsta(newstate_neg, 'RECTANGULAR', 'Latitudinal', "TITAN") transformed_state_pos = spice.xfmsta(newstate_pos, 'RECTANGULAR', 'Latitudinal', "TITAN") print("test state", newstate_neg, newstate_pos) print("test xfmsta", transformed_state_neg, transformed_state_pos) alt = transformed_state_neg[0] lon = transformed_state_neg[1] * spice.dpr() lat = transformed_state_neg[2] * spice.dpr() if anodes: anode_vecs.append(caps_all_anodes(time)) for anodecounter, i in enumerate(anode_vecs[-1]): anode_seps[anodecounter].append( spice.vsep(spice.vhat(state[3:]), spice.vhat(anode_vecs[-1][anodecounter])) * spice.dpr()) print("anode_vecs", anode_vecs) print("anode_seps", anode_seps) dvec_neg = [ transformed_state_neg[4], transformed_state_neg[5], transformed_state_neg[3] ] dvec_pos = [ transformed_state_pos[4], transformed_state_pos[5], transformed_state_pos[3] ] print("dvec xfmsta", dvec_neg, dvec_pos) print(lon, lat, alt) titanrad = spice.bodvrd('TITAN', 'RADII', 3)[1][0] # Get Titan Radius mag_test_neg = spice.unorm([ dvec_neg[0] * (alt + titanrad) * 1e3, dvec_neg[1] * (alt + titanrad) * 1e3, dvec_neg[2] * 1e3 ]) mag_test_pos = spice.unorm([ dvec_pos[0] * (alt + titanrad) * 1e3, dvec_pos[1] * (alt + titanrad) * 1e3, dvec_pos[2] * 1e3 ]) # Convert Lat/Lon from rad/s to m/s, convert Alt from km/s to m/s. Forms Unit Vector here print("mag test", mag_test_pos) dlon_neg = mag_test_neg[0][0] * abs(negwindspeed) dlat_neg = mag_test_neg[0][1] * abs(negwindspeed) dalt_neg = mag_test_neg[0][2] * abs(negwindspeed) dlon_pos = mag_test_pos[0][0] * abs(poswindspeed) dlat_pos = mag_test_pos[0][1] * abs(poswindspeed) dalt_pos = mag_test_pos[0][2] * abs(poswindspeed) return lon, lat, alt, dlon_neg, dlat_neg, dalt_neg, dlon_pos, dlat_pos, dalt_pos
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 norm2surfpt(semi_axes, inputNormal): ### Ellipsoid semi-axes' lengths, per the ellipsoid formula: ### ### 2 2 2 ### / x \ / y \ / z \ ### ( --- ) + ( --- ) + ( --- ) = 1 ### \ a / \ b / \ c / ### Ensure abc components are positive abc = sp.vequ([abs(semi_axis) for semi_axis in semi_axes[:3]]) ### Get unit vector, n, parallel to input normal n = sp.vhat(inputNormal) ### Direction of normal, N, at [x,y,z] is [ x/(a*a), y/(b*b), z/(c*c)] ### - Vector N is unknown, and is not necessarily a unit vector ### - Argument inputNormal is parallel to N, and of arbitrary length ### - Unit vector parallel to N is n, calculated above from inputNormal ### - Assume length of N is scalar 1/k; k is initially unknown ### - Scaling unit normal, n, by k yields N: ### ### n/k = N = [x/(a*a), y/(b*b), z/(c*c)] Eqn. 1 ### ### so ### ### nx/k = x/(a*a) Eqn. 2x ### ny/k = y/(b*b) Eqn. 2y ### nz/k = z/(c*c) Eqn. 2z ### ### and, solving for surface point components, [x,y,z]: ### ### x = nx*a*a/k Eqn. 3x ### y = ny*b*b/k Eqn. 3y ### z = nz*c*c/k Eqn. 3z ### ### Substituting Eqns. 3x, 3y, and 3z for x, y, and z ### back into the ellipsoid formula (x^2/a^2 + ... = 1): ### ### (nx*a*a/k)^2/(a*a) ### + (ny*b*b/k)^2/(b*b) ### + (nz*c*c/k)^2/(c*c) = 1 Eqn. 4 ### ### and solving for k: ### ### (nx*a)^2 + (ny*b)^2 + (nz*c)^2 = k^2 Eqn. 5 ### ### Since nx, a, ny, b, nz, and c are all known, k ### can be calculated directly using Eqn. 5, and the ### surface point components, x, y, and z, can then ### be calculated using Eqns. 3x, 3y, and 3z. ### Calculate two vectors: ### - [a*a, b*b, c*c] ### - [nx*nx, ny*ny, nz*nz] abcXabc = vXv(abc, abc) nXn = vXv(n, n) ### Solve for k using abcXabc, nXn, and Eqn. 5: k2 = sp.vdot(abcXabc, nXn) k = math.sqrt(k2) ### Use k, abcXabc, n, and Eqns. 3x, 3y, and ### 3z to calculate surface point vector xyz xyz = sp.vscl(1. / k, vXv(abcXabc, n)) ### Debug logging: if doLog: ### Calculate (x/a)^2 + (y/b)^2 + (z/c)^2; it should be = 1 one = sp.vdot(vXv(xyz, xyz), 1 / abcXabc) pprint.pprint( dict(n=n, abc=abc, abcXabc=abcXabc, nXn=nXn, nMag=sp.vnorm(n), k=k, k2=k2, xyz=xyz, one=one)) ### Return surface point return xyz
for iet in range(-400,401): ### ET (TDB) et = float(iet) / 10 ets.append(et) ### Position of Bennu (2101955) wrt instrument storxinstr,ltorxinstr = sp.spkezr('2101955',et,'J2000','none','-64999') storx,ltorx = sp.spkezr('2101955',et,'J2000','none','-64') ### Position of Bennu (2101955) wrt S/C COM porxinstr,vorxinstr = storxinstr[:3],storxinstr[3:] porx,vorx = storx[:3],storx[3:] ### Delta dRange/dt dvs.append( sp.vdot(vorxinstr,sp.vhat(porx)) - sp.vdot(vorx,sp.vhat(porx)) ) porxinstrs.append(list(porxinstr)) vorxinstrs.append(list(vorxinstr)) porxs.append(list(porx)) vorxs.append(list(vorx)) with open('relvel.json','w') as fjson: sj.dump(dict(porxinstrs=porxinstrs ,vorxinstrs=vorxinstrs ,porxs=porxs ,vorxs=vorxs ,dvs=dvs ,ets=ets
degPerHour = 2.0 * dpr * twopi / hpd xTolerance = lambda xDiff: xDiff < 1e-10 xDiffTolerance = lambda x, xExpect: xTolerance(abs(x - xExpect)) et, iPass = et0, 0 while et < (et0 + spd + 1): ### Get the state of the MINUTE and HOUR bodies every half hour stMinute, lt = sp.spkezr(sMinute, et, 'J2000', 'NONE', sClock) stHour, lt = sp.spkezr(sHour, et, 'J2000', 'NONE', sClock) if (iPass % 2): ### On the half hour, the minute hand will be at RA=180, along [-1,0,0] assert xTolerance( sp.vnorm(sp.vsub([-1.0, 0., 0.], sp.vhat(stMinute[:3])))) else: ### On the hour, the minute hand will be at RA=0, at [+1,0,0] assert xTolerance( sp.vnorm(sp.vsub([1.0, 0., 0.], sp.vhat(stMinute[:3])))) vsepDeg = dpr * sp.vsep(stHour[:3], stMinute[:3]) vsepExpectDeg = (degPerHour * (iPass >> 1)) % 360 ### On the hour, the houre hand will be (30 * iPass) degrees clockwise ### from +X, and therefor also the same from the minute hand assert xDiffTolerance(vsepDeg, vsepExpectDeg) or xDiffTolerance( (360 - vsepDeg), vsepExpectDeg) ### Step to next half hour and pass et += halfHour iPass += 1
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
"earth")[0] if do_debug: ### Ensure that light-time correction is zero for earth->star vector ### for star at fixed positon relative to SSB earth2star_lt = sp.spkcpt(vstar, ssb, j2000, et, j2000, "observer", LT, "earth")[0] assert 0.0 == sp.vnormg(sp.vsubg(earth2star_none, earth2star_lt, 6), 6) ### Calculate differences between gaiif_util.py and SPICE results ... ab_vec_spice = sp.vsub(earth2star_lts[:3], earth2star_none[:3]) ab_vec_gaia = sp.vscl(range2star, sp.vsub(uvstar_ab, uvstar_noab)) ab_vec_diff = sp.vsub(ab_vec_spice, ab_vec_gaia) ab_diff_frac = sp.vnorm(ab_vec_diff) / sp.vnorm(ab_vec_spice) no_ab_diff = sp.vsep(uvstar_noab, sp.vhat(earth2star_none[:3])) ab_diff = sp.vsep(uvstar_ab, sp.vhat(earth2star_lts[:3])) ab_diff_frac = sp.vsep(uvstar_ab, sp.vhat(earth2star_lts[:3])) try: ### ... Success assert 1e-15 > no_ab_diff assert 1e-8 > ab_diff except: ### ... Failure print( dict(no_ab_diff=no_ab_diff, ab_diff=ab_diff, ab_diff_frac=ab_diff_frac, ab_mag_spice=sp.vsep(earth2star_none[:3], earth2star_lts[:3]), ab_mag_gaia=sp.vsep(uvstar_noab, uvstar_ab)))
def star_in_fov(self ,vstar ,parallax_maspau=None ,pmra_maspy=None ,pmdec_maspy=None ): """Return True if the star (RA,Dec or xyz) argument is in the FOV Argument vstar is either an RA,Dec pair (degrees) or a 3-vector """ if self.fovtype == FOV.RADECBOXTYPE: ################################################################## ### Compare star vector to [RA,Dec] box ra,dec = parse_inertial(vstar,return_radec=True)[:2] for ralo,rahi,declo,dechi in self.radec_boxes: if ra<ralo: continue if ra>rahi: continue if dec<declo: continue if dec>dechi: continue return True,sp.radrec(1.,rpd*ra,rpd*dec) return False,None ### Get inertial star unit vector without RA,Dec uvinertial = uvraw = parse_inertial(vstar,return_radec=False) ### Corrections for direction to star ### - Assume all corrections are small and can be applied in units ### of radians to a unit vector in a plane perpendicular to the ### vector to the star ### - Proper Motion (PM) ### - Uses PM in RA and Dec only, not radial velocity and parallax if (not (None is self.obs_year) ) and (not (None in (pmra_maspy,pmdec_maspy,)) ) and (pmra_maspy != 0.0 or pmdec_maspy != 0.0): ### - Unit vectors E and N in plane perpendicular to star vector uveast = sp.ucrss([0,0,1],uvraw) uvnorth = sp.ucrss(uvraw,uveast) ### - Scale unit vectors in radians, and add to nominal vector ### - pmra_maspy from Gaia includes factor of secant(Declination) uvinertial = sp.vhat(sp.vlcom3(self.obs_year*rpmas*pmdec_maspy,uvnorth ,self.obs_year*rpmas*pmra_maspy,uveast ,1.0,uvinertial ) ) ### - Parallax if (not (None is self.obs_pos) ) and (not (None is parallax_maspau) ) and (parallax_maspau != 0.0): ### - Scale observer position, by parallax in mas/AU, then scale ### to radians (since star vector is unit vector), make that the ### new origin of the vector uvinertial = sp.vhat(sp.vsub(uvinertial ,sp.vscl(aupkm*parallax_maspau*rpmas,self.obs_pos) ) ) ### - Stellar Aberration if not (None is self.obs_vel): ### - Scale observer velocity by reciprocal of the speed of light, ### add result to unit vector toward star. uvinertial = sp.vhat(sp.vadd(uvinertial ,sp.vscl(recip_clight,self.obs_vel) ) ) if self.fovtype == FOV.CIRCLETYPE: ################################################################## ### Compare inertial star vector to circular FOV return sp.vdot(uvinertial,self.uv_cone_axis) >= self.min_cosine,uvinertial assert FOV.POLYGONTYPE == self.fovtype,'Unknown FOV type [{0}]'.format(self.fovtype) #################################################################### ### Compare star vector to polygonal FOV if self.is_convex(): ### Convex FOV: a negative dot product with the inward-pointing ### normal to any side indicates star is outside FOV for inwardnorm in self.inwardsidenorms: if sp.vdot(uvinertial,inwardnorm) < 0.0: return False,uvinertial ### All dot products were non-negative: star is within FOV return True,uvinertial ### Rotate inertial unit vector to local reference frame (reffrm) uvlocalstar = self.rotate_to_local(uvinertial) ### Scale to Z=unity if uvlocalstar[2] < 1e-15: return False,uvinertial z1star = sp.vscl(1.0/uvlocalstar[2],uvlocalstar) ### Setup .localxyzs and .fovsides if None is self.localxyzs: self.setup_localxyzs() ### Count number of crossings of FOV sides count = len([None for fovside in self.fovsides if fovside.right_of(z1star) ]) return ((count&1) and True or False),uvinertial