Exemple #1
0
class constants(object):
    # acceleration of gravity at the equator
    g_equ = 9.780 * u.m / u.s / u.s
    # acceleration of gravity at the pole
    g_pol = 9.8321849378 * u.m / u.s / u.s
    # acceleration of gravity at 45deg lat
    g_45 = 9.8306 * u.m / u.s / u.s
    # acceleration of gravity [m/s/s]
    g = 9.81 * u.m / u.s / u.s
    # (mean) Earth angular velocity [rad/s]
    w_pla = (2 * np.pi) / 86164.0 * u.rad / u.s
    # Earth equatorial radius [m], from SPICE
    R_pla = sp.bodvrd("EARTH", "RADII", 3)[1][0] * u.km
    # shape of the Earth ellipsoid [m,m,m], from SPICE
    abc_pla = sp.bodvrd("EARTH", "RADII", 3)[1] * u.km
    # flatness coefficient (frmm SPICE)
    f_pla = (abc_pla[1] - abc_pla[2]) / abc_pla[1]
    # default meteoroid volumic mass [kg/m^3]
    rhometeor = 3000.0 * u.kg / (u.m * u.m * u.m)
    # altitude below which the Dark Flight is computed
    max_alt_DF = 40 * u.km
    # Dark Flight: integration step in altitude [m]
    dh = -100.0 * u.m
    # altitude below which the height above ground is computed (DrkFlgt)
    gnd_alt_thld = 5000.0 * u.m
    # default number of clones for the dark flight computation
    nclone = 10
def cassini_titan_altlatlon(tempdatetime):
    et = spice.datetime2et(tempdatetime)

    state, ltime = spice.spkezr('CASSINI', et, 'IAU_TITAN', 'NONE', 'TITAN')
    lon, lat, alt = spice.recpgr('TITAN', state[:3], spice.bodvrd('TITAN', 'RADII', 3)[1][0], 2.64e-4)

    return alt, lat * spice.dpr(), lon * spice.dpr()
Exemple #3
0
def nearest_moon(dt, target='CASSINI'):
    """

    :param dt:
    :param target:
    :return:
    """
    et = spice.datetime2et(dt)
    moondict = {}
    NAIFIDS = range(601, 654, 1)

    for x in NAIFIDS:
        moon = spice.bodc2s(x)

        frame = 'IAU_' + moon.upper()
        observ = moon.upper()
        corrtn = 'NONE'

        state, ltime = spice.spkpos(target, et, frame, corrtn, observ)
        # TODO do full calculation of altitude
        lon, lat, alt = spice.recpgr(
            moon.upper(), state,
            spice.bodvrd(moon.upper(), 'RADII', 3)[1][0], 2.64e-4)
        moondict[moon] = alt
    sorteddict = OrderedDict(sorted(moondict.items(), key=lambda t: t[1]))

    return sorteddict
Exemple #4
0
def GM(body):
    """
    Use SPICE software to get the product of the gravitational constant and the
    mass of <body>.
    """
    _, GM = spice.bodvrd(body, "GM", 1)
    return GM[0]*1e9 # convert km**2 to m**2
Exemple #5
0
    def n_body_equation(self, t, y):
        """
        n体问题微分方程 y_dot = f(t, y)
        见英文版教材P117
        :self.PARAM t: 对应tdb时刻
        :self.PARAM y: 对应时刻人造卫星的状态向量[x, y, z, vx, vy, vz]
        :return: y_dot
        """
        y_dot = np.empty((6, ))
        y_dot[:3] = y[3:]

        r_mex = y[:3]
        r_central = spice.spkezr(self.PARAM["Central_Body"], t,
                                 self.PARAM["Ref_Frame"], 'None',
                                 self.PARAM["Ref_Body"])[0][:3]
        r_central_mex = r_mex - r_central
        GM_central = spice.bodvrd(self.PARAM["Central_Body"], "GM", 1)[1][0]
        a_central = -GM_central * r_central_mex / norm(r_central_mex)**3
        perturbations = sum([
            self.perturbation(name, r_mex, t)
            for name in self.PARAM["Perturbation_Bodies"]
        ])
        y_dot[3:] = a_central + perturbations

        if self.PARAM["Radiation"]:
            radiation_pressure = self.solar_radiation_pressure(t, y)
            y_dot[3:] += radiation_pressure

        return y_dot  # v, a
def Location(et, ingress, sv, when):
    Coords = np.ones(3)
    [tgopos, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE',
                               sv.target)
    [mexpos, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.obs)
    [states, _] = spice.spkezr(sv.target, et - when, sv.fframe, 'NONE', sv.obs)
    sc2scvector = states[0:3]
    velocity = states[3:6]
    relativespeed = np.linalg.norm(velocity)
    # e9 because we are converting from km to m (SPICE outputs km, but constants in m)
    veldopp = (relativespeed / constants.c) * 437.1e9
    displacement = np.linalg.norm(sc2scvector)
    sc2scunitvector = np.true_divide(sc2scvector, displacement)
    # Extract the triaxial dimensions of Mars
    marsrad = spice.bodvrd(sv.front, 'RADII', 3)
    # For the ray that connects MEX and TGO, find the point on this ray that is closest to the Martian surface
    [nearestpoint, alt] = spice.npedln(marsrad[1][0], marsrad[1][1],
                                       marsrad[1][2], tgopos, sc2scunitvector)
    # THERE IS MORE SETTINGS ON THIS
    [radius, lon, lat] = spice.reclat(nearestpoint)
    # Rad -> Deg , frame inversion required (hence the negative 180)
    lon = 180 - (lon * (-180 / math.pi))
    lat = lat * (-180 / math.pi)

    MexNadirTGOAngle = spice.vsep(-mexpos, -sc2scvector)
    MexNadirTGOAngle = MexNadirTGOAngle * (180 / math.pi)

    # produce a string of the date and time, because an ephemeris time is not human-readable
    date_time = spice.timout(et, 'MM-DD HR:MN:SC')
    ingress_date_time = spice.timout(ingress, 'MM-DD HR:MN:SC')
    return lon, lat, displacement, nearestpoint, alt, relativespeed, date_time, ingress_date_time, veldopp, MexNadirTGOAngle
def occSurfaceTrace(et, sv):
    altTrace = 0
    marsrad = spice.bodvrd(sv.front, 'RADII', 3)
    trace = np.zeros([600, 2])
    for i in range(600):  # Find relative positions of TGO and MEX
        [targetpos, _] = spice.spkpos(sv.front, et - i, sv.fframe, 'NONE',
                                      sv.target)
        [sc2scvector, _] = spice.spkpos(sv.target, et - i, sv.fframe, 'NONE',
                                        sv.obs)
        [obspos, _] = spice.spkpos(sv.front, et - i, sv.fframe, 'NONE', sv.obs)

        # Find the unit vector between the SCs
        displacement = math.sqrt(((sc2scvector[0])**2) +
                                 ((sc2scvector[1])**2) + ((sc2scvector[2])**2))
        unitvector = np.true_divide(sc2scvector, displacement)

        # Find the point this unit vector is closest to the Mars
        [rectangularCoords,
         alt] = spice.npedln(marsrad[1][0], marsrad[1][1], marsrad[1][2],
                             targetpos, unitvector)
        [radius, lon, lat] = spice.reclat(rectangularCoords)
        # Rad -> Deg , frame inversion required (hence the negative 180)
        trace[i, 1] = (lon * (-180 / math.pi))
        trace[i, 0] = lat * (-180 / math.pi)

        altTrace = np.append(altTrace, alt)

    altTrace = altTrace[1:]

    return trace, altTrace
Exemple #8
0
def GM(body):
    """
    Use SPICE software to get the product of the gravitational constant and the
    mass of <body>. Input is SPICE name, e.g. "SUN" or "MOON".
    """
    _, GM = spice.bodvrd(body, "GM", 1)
    return GM[0] * 1e9  # convert km to m
