Coordinates computations using SPICE kernels#

How to use SPICE kernels provided by space missions to perform coordinates computations.

The SPICE observation geometry information system is being increasingly used by space missions to describe the locations of spacecraft and the time-varying orientations of reference frames. Here are two examples of mission SPICE kernels:

The sunpy.coordinates.spice module enables the use of the SkyCoord API to perform SPICE computations such as the location of bodies or the transformation of a vector from one coordinate frame to another coordinate frame. Although SPICE kernels can define coordinate frames that are very similar to the frames that sunpy.coordinates already provides, there will very likely be slight differences. Using sunpy.coordinates.spice will ensure that the definitions are exactly what the mission specifies and that the results are identical to other implementations of SPICE (e.g., CSPICE or Icy).

Note

This example requires the optional dependency spiceypy to be installed.

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.dates import DateFormatter

import astropy.units as u
from astropy.coordinates import SkyCoord

from sunpy.coordinates import spice
from sunpy.data import cache
from sunpy.time import parse_time

Download a small subset (~30 MB) of the Solar Orbiter SPICE kernel set that corresponds to about a day of the mission.

obstime = parse_time('2022-10-12') + np.arange(720) * u.min

kernel_urls = [
    "ck/solo_ANC_soc-sc-fof-ck_20180930-21000101_V03.bc",
    "ck/solo_ANC_soc-stix-ck_20180930-21000101_V03.bc",
    "ck/solo_ANC_soc-flown-att_20221011T142135-20221012T141817_V01.bc",
    "fk/solo_ANC_soc-sc-fk_V09.tf",
    "fk/solo_ANC_soc-sci-fk_V08.tf",
    "ik/solo_ANC_soc-stix-ik_V02.ti",
    "lsk/naif0012.tls",
    "pck/pck00010.tpc",
    "sclk/solo_ANC_soc-sclk_20231015_V01.tsc",
    "spk/de421.bsp",
    "spk/solo_ANC_soc-orbit-stp_20200210-20301120_280_V1_00288_V01.bsp",
]
kernel_urls = [f"http://spiftp.esac.esa.int/data/SPICE/SOLAR-ORBITER/kernels/{url}"
               for url in kernel_urls]

kernel_files = [cache.download(url) for url in kernel_urls]

Initialize sunpy.coordinates.spice with these kernels, which will create classes for SkyCoord to use.

