<jupyter_start><jupyter_text>CSM ISDThe community sensor model utilizes the concept of image support data to disentagle the process of collecting and collating a priori sensor information from the process of determining where the image is in relation to some reference frame.  In this notebook, we leverage the PVL and SpiceyPy Python libraries to generate two CSM compliant ISD objects.<jupyter_code>import json
import spiceypy as spice
import pvl
import cycsm as csm  # This is the USGS cython wrapper to the C++ CSM
# Utility Func for working with PVL
def find_in_dict(obj, key):
    """
    Recursively find an entry in a dictionary

    Parameters
    ----------
    obj : dict
          The dictionary to search
    key : str
          The key to find in the dictionary

    Returns
    -------
    item : obj
           The value from the dictionary
    """
    if key in obj:
        return obj[key]
    for k, v in obj.items():
        if isinstance(v,dict):
            item = find_in_dict(v, key)
            if item is not None:
                return item<jupyter_output><empty_output><jupyter_text>MDIS NAC We start with the MDIS NAC camera and generate a JSON ISD string that can be used to create an ISD.  This ISD utilizes the NAIF Spice Library and SPICE kernels.First, we check for the kernels that will be furnished.<jupyter_code>!ls ../tests/data/
ikid = 236820
# Load kernels same order ISIS Spice::init() does
# Frame
# TargetAttitudeShape
spice.furnsh('../tests/data/pck00010_msgr_v23.tpc')
# Instrument
spice.furnsh("../tests/data/msgr_mdis_v160.ti")
# InstrumentAddendum
spice.furnsh('../tests/data/mdisAddendum009.ti')
# LeapSecond
spice.furnsh('../tests/data/naif0012.tls')
# SpacecraftClock
spice.furnsh('../tests/data/messenger_2548.tsc')
# Extra
# TargetPosition
spice.furnsh('../tests/data/de423s.bsp')
# InstrumentPointing
spice.furnsh('../tests/data/msgr20130404.bc')
spice.furnsh('../tests/data/msgr20130405.bc')
spice.furnsh('../tests/data/msgr20130406.bc')
spice.furnsh('../tests/data/msgr20130407.bc')
spice.furnsh('../tests/data/msgr20130408.bc')
spice.furnsh('../tests/data/msgr20130409.bc')
spice.furnsh('../tests/data/msgr20130410.bc')
spice.furnsh('../tests/data/msgr20130411.bc')
spice.furnsh('../tests/data/1072683119_1965_mdis_atthist.bc')
spice.furnsh('../tests/data/1072716050_291010_mdis_pivot_pvtres.bc')
spice.furnsh('../tests/data/msgr_v231.tf')
# InstrumentPosition
spice.furnsh('../tests/data/msgr_20040803_20150430_od431sc_2.bsp')<jupyter_output><empty_output><jupyter_text>The ISD object is a python dictionary.<jupyter_code># Create the ISD object
isd = {}

# Load information from the IK kernel
isd['focal_length'] = spice.gdpool('INS-{}_FOCAL_LENGTH'.format(ikid), 0, 1)
isd['focal_length_epsilon'] = spice.gdpool('INS-{}_FL_UNCERTAINTY'.format(ikid), 0, 1)
isd['nlines'] = spice.gipool('INS-{}_PIXEL_LINES'.format(ikid), 0, 1)
isd['nsamples'] = spice.gipool('INS-{}_PIXEL_SAMPLES'.format(ikid), 0, 1)
isd['original_half_lines'] = isd['nlines'] / 2.0
isd['original_half_samples'] = isd['nsamples'] / 2.0
isd['pixel_pitch'] = spice.gdpool('INS-{}_PIXEL_PITCH'.format(ikid), 0, 1)
isd['ccd_center'] = spice.gdpool('INS-{}_CCD_CENTER'.format(ikid), 0, 2)
isd['ifov'] = spice.gdpool('INS-{}_IFOV'.format(ikid), 0, 1)
isd['boresight'] = spice.gdpool('INS-{}_BORESIGHT'.format(ikid), 0, 3)
isd['transx'] = spice.gdpool('INS-{}_TRANSX'.format(ikid), 0, 3)
isd['transy'] = spice.gdpool('INS-{}_TRANSY'.format(ikid), 0, 3)
isd['itrans_sample'] = spice.gdpool('INS-{}_ITRANSS'.format(ikid), 0, 3)
isd['itrans_line'] = spice.gdpool('INS-{}_ITRANSL'.format(ikid), 0, 3)
isd['odt_x'] = spice.gdpool('INS-{}_OD_T_X'.format(ikid), 0, 10)
isd['odt_y'] = spice.gdpool('INS-{}_OD_T_Y'.format(ikid), 0, 10)
isd['starting_detector_sample'] = spice.gdpool('INS-{}_FPUBIN_START_SAMPLE'.format(ikid), 0, 1)
isd['starting_detector_line'] = spice.gdpool('INS-{}_FPUBIN_START_LINE'.format(ikid), 0, 1)<jupyter_output><empty_output><jupyter_text>DistortionThe ik kernel defines a temperature dependent focal length distortion.  This needs to be handled outside of the camera.  It is the responsibility of the Sensor Exploitation Tool (SET).<jupyter_code>def distort_focal_length(coeffs, t):
    """
    Compute the distorted focal length
    
    Parameters
    ----------
    coeffs : iterable
             of coefficient values
    t : float
        temperature in C
        
    Returns
    -------
    focal_length : float
                   the temperature adjusted focal length
    """
    focal_length = coeffs[0]
    for i in range(1, len(coeffs[1:])):
        focal_length += coeffs[i]*t**i
    return focal_length<jupyter_output><empty_output><jupyter_text>ISIS Cube LoadHere we load the PVL from an ISIS cube in order to get image dependent information.  It is also possible (though not implemented) to bypass the cube format and load a .IMG, a .tiff, etc.  The SET can implement what ever mechanism it wants in order to get the necessary information for an ISD.<jupyter_code># Load the ISIS Cube header