Exemple #9
0
def get_sun_sizes(utc_start, utc_end, step_size):
    """get sun angular size and time steps given start and end times"""
    #spice constants
    abcorr = "None"
    #tolerance = "1"
    #method = "Intercept: ellipsoid"
    #prec = 3
    #shape = "Ellipsoid"

    #load spiceypy kernels
    os.chdir(KERNEL_DIRECTORY)
    sp.furnsh(KERNEL_DIRECTORY + os.sep + METAKERNEL_NAME)
    print(sp.tkvrsn("toolkit"))
    os.chdir(BASE_DIRECTORY)
    utctimestart = sp.str2et(utc_start)
    utctimeend = sp.str2et(utc_end)

    durationseconds = utctimeend - utctimestart
    nsteps = int(np.floor(durationseconds / step_size))
    timesteps = np.arange(nsteps) * step_size + utctimestart

    ref = "J2000"
    observer = "-143"
    target = "SUN"
    #get TGO-SUN pos
    tgo2sunpos = [
        sp.spkpos(target, time, ref, abcorr, observer)[0] for time in timesteps
    ]
    sunaxes = sp.bodvrd("SUN", "RADII", 3)[1][0]  #get mars axis values

    return ([np.arctan((sunaxes*2.0)/np.linalg.norm(tgo2sunVector))*sp.dpr()*60.0 \
                for tgo2sunVector in tgo2sunpos], timesteps)
Exemple #10
0
 def target_body_radii(self):
     """
     Returns
     -------
     : list<double>
       Radius of all three axis of the target body
     """
     rad = spice.bodvrd(self.target_name, 'RADII', 3)
     return rad[1]
    def fetch_radii(self, targets):
        targets = [self.fromName(t) for t in targets]
        radii = [10] * len(targets)
        for k, target in enumerate(targets):
            try:
                radii[k] = spiceypy.bodvrd(target, 'RADII', 3)[1][0]
            except spiceypy.utils.exceptions.SpiceKERNELVARNOTFOUND:
                pass

        return radii
Exemple #12
0
def getNorthPoleAngle(target, position, C, B, camera):
    """
    Get angle north pole of target makes with image y-axis, in radians.
    """

    # get target spin axis
    # the last row of the matrix is the north pole vector, *per spice docs*
    # seems correct, as it's nearly 0,0,1
    Bz = B[2]
    print 'Bz=north pole spin axis',Bz

    # get target radius, km
    nvalues, radii = spice.bodvrd(target, 'RADII', 3)
    targetRadiusEquator = (radii[0] + radii[1]) / 2
    targetRadiusPoles = radii[2]
    targetRadius = sum(radii) / 3
    # flatteningCoefficient = (targetRadiusEquator - targetRadiusPoles) / targetRadiusEquator
    # print 'target radius in km', targetRadius

    # get north pole location
    positionNP = position + targetRadius * Bz
    print 'positionNP=north pole in world coords', positionNP

    # get target position in camera space
    c = np.dot(C, position)
    cNP = np.dot(C, positionNP)
    print 'c=position in camera space',c
    print 'cNP=north pole in camera space',cNP

    # get camera fov and focal length
    fovDegrees = config.cameraFOVs[camera] # 0.424 or 3.169 deg
    fovRadians = fovDegrees * math.pi / 180
    f = 1.0 / math.tan(fovRadians/2) # focal length (relative to screen halfwidth of 1.0)
    print 'f=focal length',f

    # get camera-to-screen matrix S
    cz = c[2]
    fz = f/cz
    # print 'fz=f/cz',fz
    S = np.array([[fz,0,0],[0,fz,0]])

    # get screen coordinate (-1 to 1, -1 to 1)
    s = np.dot(S, c)
    sNP = np.dot(S, cNP)
    # ie sx=cx*f/cz; sy=cy*f/cz
    print 's=screen space (-1 to 1)',s
    print 'sNP=screen space north pole (-1 to 1)',sNP

    # get angle between north pole and image y-axis
    npDelta = sNP-s
    npRadians = math.atan(npDelta[0]/npDelta[1])
    npAngle = npRadians * 180/math.pi
    print 'npAngle',npAngle

    return npRadians
def producegeometrylamda(et, sv, when):
    [TGO, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.target)
    [MEX, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.obs)

    dist = math.floor(spice.vdist(TGO, MEX))
    print(dist)
    # NEED TO PRODUCE VECTOR OF SZA AND HEIGHTS THAT ARE 'DIST' LONG [13242.9 m] *comp expensive
    # start by making DIST length vector, 3 height, for every meter from mex to tgo
    # MAYBE FIND THE UNIT VECTOR AND ADD ONE IN ITS DIRECTION!!
    angleseparation = (spice.vsep(MEX, TGO)) * (180 / math.pi
                                                )  # angle taken a mars center
    initialangle = (spice.vsep(-MEX, (TGO - MEX))) * (
        180 / math.pi
    )  # angle taken at mars-MEX-tgo, that points to tgo. needed for the bending functions original starting angle
    #script needs to work via periods of ray and not meters. [totalperiods is the main iterable, not meters]
    vacuumwavelength = constants.c / 437.1e6
    scale = 1  # scale =10, means we are itertating per 100 wavelenghts instead of 1000 (default 1000 because SPICE works in km)
    wavelengthsinameter = 1 / vacuumwavelength
    a = wavelengthsinameter * dist * scale
    total1000periods = math.floor(a)  # ~that many thousands of periods
    remainingdistance = (vacuumwavelength / scale) * (
        (wavelengthsinameter * dist * scale) - total1000periods
    )  # quanitfy the remaineder, this distance can
    # added later, this remaining portion is extreamly high altitude (near Target) and has no refractive effects. therfor simply added (km)
    #total1000periods = total1000periods.astype(int)

    sc2sc = TGO - MEX
    norm = np.linalg.norm(sc2sc)
    unitsc2sc = sc2sc / (norm * vacuumwavelength * scale
                         )  #this needs to shrink if the repeatable expands
    points = np.empty([3, total1000periods])
    sza = np.empty([1, total1000periods])

    marsrad = spice.bodvrd(sv.front, 'RADII', 3)
    flatteningcoefficient = (marsrad[1][0] - marsrad[1][2]) / marsrad[1][0]
    equatorialradii = marsrad[1][0]
    # find direction of sun, it will not change much during the occultation. so only calc it once
    [SUN, _] = spice.spkpos(sv.front, et, sv.fframe, 'NONE', 'SUN')
    for i in range(total1000periods):
        point = MEX + (
            i * unitsc2sc
        )  #move along ray, 1000 wavelength distance at a time (685 m). but unitsc2sc is in km...
        sza[0, i] = spice.vsep(SUN, point)
        points[:, i] = spice.recgeo(point, equatorialradii,
                                    flatteningcoefficient)
        points[0, i] = (points[0, i] * (-180 / math.pi))
        points[1, i] = (points[1, i] * (-180 / math.pi))

        print((i / math.floor(total1000periods)) * 100)

    ray = np.concatenate((points, sza), axis=0)
    print('stop here')

    return ray, dist, angleseparation, initialangle, total1000periods, vacuumwavelength, remainingdistance