spice.initialize(kernel_files)
INFO: Installing SOLO_IAU_SUN_2003 PCK frame (-144994) as 'spice_SOLO_IAU_SUN_2003' [sunpy.coordinates.spice]
INFO: Creating ICRF frame with SUN (10) origin [sunpy.coordinates.spice]
INFO: Installing SOLO_IAU_SUN_2009 PCK frame (-144993) as 'spice_SOLO_IAU_SUN_2009' [sunpy.coordinates.spice]
INFO: Installing SOLO_STIX_ILS CK frame (-144851) as 'spice_SOLO_STIX_ILS' [sunpy.coordinates.spice]
INFO: Creating ICRF frame with SOLAR-ORBITER (-144) origin [sunpy.coordinates.spice]
INFO: Installing SOLO_SPICE_LW_ILS CK frame (-144821) as 'spice_SOLO_SPICE_LW_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_SPICE_SW_ILS CK frame (-144811) as 'spice_SOLO_SPICE_SW_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_SOLOHI_ILS CK frame (-144701) as 'spice_SOLO_SOLOHI_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_PHI_HRT_ILS CK frame (-144521) as 'spice_SOLO_PHI_HRT_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_PHI_FDT_ILS CK frame (-144511) as 'spice_SOLO_PHI_FDT_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_METIS_M0_TEL CK frame (-144431) as 'spice_SOLO_METIS_M0_TEL' [sunpy.coordinates.spice]
INFO: Installing SOLO_METIS_IEO-M0 CK frame (-144430) as 'spice_SOLO_METIS_IEOnM0' [sunpy.coordinates.spice]
INFO: Installing SOLO_METIS_VIS_ILS CK frame (-144421) as 'spice_SOLO_METIS_VIS_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_METIS_EUV_ILS CK frame (-144411) as 'spice_SOLO_METIS_EUV_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_EUI_HRI_EUV_ILS CK frame (-144231) as 'spice_SOLO_EUI_HRI_EUV_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_EUI_HRI_LYA_ILS CK frame (-144221) as 'spice_SOLO_EUI_HRI_LYA_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_EUI_FSI_ILS CK frame (-144211) as 'spice_SOLO_EUI_FSI_ILS' [sunpy.coordinates.spice]
INFO: Installing SOLO_INS_BOOM_OB CK frame (-144042) as 'spice_SOLO_INS_BOOM_OB' [sunpy.coordinates.spice]
INFO: Installing SOLO_INS_BOOM_IB CK frame (-144041) as 'spice_SOLO_INS_BOOM_IB' [sunpy.coordinates.spice]
INFO: Installing SOLO_MGA_EL CK frame (-144031) as 'spice_SOLO_MGA_EL' [sunpy.coordinates.spice]
INFO: Installing SOLO_SA-Y CK frame (-144017) as 'spice_SOLO_SAnY' [sunpy.coordinates.spice]
INFO: Creating ICRF frame with SOLO_SA-Y (-144017) origin [sunpy.coordinates.spice]
INFO: Installing SOLO_SA+Y CK frame (-144015) as 'spice_SOLO_SApY' [sunpy.coordinates.spice]
INFO: Creating ICRF frame with SOLO_SA+Y (-144015) origin [sunpy.coordinates.spice]
INFO: Installing SOLO_HGA_AZ CK frame (-144012) as 'spice_SOLO_HGA_AZ' [sunpy.coordinates.spice]
INFO: Installing SOLO_HGA_EL CK frame (-144011) as 'spice_SOLO_HGA_EL' [sunpy.coordinates.spice]
INFO: Installing SOLO_FOF CK frame (-144001) as 'spice_SOLO_FOF' [sunpy.coordinates.spice]
INFO: Installing SOLO_SRF CK frame (-144000) as 'spice_SOLO_SRF' [sunpy.coordinates.spice]
INFO: Installing SOLO_SWA_EAS2-SCI TK frame (-144877) as 'spice_SOLO_SWA_EAS2nSCI' [sunpy.coordinates.spice]
INFO: Installing SOLO_SWA_EAS1-SCI TK frame (-144876) as 'spice_SOLO_SWA_EAS1nSCI' [sunpy.coordinates.spice]
INFO: Installing SOLO_SWA_EAS2 TK frame (-144875) as 'spice_SOLO_SWA_EAS2' [sunpy.coordinates.spice]
INFO: Installing SOLO_SWA_EAS1 TK frame (-144874) as 'spice_SOLO_SWA_EAS1' [sunpy.coordinates.spice]
INFO: Installing SOLO_SWA_EAS TK frame (-144873) as 'spice_SOLO_SWA_EAS' [sunpy.coordinates.spice]
INFO: Installing SOLO_SWA_PAS TK frame (-144872) as 'spice_SOLO_SWA_PAS' [sunpy.coordinates.spice]
INFO: Installing SOLO_SWA_HIS TK frame (-144871) as 'spice_SOLO_SWA_HIS' [sunpy.coordinates.spice]
INFO: Installing SOLO_STIX_OPT TK frame (-144852) as 'spice_SOLO_STIX_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_SPICE_LW_OPT TK frame (-144822) as 'spice_SOLO_SPICE_LW_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_SPICE_SW_OPT TK frame (-144812) as 'spice_SOLO_SPICE_SW_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_SOLOHI_OPT TK frame (-144702) as 'spice_SOLO_SOLOHI_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_RPW_SCM TK frame (-144640) as 'spice_SOLO_RPW_SCM' [sunpy.coordinates.spice]
INFO: Installing SOLO_RPW_ANT_3 TK frame (-144630) as 'spice_SOLO_RPW_ANT_3' [sunpy.coordinates.spice]
INFO: Installing SOLO_RPW_ANT_2 TK frame (-144620) as 'spice_SOLO_RPW_ANT_2' [sunpy.coordinates.spice]
INFO: Installing SOLO_RPW_ANT_1 TK frame (-144610) as 'spice_SOLO_RPW_ANT_1' [sunpy.coordinates.spice]
INFO: Installing SOLO_PHI_HRT_OPT TK frame (-144522) as 'spice_SOLO_PHI_HRT_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_PHI_FDT_OPT TK frame (-144512) as 'spice_SOLO_PHI_FDT_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_METIS_VIS_OPT TK frame (-144422) as 'spice_SOLO_METIS_VIS_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_METIS_EUV_OPT TK frame (-144412) as 'spice_SOLO_METIS_EUV_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_MAG_OBS TK frame (-144302) as 'spice_SOLO_MAG_OBS' [sunpy.coordinates.spice]
INFO: Installing SOLO_MAG_IBS TK frame (-144301) as 'spice_SOLO_MAG_IBS' [sunpy.coordinates.spice]
INFO: Installing SOLO_EUI_HRI_EUV_OPT TK frame (-144232) as 'spice_SOLO_EUI_HRI_EUV_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_EUI_HRI_LYA_OPT TK frame (-144222) as 'spice_SOLO_EUI_HRI_LYA_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_EUI_FSI_OPT TK frame (-144212) as 'spice_SOLO_EUI_FSI_OPT' [sunpy.coordinates.spice]
INFO: Installing SOLO_EPD_EPT-HET_PY TK frame (-144122) as 'spice_SOLO_EPD_EPTnHET_PY' [sunpy.coordinates.spice]
INFO: Installing SOLO_EPD_EPT-HET_MY TK frame (-144121) as 'spice_SOLO_EPD_EPTnHET_MY' [sunpy.coordinates.spice]
INFO: Installing SOLO_EPD_SIS_SW TK frame (-144112) as 'spice_SOLO_EPD_SIS_SW' [sunpy.coordinates.spice]
INFO: Installing SOLO_EPD_SIS_ASW TK frame (-144111) as 'spice_SOLO_EPD_SIS_ASW' [sunpy.coordinates.spice]
INFO: Installing SOLO_EPD_SIS TK frame (-144110) as 'spice_SOLO_EPD_SIS' [sunpy.coordinates.spice]
INFO: Installing SOLO_EPD_STEP TK frame (-144100) as 'spice_SOLO_EPD_STEP' [sunpy.coordinates.spice]
INFO: Installing SOLO_STR-2 TK frame (-144052) as 'spice_SOLO_STRn2' [sunpy.coordinates.spice]
INFO: Installing SOLO_STR-1 TK frame (-144051) as 'spice_SOLO_STRn1' [sunpy.coordinates.spice]
INFO: Installing SOLO_MGA_MRF TK frame (-144032) as 'spice_SOLO_MGA_MRF' [sunpy.coordinates.spice]
INFO: Installing SOLO_MGA_URF TK frame (-144030) as 'spice_SOLO_MGA_URF' [sunpy.coordinates.spice]
INFO: Installing SOLO_LGA_MZ TK frame (-144021) as 'spice_SOLO_LGA_MZ' [sunpy.coordinates.spice]
INFO: Installing SOLO_LGA_PZ TK frame (-144020) as 'spice_SOLO_LGA_PZ' [sunpy.coordinates.spice]
INFO: Installing SOLO_SA-Y_ZERO TK frame (-144016) as 'spice_SOLO_SAnY_ZERO' [sunpy.coordinates.spice]
INFO: Installing SOLO_SA+Y_ZERO TK frame (-144014) as 'spice_SOLO_SApY_ZERO' [sunpy.coordinates.spice]
INFO: Installing SOLO_HGA_MRF TK frame (-144013) as 'spice_SOLO_HGA_MRF' [sunpy.coordinates.spice]
INFO: Installing SOLO_HGA_URF TK frame (-144010) as 'spice_SOLO_HGA_URF' [sunpy.coordinates.spice]
INFO: Installing SUN_ARIES_ECL TK frame (1000010000) as 'spice_SUN_ARIES_ECL' [sunpy.coordinates.spice]
INFO: Installing SOLO_VSO dynamic frame (-144999) as 'spice_SOLO_VSO' [sunpy.coordinates.spice]
INFO: Creating ICRF frame with VENUS (299) origin [sunpy.coordinates.spice]
INFO: Installing EARTH_MECL_MEQX_J2000 dynamic frame (-144998) as 'spice_EARTH_MECL_MEQX_J2000' [sunpy.coordinates.spice]
INFO: Creating ICRF frame with EARTH (399) origin [sunpy.coordinates.spice]
INFO: Installing SOLO_HEE dynamic frame (-144997) as 'spice_SOLO_HEE' [sunpy.coordinates.spice]
INFO: Installing SOLO_GSE dynamic frame (-144996) as 'spice_SOLO_GSE' [sunpy.coordinates.spice]
INFO: Installing SOLO_GAE dynamic frame (-144995) as 'spice_SOLO_GAE' [sunpy.coordinates.spice]
INFO: Installing SOLO_SOLAR_MHP dynamic frame (-144992) as 'spice_SOLO_SOLAR_MHP' [sunpy.coordinates.spice]
INFO: Installing SOLO_SUN_RTN dynamic frame (-144991) as 'spice_SOLO_SUN_RTN' [sunpy.coordinates.spice]
INFO: Installing SOLO_HOR dynamic frame (-144985) as 'spice_SOLO_HOR' [sunpy.coordinates.spice]
INFO: Installing SOLO_GEORTN dynamic frame (-144984) as 'spice_SOLO_GEORTN' [sunpy.coordinates.spice]
INFO: Installing SOLO_HEEQ dynamic frame (-144983) as 'spice_SOLO_HEEQ' [sunpy.coordinates.spice]
INFO: Installing SOLO_HEE_NASA dynamic frame (-144982) as 'spice_SOLO_HEE_NASA' [sunpy.coordinates.spice]
INFO: Installing SOLO_HCI dynamic frame (-144981) as 'spice_SOLO_HCI' [sunpy.coordinates.spice]
INFO: Installing SOLO_ECLIPDATE dynamic frame (-144980) as 'spice_SOLO_ECLIPDATE' [sunpy.coordinates.spice]
INFO: Installing SOLO_GSM dynamic frame (-144962) as 'spice_SOLO_GSM' [sunpy.coordinates.spice]
INFO: Installing EARTH_MECL_MEQX dynamic frame (300399000) as 'spice_EARTH_MECL_MEQX' [sunpy.coordinates.spice]
INFO: Installing EARTH_SUN_ECL dynamic frame (300399005) as 'spice_EARTH_SUN_ECL' [sunpy.coordinates.spice]
INFO: Installing SUN_EARTH_CEQU dynamic frame (1000010001) as 'spice_SUN_EARTH_CEQU' [sunpy.coordinates.spice]
INFO: Installing SUN_EARTH_ECL dynamic frame (1000010002) as 'spice_SUN_EARTH_ECL' [sunpy.coordinates.spice]
INFO: Installing SUN_INERTIAL dynamic frame (1000010004) as 'spice_SUN_INERTIAL' [sunpy.coordinates.spice]

