Example #1
0
def test_SpicePlane():
    norm = [0.0, 0.0, 1.0]
    orig = [0.0, 0.0, 0.0]
    plane = spice.nvp2pl(norm, orig)
    npt.assert_array_almost_equal(plane.normal, norm)
    assert plane.constant == 0.0
    assert str(plane).startswith("<SpicePlane")
def test_SpicePlane():
    norm = [0.0, 0.0, 1.0]
    orig = [0.0, 0.0, 0.0]
    plane = spice.nvp2pl(norm, orig)
    npt.assert_array_almost_equal(plane.normal, norm)
    assert plane.constant == 0.0
    assert str(plane).startswith("<SpicePlane")
def nh_ort_find_ring_pole():
    
    file_superstack = '/Users/throop/Data/ORT4/superstack_ORT4_z4_mean_wcs_sm_hbt.fits'
    
    file_tm = 'kernels_kem_prime.tm'
    sp.unload(file_tm)
    sp.furnsh(file_tm)
    
    f = fits.open(file_superstack)
    
    img = f[0].data
    
#    plt.imshow(stretch(img))
#    plt.show()
    
    wcs = WCS(file) 
    
    num_pts = 200
    
    ra_pole   = 275 * hbt.d2r
#    dec_pole  = -56 * hbt.d2r
    dec_pole  = 13 * hbt.d2r

    radius_ring = 9000  # Radius in km

    vec_pole_j2k = sp.radrec(1, ra_pole, dec_pole)
    
    et = float(f[0].header['SPCSCET'])
    utc = sp.et2utc(et, 'C', 0)
    
    # Get position from NH to UT
    
    (st, lt) = sp.spkezr('MU69', et, 'J2000', 'LT', 'New Horizons')

    vec_nh_ut = st[0:3]
 
     # Get position from Sun, to NH
    
    (st, lt) = sp.spkezr('New Horizons', et, 'J2000', 'LT', 'Sun')
    
    vec_sun_nh = st[0:3]

    vec_sun_ut = vec_sun_nh + vec_nh_ut
    
    # Define a 'ring plane', based on a pole vector, and a point
    # This ring plane should be in J2K space -- that is, centered on Sun.
    
    plane_ring = sp.nvp2pl(vec_pole_j2k, vec_sun_ut)  # Pole position is variable. Point is UT in J2K.
 
    # Get the point and spanning vectors that define this plane
    
    # XXX for some reason, these values from pl2psv do not depend on value of vec_pol_j2k
    
    (pt_pl, vec1_pl, vec2_pl) = sp.pl2psv(plane_ring)

    # Now take a bunch of linear combinations of these spanning vectors
    
    # Plot UT's position on the plot
    
    (st, lt) = sp.spkezr('MU69', et, 'J2000', 'LT', 'New Horizons')

    (_, ra, dec) = sp.recrad(vec_nh_ut)
        
    (x, y) = wcs.wcs_world2pix(ra*hbt.r2d, dec*hbt.r2d, 0)
       
#    plt.plot(x, y, marker = 'o', ms = 10, alpha=0.3, color='purple')
    
    # Set an offset to the WCS values, in case UT is not in the right position (ie, not centered properly)
    
    dy = 0 # Large value moves up
    dx = 0

    # Draw the ring image
    
    plt.imshow(stretch(img), origin='lower')
    
    # Calculate and draw all of the ring points
        
    for i in range(num_pts):
        
        angle_azimuth = 2*math.pi * (i / num_pts)   # Put in range 0 .. 2 pi
        vec_i = vec1_pl * math.sin(angle_azimuth) + vec2_pl * math.cos(angle_azimuth)
        vec_i = vec_i * radius_ring
        
        # Now get the point in space, J2K
        
        pt_ring_i_j2k = vec_i + vec_sun_ut
        
        vec_sun_ring_i = pt_ring_i_j2k
        
        vec_nh_ring_i = vec_sun_ring_i- vec_sun_nh
        
        # 
        (_, ra_i, dec_i) = sp.recrad(vec_nh_ring_i)
        
        (x, y) = wcs.wcs_world2pix(ra_i*hbt.r2d, dec_i*hbt.r2d, 0)
        
        plt.plot(x+dx, y+dy, marker = 'o', ms = 1, color='red', alpha = 0.15)