def producegeometrymeter(et, sv, when):
    [TGO, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.target)
    [MEX, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.obs)

    dist = math.floor(spice.vdist(TGO, MEX))
    print(dist)
    # NEED TO PRODUCE VECTOR OF SZA AND HEIGHTS THAT ARE 'DIST' LONG [13242.9 m] *comp expensive
    # start by making DIST length vector, 3 height, for every meter from mex to tgo
    # MAYBE FIND THE UNIT VECTOR AND ADD ONE IN ITS DIRECTION!!
    angleseparation = (spice.vsep(MEX, TGO))  # angle taken a mars center
    initialangle = (spice.vsep(-MEX, (TGO - MEX))) * (
        180 / math.pi
    )  # angle taken at mars-MEX-tgo, that points to tgo. needed for the bending functions original starting angle
    #script needs to work via periods of ray and not meters. [totalperiods is the main iterable, not meters]

    scale = 0.1  # scale =10, means we are itertating per 100 wavelenghts instead of 1000 (default 1000 because SPICE works in km)

    dist = math.floor(dist)  # km

    sc2sc = TGO - MEX
    norm = np.linalg.norm(sc2sc)
    unitsc2sc = sc2sc / norm  #this needs to shrink if the repeatable expands
    points = np.empty([3, dist])
    sza = np.empty([1, dist])
    angleprogression = np.empty([1, dist])

    xyzpoints = np.zeros([3, dist])
    marsrad = spice.bodvrd(sv.front, 'RADII', 3)
    flatteningcoefficient = (marsrad[1][0] - marsrad[1][2]) / marsrad[1][0]
    equatorialradii = marsrad[1][0]
    # find direction of sun, it will not change much during the occultation. so only calc it once
    [SUN, _] = spice.spkpos(sv.front, et, sv.fframe, 'NONE', 'SUN')
    for i in range(dist):
        xyzpoint = MEX + (
            i * unitsc2sc
        )  #move along ray, 1000 wavelength distance at a time (685 m). but unitsc2sc is in km...
        xyzpoints[:, i] = xyzpoint
        sza[0, i] = spice.vsep(SUN, xyzpoint)
        angleprogression[0, i] = (spice.vsep(xyzpoint, MEX)) * (180 / math.pi)
        points[:, i] = spice.recgeo(xyzpoint, equatorialradii,
                                    flatteningcoefficient)
        points[0, i] = (points[0, i] * (-180 / math.pi))
        points[1, i] = (points[1, i] * (-180 / math.pi))

        print((i / math.floor(dist)) * 100)

    ray = np.concatenate((points, sza), axis=0)

    #plt.plot(angleprogression[0,:], ray[2,:])
    #plt.show()

    # ray is in lat/lon/alt + sza and xyzpoints is cartesian, both describe the same thing
    return ray, dist, unitsc2sc, angleseparation, initialangle, MEX, TGO, xyzpoints, angleprogression
Exemple #15
0
    def target_body_radii(self):
        """
        Returns a list containing the radii of the target body
        Expects target_name to be defined. This must be a string containing the name
        of the target body

        Returns
        -------
        : list<double>
          Radius of all three axis of the target body
        """
        rad = spice.bodvrd(self.target_name, 'RADII', 3)
        return rad[1]
Exemple #16
0
def cassini_altlatlon(utc, target='TITAN', output=False):
    state = cassini_phase(utc)

    lon, lat, alt = spice.recpgr(target, state[:3],
                                 spice.bodvrd(target, 'RADII', 3)[1][0],
                                 2.64e-4)

    if output:
        print("ALtitude", alt)
        print("Latitude", lat * spice.dpr())
        print("Longtitude", lon * spice.dpr())

    return alt, lat * spice.dpr(), lon * spice.dpr()
Exemple #17
0
    def body_shadow_function(self, r_mex, name, t):
        """
        计算阴影函数(shadow function)见英文教材P81 3.4.2
        :self.PARAM r_mex: 人造卫星(MEX)的位置向量
        :self.PARAM name: 遮挡天体的名称
        :self.PARAM t: TDB时刻
        :return: 阴影函数v
        """
        r_body = spice.spkezr(name, t, self.PARAM["Ref_Frame"], 'None',
                              self.PARAM["Ref_Body"])[0][:3]
        r_sun = spice.spkezr("Sun", t, self.PARAM["Ref_Frame"], 'None',
                             self.PARAM["Ref_Body"])[0][:3]
        r_body_mex = r_mex - r_body
        r_mex_sun = r_sun - r_mex

        R_body = spice.bodvrd(name, "RADII", 3)[1][0]
        RS = spice.bodvrd("Sun", "RADII", 3)[1][0]
        a = asin(RS / norm(r_mex_sun))
        b = asin(R_body / norm(r_body_mex))
        c = self.agl_between(-1 * r_body_mex, r_mex_sun)
        v = self.shadow_function(a, b, c)
        return v
Exemple #18
0
def orbital_elements(date, observer, target, frame='J2000', abcorr='LT+S'):
    state = get_state(date, observer, target, frame, abcorr)

    et = spiceypy.str2et(date)

    mu = spiceypy.bodvrd(observer, 'GM', 1)[1][0]

    #
    # Compute the orbital elements
    #
    elements = spiceypy.oscltx(state, et, mu)

    return elements