The above call automatically installs all SPICE frames defined in the kernels, but you may also want to use one of the built-in SPICE frames (e.g., inertial frames or body-fixed frames). Here, we manually install the ‘IAU_SUN’ built-in SPICE frame for potential later use.

spice.install_frame('IAU_SUN')
INFO: Installing IAU_SUN PCK frame (10010) as 'spice_IAU_SUN' [sunpy.coordinates.spice]

We can request the location of the spacecraft in any SPICE frame. Here, we request it in ‘SOLO_HEEQ’, which is Stonyhurst heliographic coordinates.

spacecraft = spice.get_body('Solar Orbiter', obstime, spice_frame='SOLO_HEEQ')
print(spacecraft[:4])
<SkyCoord (spice_SOLO_HEEQ: obstime=['2022-10-12T00:00:00.000' '2022-10-12T00:01:00.000'
 '2022-10-12T00:02:00.000' '2022-10-12T00:03:00.000']): (lon, lat, distance) in (deg, deg, km)
    [(-119.01442862, -3.70146925, 43862837.40904176),
     (-119.00981917, -3.7008108 , 43862685.53850186),
     (-119.0052097 , -3.70015231, 43862533.79895207),
     (-119.00060019, -3.69949378, 43862382.19039429)]>

Plot the radial distance from the Sun over the time range.