header = pvl.load('../tests/data/EN1007907102M.cub')

isd['instrument_id'] = find_in_dict(header, 'InstrumentId')
isd['spacecraft_name'] = find_in_dict(header, 'SpacecraftName')
isd['target_name'] = find_in_dict(header, 'TargetName')

# Get the radii from SPICE
rad = spice.bodvrd(isd['target_name'], 'RADII', 3)
radii = rad[1]
isd['semi_major_axis'] = rad[1][0]
isd['semi_minor_axis'] = rad[1][1]

# Get temperature from SPICE and adjust focal length
spice.gdpool('INS-{}_FOCAL_LENGTH'.format(ikid), 0, 1)
temp_coeffs = spice.gdpool('INS-{}_FL_TEMP_COEFFS'.format(ikid), 0, 6)
temp = find_in_dict(header, 'FocalPlaneTemperature').value
isd['focal_length'] = distort_focal_length(temp_coeffs, temp)<jupyter_output><empty_output><jupyter_text>PVL HeaderA nice byproduct of this process is that we have the PVL header as a python dict as well.<jupyter_code>header<jupyter_output><empty_output><jupyter_text>TimeNext we need to compute the time.  Since this is a framing camera, we take the mean of the start and stop time.<jupyter_code># Here convert the sclock
sclock = find_in_dict(header, 'SpacecraftClockCount')
exposure_duration = find_in_dict(header, 'ExposureDuration')
exposure_duration = exposure_duration.value * 0.001  # Scale to seconds

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

isd['ephemeris_time'] = et<jupyter_output><empty_output><jupyter_text>Sensor PositionThe CSM requires that the sensor position be provided in some reference frame.  We use the spice routines to get the senosor position in the Mercury Reference Frame.<jupyter_code># Comparing spacecraft position direct to Mercury body-fixed frame with J2000 to Mercury body-fixed
loc_direct, _ = spice.spkpos(isd['target_name'], isd['ephemeris_time'], 'IAU_MERCURY', 'LT+S', 'MESSENGER')
print(loc_direct * -1000)

# Target=Mercury, Observer=Messenger, go to J2000 first (to match ISIS3)
loc, ltc = spice.spkpos(isd['target_name'], isd['ephemeris_time'], 'J2000', 'LT+S', 'MESSENGER')
# Get the transformation from J2000 to Mercury body-fixed frame
rotation = spice.pxform('J2000', 'IAU_MERCURY', isd['ephemeris_time'])
loc = spice.mxv(rotation, loc)

# Scale to meters and reverse direction (since target=Mercury and observer=messenger)
# we want vector from Mercury origin to spacecraft position
isd['x_sensor_origin'] = loc[0] * -1000
isd['y_sensor_origin'] = loc[1] * -1000
isd['z_sensor_origin'] = loc[2] * -1000<jupyter_output>[ 1728181.06360082 -2088202.56686554  2082707.60899282]<jupyter_text>Sensor OrientationThe CSM also requires the sensor orientation as $\omega, \phi, \kappa$.  Again, NAIF provides this easily.  Note the order that NAIF returns though!<jupyter_code># Get the rotation angles from MDIS NAC frame to Mercury body-fixed frame
camera2bodyfixed = spice.pxform('MSGR_MDIS_NAC','IAU_MERCURY', isd['ephemeris_time'])
opk = spice.m2eul(camera2bodyfixed, 3, 2, 1)