#        print(f'{i}, {ra_i*hbt.r2d}, {dec_i*hbt.r2d}, {x}, {y}')
    
    plt.title(f'ORT4 Superstack, Ring Pole = ({ra_pole*hbt.r2d},{dec_pole*hbt.r2d}) deg')    
    plt.show()
    
    return
def compute_backplanes(file,
                       name_target,
                       frame,
                       name_observer,
                       angle1=0,
                       angle2=0,
                       angle3=0):
    """
    Returns a set of backplanes for a single specified image. The image must have WCS coords available in its header.
    Backplanes include navigation info for every pixel, including RA, Dec, Eq Lon, Phase, etc.
    
    The results are returned to memory, and not written to a file.
    
    SPICE kernels must be alreaded loaded, and spiceypy running.
    
    Parameters
    ----
    
    file:
        String. Input filename, for FITS file.
    frame:
        String. Reference frame of the target body. 'IAU_JUPITER', 'IAU_MU69', '2014_MU69_SUNFLOWER_ROT', etc.
        This is the frame that the Radius_eq and Longitude_eq are computed in.
    name_target:
        String. Name of the central body. All geometry is referenced relative to this (e.g., radius, azimuth, etc)
    name_observer:
        String. Name of the observer. Must be a SPICE body name (e.g., 'New Horizons')
    
    Optional Parameters
    ----
    
    angle{1,2,3}:
        **NOT REALLY TESTED. THE BETTER WAY TO CHANGE THE ROTATION IS TO USE A DIFFERENT FRAME.**

        Rotation angles which are applied when defining the plane in space that the backplane will be generated for.
        These are applied in the order 1, 2, 3. Angles are in radians. Nominal values are 0.
       
        This allows the simulation of (e.g.) a ring system inclined relative to the nominal body equatorial plane.
        
        For MU69 sunflower rings, the following descriptions are roughly accurate, becuase the +Y axis points
        sunward, which is *almost* toward the observer. But it is better to experiment and find the 
        appropriate angle that way, than rely on this ad hoc description. These are close for starting with.
 
                      - `angle1` = Tilt front-back, from face-on. Or rotation angle, if tilted right-left.
                      - `angle2` = Rotation angle, if tilted front-back. 
                      - `angle3` = Tilt right-left, from face-on.
    
    do_fast:
        Boolean. If set, generate only an abbreviated set of backplanes. **NOT CURRENTLY IMPLEMENTED.**
        
    Output
    ----

    Output is a tuple, consisting of each of the backplanes, and a text description for each one. 
    The size of each of these arrays is the same as the input image.
    
    The position of each of these is the plane defined by the target body, and the normal vector to the observer.
    
    output = (backplanes, descs)
    
        backplanes = (ra,      dec,      radius_eq,      longitude_eq,      phase)
        descs      = (desc_ra, desc_dec, desc_radius_eq, desc_longitude_eq, desc_phase)
    
    Radius_eq:
        Radius, in the ring's equatorial plane, in km
    Longitude_eq:
        Longitude, in the equatorial plane, in radians (0 .. 2pi)
    RA:
        RA of pixel, in radians
    Dec:
        Dec of pixel, in
    dRA:
        Projected offset in RA  direction between center of body (or barycenter) and pixel, in km.
    dDec:
        Projected offset in Dec direction between center of body (or barycenter) and pixel, in km.
        
    Z:  
        Vertical value of the ring system.
        
    With special options selected (TBD), then additional backplanes will be generated -- e.g., a set of planes
    for each of the Jovian satellites in the image, or sunflower orbit, etc.
    
    No new FITS file is written. The only output is the returned tuple.
    
    """

    if not (frame):
        raise ValueError('frame undefined')

    if not (name_target):
        raise ValueError('name_target undefined')

    if not (name_observer):
        raise ValueError('name_observer undefined')

    name_body = name_target  # Sometimes we use one, sometimes the other. Both are identical

    fov_lorri = 0.3 * hbt.d2r

    abcorr = 'LT'

    do_satellites = False  # Flag: Do we create an additional backplane for each of Jupiter's small sats?

    # Open the FITS file

    w = WCS(
        file
    )  # Warning: I have gotten a segfault here before if passing a FITS file with no WCS info.
    hdulist = fits.open(file)

    et = float(hdulist[0].header['SPCSCET'])  # ET of mid-exposure, on s/c
    n_dx = int(hdulist[0].header['NAXIS1']
               )  # Pixel dimensions of image. Both LORRI and MVIC have this.
    n_dy = int(hdulist[0].header['NAXIS2'])

    hdulist.close()

    # Setup the output arrays

    lon_arr = np.zeros(
        (n_dy, n_dx))  # Longitude of pixel (defined with recpgr)
    lat_arr = np.zeros(
        (n_dy, n_dx))  # Latitude of pixel (which is zero, so meaningless)
    radius_arr = np.zeros((n_dy, n_dx))  # Radius, in km
    altitude_arr = np.zeros((n_dy, n_dx))  # Altitude above midplane, in km
    ra_arr = np.zeros((n_dy, n_dx))  # RA of pixel
    dec_arr = np.zeros((n_dy, n_dx))  # Dec of pixel
    dra_arr = np.zeros(
        (n_dy, n_dx)
    )  # dRA  of pixel: Distance in sky plane between pixel and body, in km.
    ddec_arr = np.zeros(
        (n_dy, n_dx)
    )  # dDec of pixel: Distance in sky plane between pixel and body, in km.
    phase_arr = np.zeros((n_dy, n_dx))  # Phase angle
    x_arr = np.zeros(
        (n_dy, n_dx))  # Intersection of sky plane: X pos in bdoy coords
    y_arr = np.zeros(
        (n_dy, n_dx))  # Intersection of sky plane: X pos in bdoy coords
    z_arr = np.zeros(
        (n_dy, n_dx))  # Intersection of sky plane: X pos in bdoy coords

    # =============================================================================
    #  Do the backplane, in the general case.
    #  This is a long routine, because we deal with all the satellites, the J-ring, etc.
    # =============================================================================

    if (True):

        # Look up body parameters, used for PGRREC().

        (num, radii) = sp.bodvrd(name_target, 'RADII', 3)

        r_e = radii[0]
        r_p = radii[2]
        flat = (r_e - r_p) / r_e

        # Define a SPICE 'plane' along the plane of the ring.
        # Do this in coordinate frame of the body (IAU_JUPITER, 2014_MU69_SUNFLOWER_ROT, etc).

        # =============================================================================
        # Set up the Jupiter system specifics
        # =============================================================================

        if (name_target.upper() == 'JUPITER'):
            plane_target_eq = sp.nvp2pl(
                [0, 0, 1],
                [0, 0, 0
                 ])  # nvp2pl: Normal Vec + Point to Plane. Jupiter north pole?

            # For Jupiter only, define a few more output arrays for the final backplane set

            ang_metis_arr = np.zeros(
                (n_dy, n_dx))  # Angle from pixel to body, in radians
            ang_adrastea_arr = ang_metis_arr.copy()
            ang_thebe_arr = ang_metis_arr.copy()
            ang_amalthea_arr = ang_metis_arr.copy()