fig = plt.figure()
ax = fig.add_subplot()
ax.plot(obstime.datetime64, spacecraft.distance.to('AU'))
ax.xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax.set_xlabel('2022 October 12 (UTC)')
ax.set_ylabel('Radial distance (AU)')
ax.set_title('Solar Orbiter distance from Sun center')
Solar Orbiter distance from Sun center
Text(0.5, 1.0, 'Solar Orbiter distance from Sun center')

We can then transform the coordinate to a different SPICE frame. When specifying the frame for SkyCoord, SPICE frame names should be prepended with 'spice_'. Here, we transform it to ‘SOLO_GAE’.

spacecraft_gae = spacecraft.transform_to("spice_SOLO_GAE")
print(spacecraft_gae[:4])
<SkyCoord (spice_SOLO_GAE: obstime=['2022-10-12T00:00:00.000' '2022-10-12T00:01:00.000'
 '2022-10-12T00:02:00.000' '2022-10-12T00:03:00.000']): (lon, lat, distance) in (deg, deg, km)
    [(-149.06635461, -1.03769419, 1.74975667e+08),
     (-149.06492563, -1.03771064, 1.74972891e+08),
     (-149.06349666, -1.03772709, 1.74970115e+08),
     (-149.06206771, -1.03774353, 1.74967339e+08)]>

Plot the radial distance from the Earth over the time range.