def producegeometrymeter(et, sv, when):

    [TGO, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.target)
    [MEX, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE', sv.obs)

    dist = math.floor(spice.vdist(TGO, MEX))

    angleseparation = (spice.vsep(MEX, TGO))  # angle taken a mars center
    initialangle = (spice.vsep(-MEX, (TGO - MEX))) * (
        180 / math.pi
    )  # angle taken at mars-MEX-tgo, that points to tgo. needed for the bending functions original starting angle
    #script needs to work via periods of ray and not meters. [totalperiods is the main iterable, not meters]

    sc2sc = TGO - MEX
    norm = np.linalg.norm(sc2sc)
    unitsc2sc = sc2sc / norm  #this needs to shrink if the repeatable expands
    points = np.empty([3, dist])
    sza = np.empty([1, dist])
    angleprogression = np.empty([1, dist])

    xyzpoints = np.zeros([3, dist])
    marsrad = spice.bodvrd(sv.front, 'RADII', 3)
    flatteningcoefficient = (marsrad[1][0] - marsrad[1][2]) / marsrad[1][0]
    equatorialradii = marsrad[1][0]
    # find direction of sun, it will not change much during the occultation. so only calc it once
    [SUN, _] = spice.spkpos(sv.front, et, sv.fframe, 'NONE', 'SUN')
    for i in range(dist):
        xyzpoint = MEX + (
            i * unitsc2sc
        )  #move along ray, 1000 wavelength distance at a time (685 m). but unitsc2sc is in km...
        xyzpoints[:, i] = xyzpoint
        sza[0, i] = spice.vsep(SUN, xyzpoint)
        angleprogression[0, i] = (spice.vsep(xyzpoint, MEX)) * (180 / math.pi)
        points[:, i] = spice.recgeo(xyzpoint, equatorialradii,
                                    flatteningcoefficient)
        points[0, i] = (points[0, i] * (-180 / math.pi))
        points[1, i] = (points[1, i] * (-180 / math.pi))

    ray = np.concatenate((points, sza),
                         axis=0)  # important for when sza is included

    #plt.plot(angleprogression[0,:], ray[2,:])
    #plt.show()

    # ray is in lat/lon/alt + sza and xyzpoints is cartesian, both describe the same thing
    return initialangle, MEX, TGO, xyzpoints
def TangentPointAltitude(time):
    epoch = 636491202.20059

    target = '-143'
    obs = '-41'
    alt = np.zeros(len(time) + 1)
    for i in time:
        [tgo, _] = spice.spkpos('MARS', epoch - i, 'IAU_MARS', 'NONE', target)
        [mex, _] = spice.spkpos('MARS', epoch - i, 'IAU_MARS', 'NONE', obs)
        [states, _] = spice.spkezr(target, epoch - i, 'IAU_MARS', 'NONE', obs)
        sc2scvector = states[0:3]
        displacement = np.linalg.norm(sc2scvector)
        sc2scunitvector = np.true_divide(sc2scvector, displacement)
        marsrad = spice.bodvrd('MARS', 'RADII', 3)
        _, alt[i] = spice.npedln(marsrad[1][0], marsrad[1][1], marsrad[1][2],
                                 tgo, sc2scunitvector)
    return alt * 1000
def get_radius(bod_id):
    """
    aether-rest-server.py -- get_radius
        This function simply gets the radii of the body specified by bod_id.

    Params: bod_id <str> -- the NAIF ID of the body for which min and max speeds shall be obtained. Optionally, a valid
                body name may be passed instead.

    Returns: list[<float>, <float>, <float>]  -- TODO
    """

    try:
        # get radii from SPICE
        return spice.bodvrd(bod_id, "RADII", 3)[1].tolist()
    # This should never be hit, since this function is only called on bodies which have radius data, but just in case...
    except:
        return "NO RADIUS DATA AVAILABLE"
def get_mass(bod_id):
    """
    aether-rest-server.py -- get_mass
        This function simply gets the mass in kg of the body specified by bod_id.

    Params: bod_id <str> -- the NAIF ID of the body for which min and max speeds shall be obtained. Optionally, a valid
                body name may be passed instead.

    Returns: <float> the mass of the body in kg
    """

    # the gravitational constant
    G = 6.67430e-11

    try:
        # return mass from SPICE -- G is divided so the result is in kg
        return float(spice.bodvrd(bod_id, "GM", 1)[1][0] / (G / 1000000000))
    # This should never be hit, since this function is only called on bodies which have mass data, but just in case...
    except:
        return "NO MASS DATA AVAILABLE"
Exemple #23
0
def cassini_altitude(dt, moon, target='CASSINI'):
    """

    :param dt:
    :param moon:
    :param target:
    :return:
    """
    et = spice.datetime2et(dt)
    frame = 'IAU_' + moon.upper()
    observ = moon.upper()
    corrtn = 'NONE'

    state, ltime = spice.spkpos(target, et, frame, corrtn, observ)
    # TODO do full calculation of altitude
    lon, lat, alt = spice.recpgr(moon.upper(), state,
                                 spice.bodvrd(moon.upper(), 'RADII', 3)[1][0],
                                 2.64e-4)

    return alt
Exemple #24
0
def perturbation(name, r_mex, t):
    """
    计算摄动天体加速度
    :param name: 摄动天体名称
    :param r_mex: 人造卫星(MEX)的位置向量
    :param t: TDB时刻
    :return: 对应天体摄动加速度加速度[ax, ay, az]
    """
    r_body = spice.spkezr(name, t, PARAM["Ref_Frame"], 'None',
                          PARAM["Ref_Body"])[0][:3]
    r_cent = spice.spkezr(PARAM["Central_Body"], t, PARAM["Ref_Frame"], 'None',
                          PARAM["Ref_Body"])[0][:3]

    GM_body = spice.bodvrd(name, "GM", 1)[1][0]

    r_body_mex = r_mex - r_body
    r_cent_body = r_body - r_cent

    a = -GM_body * (r_cent_body / norm(r_cent_body)**3 +
                    r_body_mex / norm(r_body_mex)**3)
    return a
def Location(et, sv, when):
    Coords = np.ones(3)
    [tgopos, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE',
                               sv.target)
    [mexpos, _] = spice.spkpos(sv.front, et - when, sv.fframe, 'NONE',
                               sv.target)
    [sc2scvector, _] = spice.spkpos(sv.target, et - when, sv.fframe, 'NONE',
                                    sv.obs)
    displacement = np.linalg.norm(sc2scvector)
    sc2scunitvector = np.true_divide(sc2scvector, displacement)
    marsrad = spice.bodvrd(sv.front, 'RADII',
                           3)  # Extract the triaxial dimensions of Mars
    # For the ray that connects MEX and TGO, find the point on this ray that is closest to the Martian surface
    [nearestpoint, alt] = spice.npedln(marsrad[1][0], marsrad[1][1],
                                       marsrad[1][2], tgopos, sc2scunitvector)
    [radius, lon, lat] = spice.reclat(nearestpoint)
    lon = lon * (
        -180 / math.pi
    )  # Rad -> Deg , frame inversion required (hence the negative 180)
    lat = lat * (-180 / math.pi)
    #Coords[0] = lon
    #Coords[1] = lat
    return lon, lat, displacement, nearestpoint, alt
Exemple #26
0
 def R(self):
     if self._R is None:
         results = spice.bodvrd(self.name, "RADII", 3)
         self._R = np.mean(results[1]) * u.km
     return self._R
def get_isd(label):
    """
    TODO: This function (and all like it) needs to open with some robust method to make sure this is
          in fact an MDIS label.

    """

    instrument_name = {
        'MDIS-NAC': 'MSGR_MDIS_NAC',
        'MERCURY DUAL IMAGING SYSTEM NARROW ANGLE CAMERA': 'MSGR_MDIS_NAC',
        'MERCURY DUAL IMAGING SYSTEM WIDE ANGLE CAMERA': 'MSGR_MDIS_WAC'
    }

    metakernel_dir = config.mdis
    mks = sorted(glob(os.path.join(metakernel_dir, '*.tm')))

    instrument_id = instrument_name[label['INSTRUMENT_ID']]
    spacecraft_name = label['MISSION_NAME']
    target_name = label['TARGET_NAME']
    time = label['START_TIME']

    messenger_mk = None
    for mk in mks:
        if str(time.year) in os.path.basename(mk):
            messenger_mk = mk

    spice.furnsh(messenger_mk)

    # Spice likes ids over names, so grab the ids from the names
    spacecraft_id = spice.bods2c(spacecraft_name)
    ikid = spice.bods2c(instrument_id)

    # Load the instrument and target metadata into the ISD
    reference_frame = 'IAU_{}'.format(target_name)

    isd = {}

    rad = spice.bodvrd(target_name, 'RADII', 3)
    isd['radii'] = {}
    isd['radii']['semimajor'] = rad[1][0]
    isd['radii']['semiminor'] = rad[1][1]

    isd['optical_distortion'] = {}
    odk_mssgr_x = spice.gdpool('INS{}_OD_T_X'.format(ikid), 0, 10)
    odk_mssgr_y = spice.gdpool('INS{}_OD_T_Y'.format(ikid), 0, 10)

    isd['optical_distortion']['x'] = list(odk_mssgr_x)
    isd['optical_distortion']['y'] = list(odk_mssgr_y)

    isd['focal2pixel_samples'] = list(
        spice.gdpool('INS{}_TRANSX'.format(ikid), 0, 3))
    isd['focal2pixel_lines'] = list(
        spice.gdpool('INS{}_TRANSY'.format(ikid), 0, 3))

    # Load information from the IK kernel
    isd['focal_length_model'] = {}

    focal_legnth_coeffs = spice.gdpool('INS{}_FL_TEMP_COEFFS '.format(ikid), 0,
                                       5)
    isd['focal_length_model']['focal_length'] = focal_length_from_temp(
        label['FOCAL_PLANE_TEMPERATURE'].value, focal_legnth_coeffs)

    isd['focal_length_model']['focal_length_epsilon'] = float(
        spice.gdpool('INS{}_FL_UNCERTAINTY'.format(ikid), 0, 1)[0])

    isd['image_lines'] = int(
        spice.gipool('INS{}_PIXEL_LINES'.format(ikid), 0, 1)[0])
    isd['image_samples'] = int(
        spice.gipool('INS{}_PIXEL_SAMPLES'.format(ikid), 0, 1)[0])

    isd['starting_detector_sample'] = int(
        spice.gdpool('INS{}_FPUBIN_START_SAMPLE'.format(ikid), 0, 1)[0])
    isd['starting_detector_line'] = int(
        spice.gdpool('INS{}_FPUBIN_START_LINE'.format(ikid), 0, 1)[0])

    # Now time
    sclock = label['SPACECRAFT_CLOCK_START_COUNT']
    exposure_duration = label['EXPOSURE_DURATION'].value
    exposure_duration = exposure_duration * 0.001  # Scale to seconds

    # Get the instrument id, and, since this is a framer, set the time to the middle of the exposure
    start_et = spice.scs2e(spacecraft_id, sclock)
    start_et += (exposure_duration / 2.0)

    end_et = spice.scs2e(
        spacecraft_id,
        label['SPACECRAFT_CLOCK_STOP_COUNT']) + (exposure_duration / 2.0)
    del_et = end_et - start_et
    et = (start_et + end_et) / 2

    isd['starting_ephemeris_time'] = start_et
    isd['dt_ephemeris'] = del_et
    isd['number_of_ephemerides'] = 1
    isd['interpolation_method'] = 'lagrange'
    isd['center_ephemeris_time'] = et

    # Get the rotation angles from MDIS NAC frame to Mercury body-fixed frame
    camera2bodyfixed = spice.pxform(instrument_id, reference_frame, et)
    quat = spice.m2q(camera2bodyfixed)

    isd['sensor_orientation'] = list(quat)

    # Get the Sensor Position
    loc, _ = spice.spkpos(target_name, et, reference_frame, 'LT+S',
                          spacecraft_name)

    isd['sensor_location'] = {}
    isd['sensor_location']['x'] = loc[0]
    isd['sensor_location']['y'] = loc[1]
    isd['sensor_location']['z'] = loc[2]
    isd['sensor_location']['unit'] = 'm'

    # Get the velocity
    v_state, lt = spice.spkezr(spacecraft_name, et, reference_frame, 'NONE',
                               target_name)

    isd['sensor_velocity'] = {}
    isd['sensor_velocity']['x'] = v_state[3]
    isd['sensor_velocity']['y'] = v_state[4]
    isd['sensor_velocity']['z'] = v_state[5]
    isd['sensor_velocity']['unit'] = 'm'
    isd['reference_height'] = {}
    isd['reference_height']['minheight'] = label.get('min_valid_height', -8000)
    isd['reference_height']['maxheight'] = label.get('max_valid_height', 8000)
    isd['reference_height']['unit'] = 'KM'

    # Get the sun position
    sun_state, lt = spice.spkezr("SUN", et, reference_frame, 'NONE',
                                 target_name)

    # Get the sun position, convert to meters
    xpos, ypos, zpos = [e.value for e in label['SC_SUN_POSITION_VECTOR']]
    xvel, yvel, zvel = [e.value for e in label['SC_SUN_VELOCITY_VECTOR']]

    # lighttime should always be off
    isd['sun_position'] = {}
    isd['sun_position']['x'] = sun_state[0]
    isd['sun_position']['y'] = sun_state[1]
    isd['sun_position']['z'] = sun_state[2]

    isd['sun_velocity'] = {}
    isd['sun_velocity']['x'] = sun_state[3]
    isd['sun_velocity']['y'] = sun_state[4]
    isd['sun_velocity']['z'] = sun_state[5]
    return isd
Exemple #28
0
def EarthRepeatOrbits(jk,
                      e,
                      Variable,
                      VarType,
                      isHighFidelity=False,
                      printStatus=False):
    """Find a set of repeating ground track orbits around Earth (Earth-repeat orbits)."""

    # importing the required modules
    import spiceypy as spice
    import math
    import numpy as np

    Result = 0
    Nsolutions = 0

    # Error handling
    jkSize = jk.shape
    if (int(jkSize[1]) != 2):
        print(
            'Incorrect matrix size for jk matrix. Only two coloumns are required.'
        )
        return Result
    elif (VarType != 'Alti') and (VarType != 'Inclin'):
        print(
            'Inrecognized input for the argument specifying the variable type.'
        )
        return Result
    elif e >= 1.0 or e < 0:
        print(
            'Only circular or elliptical orbits are possible. Check the value of eccentricity.'
        )
        return Result
    elif (Variable[1] - Variable[0]) % Variable[2] != 0:
        print(
            'Integer number of steps are not possible. Check inputs for the argument - "Variable".'
        )
        return Result

    # Extracting the parameters
    spice.furnsh("./External_files/Spice_kernels/kernel_load.txt")
    muE = spice.bodvrd('Earth', 'GM', 1)
    mu = muE[1][0]  # [km3/s2] for earth
    J2 = 1082.63E-6  #J2 for earth
    RE = spice.bodvrd('EARTH', 'RADII', 3)
    Re = RE[1][0]  # [km], Average radius of Earth
    De = 86164.1004  # [s], Sidereal day
    k2 = 0.75 * J2 * math.sqrt(mu) * Re * Re
    r2d = 1 / spice.rpd()  # Radian to degree conversion

    # Creating storage
    steps = int((Variable[1] - Variable[0]) / Variable[2]) + 1
    Result = np.zeros(int(jkSize[0]) * int(steps) * 6)
    Result.shape = [int(jkSize[0]), int(steps), 6]

    # Computations
    for count in range(0, jkSize[0]):  #looping over j and k values

        for rowCount in range(0, int(steps)):  #looping over variable values
            j = jk[count, 0]
            k = jk[count, 1]

            # Extracting the value of variable
            var = Variable[0] + rowCount * Variable[2]

            # Storing values known so far
            Result[count, rowCount, 0] = j
            Result[count, rowCount, 1] = k
            Result[count, rowCount, 2] = e
            Result[count, rowCount, 3] = var
            Result[count, rowCount, 4] = math.nan  # to be computed
            Result[count, rowCount, 5] = math.nan  # to be computed

            if isHighFidelity == False:
                if VarType == 'Alti':
                    a = (Re + var) / (1 - e)
                    T = 2 * math.pi * math.sqrt(a**3 / mu)

                    DeltaL1 = -2 * math.pi * (T / De)
                    # DeltaL2 = C1 * cosi
                    C1 = (-3 * math.pi * J2 * Re**2) / (a**2 * (1 - e**2)**2)
                    C2 = -2 * math.pi * k / j - DeltaL1
                    C3 = 2 * math.pi * k / j - DeltaL1

                    # Inverse cosine
                    if (C2 < 0) & (abs(C2 / C1) <= 1):
                        DeltaL2 = C2
                        i = math.acos(
                            DeltaL2 / C1) * r2d  # for i = [0, 90] deg
                        Result[count, rowCount, 4] = i
                        Nsolutions += 1
                    elif (C2 > 0) & (abs(C2 / C1) <= 1):
                        DeltaL2 = C2
                        i = math.acos(
                            DeltaL2 / C1) * r2d  # for i = (90, 180] deg
                        Result[count, rowCount, 4] = i
                        Nsolutions += 1
                    elif (C3 < 0) & (abs(C3 / C1) <= 1):
                        DeltaL2 = C3
                        i = math.acos(
                            DeltaL2 / C1) * r2d  # for i = [0, 90] deg
                        Result[count, rowCount, 5] = i
                        Nsolutions += 1
                    elif (C3 > 0) & (abs(C3 / C1) <= 1):
                        DeltaL2 = C3
                        i = math.acos(
                            DeltaL2 / C1) * r2d  # for i = (90, 180] deg
                        Result[count, rowCount, 5] = i
                        Nsolutions += 1
                elif VarType == 'Inclin':
                    print(
                        'Function is not defined for low fidelity + unknown inclination case.'
                    )
                    return Result
            else:
                if VarType == 'Alti':
                    a = (Re + var) / (1 - e)
                    C1 = -2 * k2 * (a**-3.5) * (
                        (1 - e**2)**-2) * De * r2d  # RAAN_dot = C1 * cos(i)
                    C2 = k2 * (a**-3.5) * (
                        (1 - e**2)**
                        -2) * De * r2d  # omega_dot = C2 *(5*(cosd(i))^2 -1)
                    C3 = k2 * (a**-3.5) * (
                        (1 - e**2)**
                        -1.5) * De * r2d  # M_dot = C3 *(3*(cosd(i))^2 -1)
                    n = De * r2d * math.sqrt(mu / a**3)

                    # For the quadratic equation A*x^2 + B*x +C = 0 with x = cos(i)
                    A = 5 * C2 + 3 * C3
                    B = C1 * j / k
                    C = n - 360 * j / k - (C2 + C3)
                    D = B**2 - 4 * A * C

                    if D >= 0:
                        if (D == 0) & (abs(-B / (2 * A)) <= 1):
                            i1 = math.acos(-B / (2 * A))
                            Result[count, rowCount, 4] = i1 * r2d
                            Nsolutions += 1
                        elif (D > 0):
                            x1 = (-B + math.sqrt(D)) / (2 * A)
                            x2 = (-B - math.sqrt(D)) / (2 * A)
                            if abs(x1) <= 1:
                                i1 = math.acos(x1)
                                Result[count, rowCount, 4] = i1 * r2d
                                Nsolutions += 1
                            if abs(x2) <= 1:
                                i2 = math.acos(x2)
                                Result[count, rowCount, 5] = i2 * r2d
                                Nsolutions += 1
                elif VarType == 'Inclin':

                    a0 = math.pow(
                        (mu * De**2 * k**2 / (4 * math.pi**2 * j**2)), 1 / 3)
                    iterations = 0
                    i = var
                    L_dot = 360

                    while True:
                        iterations = iterations + 1

                        RAAN_dot = -2 * k2 * math.pow(a0, -3.5) * math.cos(
                            i / r2d) * math.pow(1 - e**2, -2) * De * r2d
                        omega_dot = k2 * math.pow(
                            a0, -3.5) * (5 * (math.cos(i / r2d))**2 -
                                         1) * math.pow(1 - e**2, -2) * De * r2d
                        M_dot = k2 * math.pow(a0, -3.5) * (
                            3 * (math.cos(i / r2d))**2 - 1) * math.pow(
                                1 - e**2, -1.5) * De * r2d
                        n = (j / k) * (L_dot - RAAN_dot) - (omega_dot + M_dot)

                        a1 = (mu / (n / (De * r2d))**2)**(1 / 3)

                        if iterations > 1000:
                            a0 = a1
                            break
                        elif (abs(a1 - a0) < 10**(-10)):
                            a0 = a1
                            break
                        else:
                            a0 = a1

                    # Only tracking the feasible solutions i.e. positive altitudes
                    if ((a0 * (1 - e)) - Re) > 0:
                        Nsolutions += 1
                        Result[count, rowCount, 4] = (a0 * (1 - e)) - Re

    # Printing the status of solutions obtained
    if printStatus == True:
        print(Nsolutions,
              ' solutions are obtained for the Earth-repeat orbits.')

    return Result
Exemple #29
0
def geometry(et, bsight, target, frame, sensor, observer=''):

    if not observer:
        observer = sensor

    # Time tag [UTC]
    # pixel id [(x,y)]
    # corner id [(x,y)]

    # Requested geometry

    # lat lon intersection (planetocentric)
    # lat lon subspacecraft
    # lat lon subsolar
    # target distance intersection
    # target angular diameter
    # local solar time intersection
    # phase angle intersection
    # emission angle intersection
    # incidence angle intersection

    #
    # We retrieve the camera information using GETFOV. More info available:
    #
    #   https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/getfov_c.html
    #
    sensor_id = spiceypy.bodn2c(sensor)
    (shape, sensor_frame, ibsight, vectors,
     bounds) = spiceypy.getfov(sensor_id, 100)

    visible = spiceypy.fovtrg(sensor, target, 'ELLIPSOID', frame, 'LT+S',
                              observer, et)

    if not visible:
        return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

    tarid = spiceypy.bodn2c(target)

    n, radii = spiceypy.bodvrd(target, 'RADII', 3)
    re = radii[0]
    rp = radii[2]
    f = (re - rp) / re

    try:
        #
        # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/sincpt_c.html
        #
        # For each pixel we compute the possible intersection with the target, if
        # the target is intersected we then compute the illumination angles. We
        # use the following SPICE APIs: SINCPT and ILLUMF
        #
        #   https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/sincpt_c.html
        #   https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/illumf_c.html
        #
        (spoint, trgepc, srfvec) = \
            spiceypy.sincpt('ELLIPSOID', target, et, frame, 'LT+S', observer, sensor_frame, bsight)

        (tarlon, tarlat, taralt) = spiceypy.recgeo(spoint, re, f)
        tardis = spiceypy.vnorm(srfvec)

        #
        # Angular diameter
        #
        tarang = np.degrees(
            2 * np.arctan(max(radii) / spiceypy.vnorm(spoint + srfvec)))

        #
        # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/illumf_c.html
        #
        (trgenpc, srfvec, phase, incdnc, emissn, visiblef, iluminatedf) = \
             spiceypy.illumf('ELLIPSOID', target, 'SUN', et, frame, 'LT+S', observer, spoint)

        phase *= spiceypy.dpr()
        incdnc *= spiceypy.dpr()
        emissn *= spiceypy.dpr()

        #
        # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/et2lst_c.html
        #
        #    VARIABLE  I/O  DESCRIPTION
        #    --------  ---  --------------------------------------------------
        #    et         I   Epoch in seconds past J2000 epoch.
        #    body       I   ID-code of the body of interest.
        #    lon        I   Longitude of surface point (RADIANS).
        #    type       I   Type of longitude "PLANETOCENTRIC", etc.
        #    timlen     I   Available room in output time string.
        #    ampmlen    I   Available room in output `ampm' string.
        #    hr         O   Local hour on a "24 hour" clock.
        #    mn         O   Minutes past the hour.
        #    sc         O   Seconds past the minute.
        #    time       O   String giving local time on 24 hour clock.
        #    ampm       O   String giving time on A.M./ P.M. scale.
        (hr, mn, sc, ltime, ampm) = \
            spiceypy.et2lst(et, tarid, tarlon, 'PLANETOCENTRIC', 80, 80)

        #
        # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/subpnt_c.html
        #
        #    Variable  I/O  Description
        #    --------  ---  --------------------------------------------------
        #    method     I   Computation method.
        #    target     I   Name of target body.
        #    et         I   Epoch in TDB seconds past J2000 TDB.
        #    fixref     I   Body-fixed, body-centered target body frame.
        #    abcorr     I   Aberration correction flag.
        #    obsrvr     I   Name of observing body.
        #    spoint     O   Sub-observer point on the target body.
        #    trgepc     O   Sub-observer point epoch.
        #    srfvec     O   Vector from observer to sub-observer point
        #
        (spoint, trgepc, srfev) = \
            spiceypy.subpnt('INTERCEPT/ELLIPSOID', target, et, frame, 'LT+S', observer)

        (sublon, sublat, subalt) = spiceypy.recgeo(spoint, re, f)

        #
        # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/subslr_c.html
        #
        #    Variable  I/O  Description
        #    --------  ---  --------------------------------------------------
        #    method     I   Computation method.
        #    target     I   Name of target body.
        #    et         I   Epoch in ephemeris seconds past J2000 TDB.
        #    fixref     I   Body-fixed, body-centered target body frame.
        #    abcorr     I   Aberration correction.
        #    obsrvr     I   Name of observing body.
        #    spoint     O   Sub-solar point on the target body.
        #    trgepc     O   Sub-solar point epoch.
        #    srfvec     O   Vector from observer to sub-solar point.
        #
        (spoint, trgepc, srfev) = \
            spiceypy.subslr('INTERCEPT/ELLIPSOID', target, et, frame, 'LT+S', observer)

        (sunlon, sunlat, sunalt) = spiceypy.recgeo(spoint, re, f)

        return tarlon, tarlat, sublon, sublat, sunlon, sunlat, tardis, tarang, ltime, phase, emissn, incdnc

    except:
        return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Exemple #30
0
 def _get_radii(self):
     _, radii = spice.bodvrd(self.target, "RADII", 3)
     return Radii(*radii)
def flatbending(xyzpoints, initialangle, MEX, TGO, referencedirection):
    class SpiceVariables:
        obs = '-74'  # NAIF code for MEX
        target = 'MARS ODYSSEY'  # NAIF code for TGO ['EARTH'/'SUN'/ a groundstation etc]
        obsfrm = 'IAU_MARS'
        abcorr = 'NONE'
        crdsys = 'LATITUDINAL'
        coord = 'LATITUDE'
        stepsz = 100.0  # Check every 300 seconds if there is an occultation
        MAXILV = 100000  #Max number of occultations that can be returned by gfoclt
        bshape = 'POINT'
        fshape = 'DSK/UNPRIORITIZED'
        front = 'MARS'
        fframe = 'IAU_MARS'
        TFMT = 'YYYY-MM-DD HR:MN:SC'  # Format that Cosmographia understands

    sv = SpiceVariables()

    #form a coordinate system where tgo is @ y=0 and x= (5000 +norm), Mar's Barrycenter being @ [5000,0]
    subgroupsize = 1
    #initialise non-global variables
    miniray = np.zeros(subgroupsize)
    raystep = np.zeros(
        (2,
         100000000))  # create a large array to populate and then shrink later

    barry2mex = np.linalg.norm(MEX)
    barry2tgo = np.linalg.norm(TGO)

    #find the martian geomoerty so you can reliably find the altitude of a point
    marsrad = spice.bodvrd(sv.front, 'RADII', 3)
    flatteningcoefficient = (marsrad[1][0] - marsrad[1][2]) / marsrad[1][0]
    equatorialradii = marsrad[1][0]

    TGO = TGO + 0
    MEX = MEX + 0  #force to be non-strided
    _, _, MEXalt = spice.recgeo(MEX, equatorialradii, flatteningcoefficient)
    _, _, TGOalt = spice.recgeo(TGO, equatorialradii, flatteningcoefficient)

    #the possition of MEX is found by assuming that it will be somewhere over the relative horizon from TGO
    # (meaning over θ = 90°), finding the angle between the MEX and TGO's negative vector, will give the coords of MEX
    MexRelativeElevation = spice.vsep(-TGO, MEX)  #radians
    mex_y = barry2mex * np.sin(MexRelativeElevation)
    mex_x = barry2mex * np.cos(MexRelativeElevation)

    mex = np.array([0 - mex_x, mex_y])
    tgo = np.array([0 + barry2tgo, 0])
    barry = np.array([0, 0])

    #to plot the non-refracted propogation, we must convert the 3d xyzpoints to 2d, we do this the same way we found the x&y for MEX
    # ,using the norm distance and sep from -TGO5
    length = np.size(xyzpoints, 1)
    UnrefractedDistance = np.linalg.norm(xyzpoints[:, 0] -
                                         xyzpoints[:, -1])  #in km
    UnrefractedRay = np.zeros([2, length])
    for i in range(length):  #conversion to 2D
        point = xyzpoints[:,
                          i] + 0  #need to put vector into temp variable as spice cant handle strided array inputs
        angle = spice.vsep(-TGO, point)
        norm = np.linalg.norm(point)
        point_x = norm * np.cos(angle)
        point_y = norm * np.sin(angle)
        UnrefractedRay[0, i] = 0 - point_x
        UnrefractedRay[1, i] = point_y

    #this will produce and angle that is likly not going to be exactly on
    #the original propagation path, you compare to this if there is a drifting error, as both this and the resultant refracted ray
    # have the same bias error. THIS ANGLE IS BENDING ANTICLOCKWISE IN THIS FRAME (BENDING UPWARDS)
    initialtheta = -(spice.vsep(MEX - TGO, MEX))
    nicetohave = np.degrees(initialtheta)

    #THIS NEEDS TO VARY IF THERE IS AN OVERSHOOT
    unit = 1  # in km
    unitoriginal = unit

    rotationvector = np.array(((np.cos(initialtheta), -np.sin(initialtheta)),
                               (np.sin(initialtheta), np.cos(initialtheta))))

    #get unit vecotr of -MEX (then add this vecotr to MEX for each alt calcultation)
    unitmex = -mex / barry2mex  #unit direction (2d)

    if referencedirection == [0, 0, 0]:
        initialdirection = unitmex.dot(
            rotationvector
        ) * unit  #make a 2d vector coming from MEX YOU DO NOT KNOW WHAT WAY THIS IS ROTATING
    else:  # if there is a value for the fed-back starting direction than use this as the first firection
        initialdirection = referencedirection

    iterationcount = 0
    #while iterationcount<100:
    #print( "Finding Bending Angle (", str(iterationcount) ,"% Complete)")
    errorstore = np.zeros((11, 100000))
    S = np.zeros(20000)

    #IF REFERCEDRECTION==0 DO NORMAL, IF /= INCLUDE THIS AS THE FIRST DIRECTION.

    while iterationcount < 10:
        tic = timer.perf_counter()

        if iterationcount == 0:
            direction = initialdirection

        else:

            missangle = missangle / 1

            missrotationvector = np.array(
                ((np.cos(missangle), -np.sin(missangle)), (np.sin(missangle),
                                                           np.cos(missangle))))

            direction = initialdirection.dot(missrotationvector)
            #check the differecne between the initial and the direction, see if the same for both
            CHECK_ME = direction - initialdirection
            initialdirection = direction

        turningcounter = 0
        stage = 0
        t = 0
        unit = unitoriginal
        #with tqdm(total = mex[1], desc = "Progress", leave=False) as pbar:
        while stage < 2:  #==0first unit, so move two units. ==1 propergate step by step. ==2 exit and analyse entire path
            #take the alt at 10 possitions across this unit

            #lets get a quick calculated for the magnitude of the direction
            MAAAAG = np.linalg.norm(direction)  # this should = unit

            if stage == 0:
                for k in range(subgroupsize):  #this starts with 0
                    point = mex + ((k + 1) * (direction / subgroupsize))
                    #_,_,miniray[k] = spice.recgeo(point, equatorialradii,flatteningcoefficient)
                    miniray[k] = np.linalg.norm(
                        point) - 3389  #average radii of mars

                N0 = findrefractivity(miniray, subgroupsize)
                raystep[:, t] = point  #save the last location
                t = t + 1
                stage = stage + 1

            if stage == 1:
                for k in range(subgroupsize):
                    point = raystep[:, t - 1] + (
                        (k + 1) * (direction / subgroupsize)
                    )  #am i double counting the end of the last and the start of the next?
                    #_,_,miniray[k] = spice.recgeo(point, equatorialradii,flatteningcoefficient)  #THIS ONLY WORKS IN 3D
                    # IMPLEMENTING MARS AS A SIMPLE CIRCLE OF AVERAGE 3389 KM RADIUS, !THIS WILL BE UPDATED TO ELLIPSE!
                    miniray[k] = np.linalg.norm(point) - 3389

                raystep[:,
                        t] = point  #9 is the end of the unit, and refraction always happens relative to the center of refractivity, so rotate off this vector
                N1 = findrefractivity(miniray, subgroupsize)

                #IF THE Y VALUE DROPS BELOW 0, LOOP BACK WITH A STEP SIZE OF 1/10TH#################################################################<- HERE
                if point[1] < 0:  #if the position drops below the x axis
                    direction = direction / 10  #drop the step size
                    unit = unit / 10
                    #stage = stage+1

                    #MAYBE SHRINK THE DIRECTION INSTEAD OF THE UNIT, IT DOESNT GET REINITIALED IN THIS WHILE LOOP
                    # t is not incrememented so that it can loop back to the last position
                    # , t-1 is the position just before crossing over into negative space
                    if abs(point[1]) < 0.00001:  # is it smaller than cm
                        stage = stage + 1  #increase the stage value so the while loop is exited
                    continue

                    # #this section allows for better timing of the function, increment the progresbar by 1 if the current
                    # #position goes one y-value lower
                    # currenty = raystep[1,t]
                    # progress[1] = mex[1] - currenty #this value will be increasing from 0 -> Mex height
                    # increment = np.floor(progress[1])- np.floor(progress[0]) #only when
                    # if increment ==1 :
                    #     pbar.update(1)
                    # progress[0] = progress[1]

                if abs(N1) < 1e-20:  #catch for precision errors
                    S[t] = unit - (
                        N1 * unit
                    )  # whilst neglegible N, Electric distance is can simply be inversly proportional to N
                    t = t + 1
                    N0 = N1
                    continue

                #THE SECTION BELOW IS ONLY ACCESSED WHEN THE REFRACTIVTY IS ABOVE E-20
                #print('Current Y possition is', currenty) #this is alt, so is ~3389 km smaller than the vector
                r = N0 / N1  #NEED MORE PRECISION
                numorator = N0 + 1
                denominator = N1 + 1
                rbending = numorator / denominator  #average would just add 1 to total N
                # if t==5000: #only bend when there is a refractive gradient between consecutive air volumes[NO CHANGE IN DIRECTION]
                #     t=t+1
                #     N0=N1
                #     continue

                #this section is only reached if a turning is going to happen
                # ,testing to see if there is 10 X less turing if the units are 10X smaller -TRUE
                TEST = float(rbending)
                if TEST != 1:
                    turningcounter = turningcounter + 1

                # !! NOW WITH PRECISION !!

                #find the angle between the unit (air volume) boarder and the current direction
                unitrotationaxis = raystep[:, t] / (
                    (np.linalg.norm(raystep[:, t])) * unit)
                #unitdirection = direction #MAYBE ALTERING UNITS WILL EFFECT THIS * OR / BY UNIT, CANT FIGURE OUT NOW, OK WHEN UNIT =1
                DotProduct = np.dot((unitrotationaxis), direction)
                AngleofIncidence = (math.pi / 2) - np.arccos(
                    DotProduct)  #angle it enters the next air volume
                #simple snell law to find the bending angle (should be tiny angle)
                AngleofRefraction = np.arcsin(rbending *
                                              np.sin(AngleofIncidence))
                # THIS IS NOT EXACTLY WHAT THE TURN IN DIRECTION IS, NEED TO THINK ABOUT
                rotateby = ((AngleofIncidence - AngleofRefraction)
                            )  #+ve =clockwise, -ve=anticlockwise

                INCIDENCEDEGREES = np.degrees(AngleofIncidence)
                REFRACTIONDEGREES = np.degrees(AngleofRefraction)
                ROTATIONDEGREES = np.degrees(rotateby)

                #an if statement is required, if this
                if ROTATIONDEGREES > 1 or ROTATIONDEGREES < -1:
                    print('stophere, u r bending to much')
                rotationvector = np.array(
                    ((np.cos(rotateby), -np.sin(rotateby)),
                     (np.sin(rotateby), np.cos(rotateby))))
                direction = direction.dot(rotationvector)

                N0 = N1

                #store N1 to calc electric distance
                S[t] = unit - (N1 * unit)
                t = t + 1

            #pbar.refresh()

        unit_initial = initialdirection / np.linalg.norm(initialdirection)
        dot_product = np.dot(unit_initial, unitmex)
        FinalBendingAngle = np.arccos(dot_product)

        error = np.zeros(t)
        #print("Number of turns:", turningcounter)
        #error = swiftmain.finderror(raystep, UnrefractedRay) # also find the y-overshoot here

        miss = error  # 1D along the abscissa

        #update for the miss angle, going to include miss in the ordinate
        #START EDITING HERE
        miss = point[0] - UnrefractedRay[0, -1]  # X domain

        deltaX = UnrefractedRay[0, -1]  #TGO X value
        deltaY = UnrefractedRay[
            1,
            0]  # this is the height of the whole 2d scene (both refract and unrefacted have the same height)
        unrefractedangle = np.arctan(deltaX / deltaY)
        refractedangle = np.arctan((deltaX + miss) / deltaY)
        missangle = refractedangle - unrefractedangle  # if positive, then rotate clockwise
        #missangle = (np.arcsin(miss/UnrefractedDistance)) # this shouldnt work
        toc = timer.perf_counter()
        passingtime = toc - tic
        #print('miss =', format(miss*1000, '.5f') ,'m || Angle =', np.degrees(missangle) ,
        #                    '° || Speed =',passingtime,' Sec \n', sep = " ", end= " ", flush =True)

        if abs(miss) < 1e-4:  #is the miss smaller than 10 cm?
            #ploterrortraces(errorstore,t)
            break

        iterationcount = iterationcount + 1

    #find the total bending angle at MEX for this final configuration

    #from the refractive profile from MEX ->TGO, calc the intergral of the change in wavelength to aquire doppler
    S = S[S != 0]
    ElectricDistance = (
        np.sum(S)
    )  #N * wavelengths in a km (UNITS MUST BE KEPT THE SAME AS STEP-SIZE)[+ OVERSHOT BECAUSE IT IS A NEGATIVE VARIABLE ]

    return FinalBendingAngle, ElectricDistance, initialdirection  #feedback the starting vector for speed
def producegeometrymeter(MEX, TGO):
    #maybe completly thin this out, you know this is moslty pointless, what does it actually make

    class SpiceVariables:
        obs = '-41'  # NAIF code for MEX '-74'
        target = '-143'  # NAIF code for TGO ['EARTH'/'SUN'/ a groundstation etc] 'MARS ODYSSEY'
        obsfrm = 'IAU_MARS'
        abcorr = 'NONE'
        crdsys = 'LATITUDINAL'
        coord = 'LATITUDE'
        stepsz = 1.0  # Check every [300] seconds if there is an occultation
        MAXILV = 100000  #Max number of occultations that can be returned by gfoclt
        bshape = 'POINT'
        fshape = 'DSK/UNPRIORITIZED'
        front = 'MARS'
        fframe = 'IAU_MARS'
        TFMT = 'YYYY-MM-DD HR:MN:SC'  # Format that Cosmographia understands

    sv = SpiceVariables()

    #THIS COULD BE REMOVED
    # [TGO, _] = spice.spkpos(sv.front, et-when, sv.fframe, 'NONE', sv.target)
    # [MEX, _] = spice.spkpos(sv.front, et-when, sv.fframe, 'NONE', sv.obs)
    TGO = TGO + 0
    MEX = MEX + 0  #force to be non-strided
    dist = math.floor(spice.vdist(TGO, MEX))

    angleseparation = (spice.vsep(MEX, TGO))  # angle taken a mars center
    initialangle = (spice.vsep(-MEX, (TGO - MEX))) * (
        180 / math.pi
    )  # angle taken at mars-MEX-tgo, that points to tgo. needed for the bending functions original starting angle
    #script needs to work via periods of ray and not meters. [totalperiods is the main iterable, not meters]

    sc2sc = TGO - MEX
    norm = np.linalg.norm(sc2sc)
    unitsc2sc = sc2sc / norm  #this needs to shrink if the repeatable expands
    points = np.empty([3, dist])
    sza = np.empty([1, dist])
    angleprogression = np.empty([1, dist])

    xyzpoints = np.zeros([3, dist])
    marsrad = spice.bodvrd(sv.front, 'RADII', 3)
    flatteningcoefficient = (marsrad[1][0] - marsrad[1][2]) / marsrad[1][0]
    equatorialradii = marsrad[1][0]
    # find direction of sun, it will not change much during the occultation. so only calc it once
    #[SUN, _] = spice.spkpos(sv.front, et, sv.fframe, 'NONE', 'SUN')
    for i in range(dist):
        xyzpoint = MEX + (
            i * unitsc2sc
        )  #move along ray, 1000 wavelength distance at a time (685 m). but unitsc2sc is in km...
        xyzpoints[:, i] = xyzpoint
        #sza[0,i] = spice.vsep(SUN,xyzpoint)
        angleprogression[0, i] = (spice.vsep(xyzpoint, MEX)) * (180 / math.pi)
        points[:, i] = spice.recgeo(xyzpoint, equatorialradii,
                                    flatteningcoefficient)
        points[0, i] = (points[0, i] * (-180 / math.pi))
        points[1, i] = (points[1, i] * (-180 / math.pi))

    # ray = np.concatenate((points,sza), axis=0) # important for when sza is included

    #plt.plot(angleprogression[0,:], ray[2,:])
    #plt.show()

    # ray is in lat/lon/alt + sza and xyzpoints is cartesian, both describe the same thing
    return initialangle, xyzpoints
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)
#==============================================================================
# Find location of ring points
#==============================================================================

a_ring_outer_km = 129300
a_ring_inner_km = 122000

x_ring1, y_ring1 = hbt.get_pos_ring(et, name_body='Jupiter', radius=a_ring_inner_km, units='pixels', abcorr='LT', wcs=w)
x_ring2, y_ring2 = hbt.get_pos_ring(et, name_body='Jupiter', radius=a_ring_outer_km, units='pixels', abcorr='LT', wcs=w)

#==============================================================================
# Do the ring extraction
#==============================================================================

(numrad, rj_array) = sp.bodvrd('JUPITER', 'RADII', 3) # 71492 km
rj = rj_array[0]
r_ring_inner = 126000 # * rj   # Follow same limits as in Throop 2004 J-ring paper fig. 7
r_ring_outer = 132000 # rj

num_bins_azimuth = 300    # 500 is OK. 1000 is too many -- we get bins ~0 pixels
num_bins_radius  = 300

limits_radius = (r_ring_inner, r_ring_outer) # Distances in km

# Define an offset in X and Y for the ring. This is the residual navigation error, which we want to apply here.

dx = 0
dy = 0

# Do the unwrapping. If there are no ring points, then we catch the error.