# =============================================================================
# Set up the MU69 specifics
# =============================================================================

        if ('MU69' in name_target.upper()):

            # Define a plane, which is the plane of sunflower rings (ie, X-Z plane in Sunflower frame)
            # If additional angles are passed, then create an Euler matrix which will do additional angles of rotation.
            # This is defined in the 'MU69_SUNFLOWER' frame

            vec_plane = [0, 1,
                         0]  # Use +Y (anti-sun dir), which is normal to XZ.
            #            vec_plane = [0, 0, 1]                                 # Use +Y (anti-sun dir), which is normal to XZ.
            plane_target_eq = sp.nvp2pl(
                vec_plane,
                [0, 0, 0])  # "Normal Vec + Point to Plane". 0,0,0 = origin.

        # XXX NB: This plane in in body coords, not J2K coords. This is what we want, because the
        # target intercept calculation is also done in body coords.

# =============================================================================
# Set up the various output planes and arrays necessary for computation
# =============================================================================

# Get xformation matrix from J2K to target system coords. I can use this for points *or* vectors.

        mx_j2k_frame = sp.pxform('J2000', frame, et)  # from, to, et

        # Get vec from body to s/c, in both body frame, and J2K.
        # NB: The suffix _j2k indicates j2K frame. _frame indicates the frame of target (IAU_JUP, MU69_SUNFLOWER, etc)

        (st_target_sc_frame, lt) = sp.spkezr(name_observer, et, frame, abcorr,
                                             name_target)
        (st_sc_target_frame, lt) = sp.spkezr(name_target, et, frame, abcorr,
                                             name_observer)
        (st_target_sc_j2k, lt) = sp.spkezr(name_observer, et, 'J2000', abcorr,
                                           name_target)
        (st_sc_target_j2k, lt) = sp.spkezr(name_target, et, 'J2000', abcorr,
                                           name_observer)

        vec_target_sc_frame = st_target_sc_frame[0:3]
        vec_sc_target_frame = st_sc_target_frame[0:3]
        vec_target_sc_j2k = st_target_sc_j2k[0:3]
        vec_sc_target_j2k = st_sc_target_j2k[0:3]

        dist_target_sc = sp.vnorm(
            vec_target_sc_j2k)  # Get target distance, in km

        # vec_sc_target_frame = -vec_target_sc_frame # ACTUALLY THIS IS NOT TRUE!! ONLY TRUE IF ABCORR=NONE.

        # Name this vector a 'point'. INRYPL requires a point argument.

        pt_target_sc_frame = vec_target_sc_frame

        # Look up RA and Dec of target (from sc), in J2K

        (_, ra_sc_target, dec_sc_target) = sp.recrad(vec_sc_target_j2k)

        # Get vector from target to sun. We use this later for phase angle.

        (st_target_sun_frame,
         lt) = sp.spkezr('Sun', et, frame, abcorr,
                         name_target)  # From body to Sun, in body frame
        vec_target_sun_frame = st_target_sun_frame[0:3]

        # Create a 2D array of RA and Dec points
        # These are made by WCS, so they are guaranteed to be right.

        xs = range(n_dx)
        ys = range(n_dy)
        (i_x_2d, i_y_2d) = np.meshgrid(xs, ys)
        (ra_arr, dec_arr) = w.wcs_pix2world(i_x_2d, i_y_2d,
                                            False)  # Returns in degrees
        ra_arr *= hbt.d2r  # Convert to radians
        dec_arr *= hbt.d2r

        # Compute the projected distance from MU69, in the sky plane, in km, for each pixel.
        # dist_target_sc is the distance to MU69, and we use this to convert from radians, to km.
        # 16-Oct-2018. I had been computing this erroneously. It should be *cosdec, not /cosdec.

        dra_arr = (ra_arr - ra_sc_target) * dist_target_sc * np.cos(dec_arr)
        ddec_arr = (dec_arr - dec_sc_target) * dist_target_sc  # Convert to km

        # =============================================================================
        #  Compute position for additional Jupiter bodies, as needed
        # =============================================================================

        if (name_target.upper() == 'JUPITER'):
            vec_metis_j2k, lt = sp.spkezr('Metis', et, 'J2000', abcorr,
                                          'New Horizons')
            vec_adrastea_j2k, lt = sp.spkezr('Adrastea', et, 'J2000', abcorr,
                                             'New Horizons')
            vec_thebe_j2k, lt = sp.spkezr('Thebe', et, 'J2000', abcorr,
                                          'New Horizons')
            vec_amalthea_j2k, lt = sp.spkezr('Amalthea', et, 'J2000', abcorr,
                                             'New Horizons')

            vec_metis_j2k = np.array(vec_metis_j2k[0:3])
            vec_thebe_j2k = np.array(vec_thebe_j2k[0:3])
            vec_adrastea_j2k = np.array(vec_adrastea_j2k[0:3])
            vec_amalthea_j2k = np.array(vec_amalthea_j2k[0:3])