fig = plt.figure()
ax = fig.add_subplot()
ax.plot(obstime.datetime64, spacecraft_gae.distance.to('AU'))
ax.xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax.set_xlabel('2022 October 12 (UTC)')
ax.set_ylabel('Radial distance (AU)')
ax.set_title('Solar Orbiter distance from Earth center')
Solar Orbiter distance from Earth center
Text(0.5, 1.0, 'Solar Orbiter distance from Earth center')

We can also leverage the Solar Orbiter SPICE kernels to look at instrument pointing. Let’s define a coordinate that points directly along the line of sight of the STIX instrument. For the ‘SOLO_STIX_ILS’ frame, 0 degrees longitude is in the anti-Sun direction, while 180 degrees longitude is in the Sun direction.

stix_ils = SkyCoord(np.repeat(0*u.deg, len(obstime)),
                    np.repeat(0*u.deg, len(obstime)),
                    frame='spice_SOLO_STIX_ILS', obstime=obstime)
print(stix_ils[:4])
<SkyCoord (spice_SOLO_STIX_ILS: obstime=['2022-10-12T00:00:00.000' '2022-10-12T00:01:00.000'
 '2022-10-12T00:02:00.000' '2022-10-12T00:03:00.000']): (lon, lat) in deg
    [(0., 0.), (0., 0.), (0., 0.), (0., 0.)]>

We can transform that line of sight to the SPICE frame ‘SOLO_SUN_RTN’, which is similar to helioprojective coordinates as observed from Solar Orbiter, except that the disk center is at 180 degrees longitude instead of 0 degrees longitude. Given how the line-of-sight coordinate is defined above, the latitude and longitude values of the resulting coordinate are the pitch and yaw offsets from disk center, respectively.

stix_ils_rtn = stix_ils.transform_to("spice_SOLO_SUN_RTN")
print(stix_ils_rtn[:4])
<SkyCoord (spice_SOLO_SUN_RTN: obstime=['2022-10-12T00:00:00.000' '2022-10-12T00:01:00.000'
 '2022-10-12T00:02:00.000' '2022-10-12T00:03:00.000']): (lon, lat) in deg
    [( 4.39956418e-05, -9.64403588e-06),
     (-6.14438994e-06,  3.36046063e-05),
     ( 7.84301580e-05,  5.45084475e-05),
     ( 1.38833163e-04, -2.82398730e-05)]>

Plot the pitch/yaw offsets over the time range.

fig, ax = plt.subplots(2, 1)
ax[0].plot(obstime.datetime64, stix_ils_rtn.lat.to('arcsec'))
ax[0].xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax[0].set_xlabel('2022 October 12 (UTC)')
ax[0].set_ylabel('Pitch offset (arcsec)')
ax[1].plot(obstime.datetime64, stix_ils_rtn.lon.to('arcsec'))
ax[1].xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax[1].set_xlabel('2022 October 12 (UTC)')
ax[1].set_ylabel('Yaw offset (arcsec)')
ax[0].set_title('Pointing offset of STIX from disk center')

plt.show()
Pointing offset of STIX from disk center

Finally, we can query the instrument field of view (FOV) via SPICE, which will be in the ‘SOLO_STIX_ILS’ frame. This call returns the corners of the rectangular FOV of the STIX instrument, and you can see they are centered around 180 degrees longitude, which is the direction of the Sun in this frame.

stix_fov = spice.get_fov('SOLO_STIX', obstime[0])
print(stix_fov)
<SkyCoord (spice_SOLO_STIX_ILS: obstime=2022-10-12T00:00:00.000): (lon, lat) in deg
    [( 179.,  0.99984773), ( 179., -0.99984773), (-179., -0.99984773),
     (-179.,  0.99984773)]>

More usefully, every coordinate in a SPICE frame has a to_helioprojective() method that converts the coordinate to Helioprojective with the observer at the center of te SPICE frame. For the ‘SOLO_STIX_ILS’ frame, the center is Solar Orbiter, which is exactly what we want.

print(stix_fov.to_helioprojective())
<SkyCoord (Helioprojective: obstime=2022-10-12T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate (obstime=2022-10-12T00:00:00.000, rsun=695700.0 km): (lon, lat, radius) in (deg, deg, km)
    (-119.01442821, -3.70146956, 43862838.51397399)>): (Tx, Ty) in arcsec
    [( 3127.44364431,  4016.75973995), ( 4017.02831673, -3126.97425142),
     (-3127.76039243, -4016.69031087), (-4017.34510585,  3127.04367531)]>

Total running time of the script: (0 minutes 13.527 seconds)

Gallery generated by Sphinx-Gallery