isd['omega'] = opk[2]
isd['phi'] = opk[1]
isd['kappa'] = opk[0]<jupyter_output><empty_output><jupyter_text>Sun PositionHaving the camera metadata and positional information is sufficient for image to ground and ground to image calls.  We also want to support the CSM API for illumination direction.  Therefore, we need to know where the sun is.<jupyter_code># Get the sun's position relative to a Mercury-fixed frame.
target = "SUN"
et = isd['ephemeris_time']
reference_frame = "IAU_MERCURY"
light_time_correction = "LT+S"
observer = "MERCURY"

sun_state, lt = spice.spkezr(target,
                             et,
                             reference_frame,
                             light_time_correction,
                             observer)

# Convert to meters
isd['x_sun_position'] = sun_state[0] * 1000
isd['y_sun_position'] = sun_state[1] * 1000
isd['z_sun_position'] = sun_state[2] * 1000
print("SUN POSITION (m): {} {} {}".format(sun_state[0]*1000,
                                          sun_state[1]*1000,
                                          sun_state[2]*1000))

# Get velocity of mdis nac (right now it is getting velocity of spacecraft)
target = "MESSENGER"
et = isd['ephemeris_time']
reference_frame = "IAU_MERCURY"
light_time_correction = "LT+S"
observer = "MERCURY"
messenger_state, lt = spice.spkezr(target,
                                   et,
                                   reference_frame,
                                   light_time_correction,
                                   observer)
print("MESSENGER VELOCITY (m/s): {} {} {}".format(messenger_state[3]*1000,
                                                  messenger_state[4]*1000,
                                                  messenger_state[5]*1000))

v,_ = spice.spkezr(observer, et, reference_frame, light_time_correction, target)
print(v)<jupyter_output>SUN POSITION (m): -31648725087.588726 -60633907522.72863 -38729485.77334732
MESSENGER VELOCITY (m/s): 1998.2873093664398 -1800.8962292264728 -1674.7631538476487
[ -1.72818106e+03   2.08820257e+03  -2.08270761e+03  -1.99769080e+00
   1.80028686e+00   1.67474034e+00]<jupyter_text>Done! We now have an ISD that is ready for ingestion into a CSM camera model.<jupyter_code>import numpy as np

# Writer class to serialize numpy arrays
class NumpyAwareJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.ndarray) and obj.ndim == 1:
            lobj = obj.tolist()
            if len(lobj) == 1:
                return lobj[0]
            else:
                return lobj
        return json.JSONEncoder.default(self, obj)

# Write it out
with open('isd.isd', 'w') as f:
    f.write(json.dumps(isd, f, cls=NumpyAwareJSONEncoder, sort_keys=True, indent=4))
!cat isd.isd<jupyter_output>{
    "boresight": [
        0.0,
        0.0,
        1.0
    ],
    "ccd_center": [
        512.5,
        512.5
    ],
    "ephemeris_time": 418855170.49264956,
    "focal_length": 549.2347965210602,
    "focal_length_epsilon": 0.5,
    "ifov": 25.44,
    "instrument_id": "MDIS-NAC",
    "itrans_line": [
        0.0,
        0.0,
        71.42857143
    ],
    "itrans_sample": [
        0.0,
        71.42857143,
        0.0
    ],
    "kappa": -0.963008015000929,
    "nlines": 1024,
    "nsamples": 1024,
    "odt_x": [
        0.0,
        1.001854269623802,
        0.0,
        0.0,
        -0.0005094440474941111,
        0.0,
        1.004010471468856e-05,
        0.0,
        1.004010471468856e-05,
        0.0
    ],
    "odt_y": [
        0.0,
        0.0,
        1.0,
        0.0009060010594996751,
        0.0,
        0.0003574842626620758,
        0.0,
        1.004010471468856e-05,
        0.0,
        1.004010471468856e-05
[...]
{'fraction_non_alphanumeric': 0.1248841324681891, 'fraction_numerical': 0.06758237128170556, 'mean_word_length': 3.4069810620126253, 'pattern_counts': {'":': 14, '<': 42, '<?xml version=': 0, '>': 42, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '657111', 'license': 'Unlicense', 'n_tokens_mistral': 4767, 'n_tokens_neox': 4079, 'n_words': 984, 'path': 'notebooks/Generating an ISD - MDIS NAC.ipynb', 'repo_name': 'kberryUSGS/CSM-SET', 'size': '11867'}