# =============================================================================
# Loop over pixels in the output image
# =============================================================================

        for i_x in xs:
            for i_y in ys:

                # Look up the vector direction of this single pixel, which is defined by an RA and Dec
                # Vector is thru pixel to ring, in J2K.
                # RA and Dec grids are made by WCS, so they are guaranteed to be right.

                vec_pix_j2k = sp.radrec(1., ra_arr[i_y, i_x], dec_arr[i_y,
                                                                      i_x])

                # Convert vector along the pixel direction, from J2K into the target body frame

                vec_pix_frame = sp.mxv(mx_j2k_frame, vec_pix_j2k)

                # And calculate the intercept point between this vector, and the ring plane.
                # All these are in body coordinates.
                # plane_target_eq is defined as the body's equatorial plane (its XZ for MU69).

                # ** Some question as to whether we should shoot this vector at the ring plane, or the sky plane.
                # Ring plane is normally the one we want. But, for the case of edge-on rings, the eq's break down.
                # So, we should use the sky plane instead. Testing shows that for the case of MU69 Eq's break down
                # for edge-on rings... there is always an ambiguity.

                # ** For testing, try intersecting the sky plane instead of the ring plane.
                # ** Confirmed: Using sky plane gives identical results in case of face-on rings.
                #    And it gives meaningful results in case of edge-on rings, where ring plane did not.
                #    However, for normal rings (e.g., Jupiter), we should continue using the ring plane, not sky plane.

                do_sky_plane = True  # For ORT4, where we want to use euler angles, need to set this to False

                if do_sky_plane and ('MU69' in name_target):
                    plane_sky_frame = sp.nvp2pl(
                        vec_sc_target_frame,
                        [0, 0, 0])  # Frame normal to s/c vec, cntrd on MU69
                    (npts,
                     pt_intersect_frame) = sp.inrypl(pt_target_sc_frame,
                                                     vec_pix_frame,
                                                     plane_sky_frame)

                    # pt_intersect_frame is the point where the ray hits the skyplane, in the coordinate frame
                    # of the target body.

                else:  # Calc intersect into equator of target plane (ie, ring plane)
                    (npts,
                     pt_intersect_frame) = sp.inrypl(pt_target_sc_frame,
                                                     vec_pix_frame,
                                                     plane_target_eq)
                    # pt, vec, plane

                # Swap axes in target frame if needed.
                # In the case of MU69 (both sunflower and tunacan), the frame is defined s.t. the ring
                # is in the XZ plane, not XY. This is strange (but correct).
                # I bet MU69 is the only ring like this. Swap it so that Z means 'vertical, out of plane' --
                # that is, put it into normal XYZ rectangular coords, so we can use RECLAT etc on it.

                if (
                        'MU69' in name_target
                ):  # Was 0 2 1. But this makes tunacan radius look in wrong dir.
                    # 201 looks same
                    # 210 similar
                    # 201 similar
                    # 102, 120 similar.
                    # ** None of these change orientation of 'radius' backplane. OK.

                    pt_intersect_frame = np.array([
                        pt_intersect_frame[0], pt_intersect_frame[2],
                        pt_intersect_frame[1]
                    ])

                # Get the radius and azimuth of the intersect, in the ring plane
                # Q: Why for the TUNACAN is the radius zero here along horizontal (see plot)?
                # A: Ahh, it is not zero. It is just that the 'projected radius' of a ring that is nearly edge-on
                # can be huge! Basically, if we try to calc the intersection with that plane, it will give screwy
                # answers, because the plane is so close to edge-on that intersection could be a long way
                # from body itself.

                # Instead, I really want to take the tangent sky plane, intersect that, and then calc the
                # position of that (in xyz, radius, longitude, etc).
                # Since that plane is fixed, I don't see a disadvantage to doing that.

                # We want the 'radius' to be the radius in the equatorial plane -- that is, sqrt(x^2 + y^2).
                # We don't want it to be the 'SPICE radius', which is the distance.
                # (For MU69 equatorial plane is nominally XZ, but we have already changed that above to XY.)

                _radius_3d, lon, lat = sp.reclat(pt_intersect_frame)

                radius_eq = sp.vnorm(
                    [pt_intersect_frame[0], pt_intersect_frame[1], 0])
                #                radius_eq = sp.vnorm([pt_intersect_frame[0], pt_intersect_frame[1], pt_intersect_frame[2]])

                # Get the vertical position (altitude)

                altitude = pt_intersect_frame[2]

                # Calculate the phase angle: angle between s/c-to-ring, and ring-to-sun

                vec_ring_sun_frame = -pt_intersect_frame + vec_target_sun_frame

                angle_phase = sp.vsep(-vec_pix_frame, vec_ring_sun_frame)

                # Save various derived quantities

                radius_arr[i_y, i_x] = radius_eq
                lon_arr[i_y, i_x] = lon
                phase_arr[i_y, i_x] = angle_phase
                altitude_arr[i_y, i_x] = altitude

                # Save these just for debugging

                x_arr[i_y, i_x] = pt_intersect_frame[0]
                y_arr[i_y, i_x] = pt_intersect_frame[1]
                z_arr[i_y, i_x] = pt_intersect_frame[2]

                # Now calc angular separation between this pixel, and the satellites in our list
                # Since these are huge arrays, cast into floats to make sure they are not doubles.

                if (name_body.upper() == 'JUPITER'):
                    ang_thebe_arr[i_y, i_x] = sp.vsep(vec_pix_j2k,
                                                      vec_thebe_j2k)
                    ang_adrastea_arr[i_y,
                                     i_x] = sp.vsep(vec_pix_j2k,
                                                    vec_adrastea_j2k)
                    ang_metis_arr[i_y, i_x] = sp.vsep(vec_pix_j2k,
                                                      vec_metis_j2k)
                    ang_amalthea_arr[i_y,
                                     i_x] = sp.vsep(vec_pix_j2k,
                                                    vec_amalthea_j2k)

        # Now, fix a bug. The issue is that SP.INRYPL uses the actual location of the bodies (no aberration),
        # while their position is calculated (as it should be) with abcorr=LT. This causes a small error in the
        # positions based on the INRYPL calculation. This should probably be fixed above, but it was not
        # obvious how. So, instead, I am fixing it here, by doing a small manual offset.

        # Calculate the shift required, by measuring the position of MU69 with abcorr=NONE, and comparing it to
        # the existing calculation, that uses abcorr=LT. This is brute force, but it works. For MU69 approach,
        # it is 0.75 LORRI 4x4 pixels (ie, 3 1X1 pixels). This is bafflingly huge (I mean, we are headed
        # straight toward MU69, and it takes a month to move a pixel, and RTLT is only a few minutes). But I have
        # confirmed the math and the magnitude, and it works.

        (st_sc_target_j2k_nolt, _) = sp.spkezr(name_target, et, 'J2000',
                                               'NONE', name_observer)
        vec_sc_target_j2k_nolt = st_sc_target_j2k_nolt[0:3]
        (_, ra_sc_target_nolt,
         dec_sc_target_nolt) = sp.recrad(vec_sc_target_j2k_nolt)

        (x0, y0) = w.wcs_world2pix(ra_sc_target_nolt * hbt.r2d,
                                   dec_sc_target_nolt * hbt.r2d, 1)
        (x1, y1) = w.wcs_world2pix(ra_sc_target * hbt.r2d,
                                   dec_sc_target * hbt.r2d, 1)
        dx = x1 - x0
        dy = y1 - y0

        print(f'Compute backplanes: INRYPL pixel shift = {dx}, {dy}')

        dx_int = int(round(dx))
        dy_int = int(round(dy))

        do_roll = True

        if do_roll:
            print(
                f'compute_backplanes: Rolling by {dx_int}, {dy_int} due to INRYPL'
            )

            # Now shift all of the planes that need fixing. The dRA_km and dDec_km are calculated before INRYPL()
            # is applied, so they do not need to be shifted. I have validated that by plotting them.
            #
            # XXX NP.ROLL() is really not ideal. I should use a function that introduces NaN at the edge, not roll it.

            radius_arr = np.roll(np.roll(radius_arr, dy_int, axis=0),
                                 dx_int,
                                 axis=1)
            lon_arr = np.roll(np.roll(lon_arr, dy_int, axis=0), dx_int, axis=1)
            phase_arr = np.roll(np.roll(phase_arr, dy_int, axis=0),
                                dx_int,
                                axis=1)
            altitude_arr = np.roll(np.roll(altitude_arr, dy_int, axis=0),
                                   dx_int,
                                   axis=1)
        else:
            print(
                f'compute_backplanes: Skipping roll due to INRYPL, based on do_roll={do_roll}'
            )

        # Assemble the results into a backplane

        backplane = {
            'RA': ra_arr.astype(float),  # return radians
            'Dec': dec_arr.astype(float),  # return radians 
            'dRA_km': dra_arr.astype(float),
            'dDec_km': ddec_arr.astype(float),
            'Radius_eq': radius_arr.astype(float),
            'Longitude_eq': lon_arr.astype(float),
            'Phase': phase_arr.astype(float),
            'Altitude_eq': altitude_arr.astype(float),
            #             'x'            : x_arr.astype(float),
            #             'y'            : y_arr.astype(float),
            #             'z'            : z_arr.astype(float),
            #
        }

        # Assemble a bunch of descriptors, to be put into the FITS headers

        desc = {
            'RA of pixel, radians',
            'Dec of pixel, radians',
            'Offset from target in target plane, RA direction, km',
            'Offset from target in target plane, Dec direction, km',
            'Projected equatorial radius, km',
            'Projected equatorial longitude, km',
            'Sun-target-observer phase angle, radians',
            'Altitude above midplane, km',
            #                'X position of sky plane intercept',
            #                'Y position of sky plane intercept',
            #                'Z position of sky plane intercept'
        }

        # In the case of Jupiter, add a few extra fields

        if (name_body.upper() == 'JUPITER'):
            backplane['Ang_Thebe'] = ang_thebe_arr.astype(
                float)  # Angle to Thebe, in radians
            backplane['Ang_Metis'] = ang_metis_arr.astype(float)
            backplane['Ang_Amalthea'] = ang_amalthea_arr.astype(float)
            backplane['Ang_Adrastea'] = ang_adrastea_arr.astype(float)

            # If distance to any of the small sats is < 0.3 deg, then delete that entry in the dictionary

            if (np.amin(ang_thebe_arr) > fov_lorri):
                del backplane['Ang_Thebe']
            else:
                print("Keeping Thebe".format(np.min(ang_thebe_arr) * hbt.r2d))

            if (np.amin(ang_metis_arr) > fov_lorri):
                del backplane['Ang_Metis']
            else:
                print("Keeping Metis, min = {} deg".format(
                    np.min(ang_metis_arr) * hbt.r2d))

            if (np.amin(ang_amalthea_arr) > fov_lorri):
                del backplane['Ang_Amalthea']
            else:
                print("Keeping Amalthea, min = {} deg".format(
                    np.amin(ang_amalthea_arr) * hbt.r2d))

            if (np.amin(ang_adrastea_arr) > fov_lorri):
                del backplane['Ang_Adrastea']
            else:
                print("Keeping Adrastea".format(
                    np.min(ang_adrastea_arr) * hbt.r2d))

    # And return the backplane set

    return (backplane, desc)
def 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)