diff --git a/examples/sensor_utils.ipynb b/examples/sensor_utils.ipynb
index 582920f5e5d1fe9b9198397569fdc75cc32af720..8a6786c1b116ac682c8f103afe8344e05dcf52ce 100644
--- a/examples/sensor_utils.ipynb
+++ b/examples/sensor_utils.ipynb
@@ -15,9 +15,15 @@
    "source": [
     "import os\n",
     "\n",
+    "os.environ[\"ISISROOT\"] = \"/Users/astamile/ISIS3/build\"\n",
+    "os.environ[\"ISISDATA\"] = \"/Volumes/isis_data1/isis_data/\"\n",
+    "\n",
     "from csmapi import csmapi\n",
     "from knoten import csm, sensor_utils\n",
     "\n",
+    "from knoten.shape import Ellipsoid\n",
+    "from knoten.illuminator import Illuminator\n",
+    "\n",
     "import ale\n",
     "import json"
    ]
@@ -31,18 +37,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "/Users/astamile/opt/anaconda3/envs/knoten/lib/python3.9/site-packages/osgeo/gdal.py:312: FutureWarning: Neither gdal.UseExceptions() nor gdal.DontUseExceptions() has been explicitly called. In GDAL 4.0, exceptions will be enabled by default.\n",
-      "  warnings.warn(\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "fileName = \"data/N1573082850_1.cub\"\n",
     "\n",
@@ -68,7 +65,9 @@
    "outputs": [],
    "source": [
     "camera = csm.create_csm(csm_isd)\n",
-    "image_pt = csmapi.ImageCoord(511.5, 511.5)"
+    "image_pt = csmapi.ImageCoord(511.5, 511.5)\n",
+    "shape = Ellipsoid.from_csm_sensor(camera)\n",
+    "illuminator = Illuminator()"
    ]
   },
   {
@@ -79,7 +78,7 @@
     {
      "data": {
       "text/plain": [
-       "38.87212509629893"
+       "38.87212509629895"
       ]
      },
      "execution_count": 4,
@@ -88,7 +87,7 @@
     }
    ],
    "source": [
-    "phaseAngle = sensor_utils.phase_angle(image_pt, camera)\n",
+    "phaseAngle = sensor_utils.phase_angle(image_pt, camera, shape, illuminator)\n",
     "\n",
     "phaseAngle"
    ]
@@ -101,7 +100,7 @@
     {
      "data": {
       "text/plain": [
-       "49.60309924896046"
+       "49.60309924893989"
       ]
      },
      "execution_count": 5,
@@ -110,7 +109,7 @@
     }
    ],
    "source": [
-    "emissionAngle = sensor_utils.emission_angle(image_pt, camera)\n",
+    "emissionAngle = sensor_utils.emission_angle(image_pt, camera, shape)\n",
     "\n",
     "emissionAngle"
    ]
@@ -123,7 +122,7 @@
     {
      "data": {
       "text/plain": [
-       "2903512972.146144"
+       "2903512972.146115"
       ]
      },
      "execution_count": 6,
@@ -132,7 +131,7 @@
     }
    ],
    "source": [
-    "slantDistance = sensor_utils.slant_distance(image_pt, camera)\n",
+    "slantDistance = sensor_utils.slant_distance(image_pt, camera, shape)\n",
     "\n",
     "slantDistance"
    ]
@@ -189,7 +188,7 @@
     {
      "data": {
       "text/plain": [
-       "59096282.02424558"
+       "59096282.024265066"
       ]
      },
      "execution_count": 9,
@@ -198,7 +197,7 @@
     }
    ],
    "source": [
-    "localRadius = sensor_utils.local_radius(image_pt, camera)\n",
+    "localRadius = sensor_utils.local_radius(image_pt, camera, shape)\n",
     "\n",
     "localRadius"
    ]
@@ -233,7 +232,7 @@
     {
      "data": {
       "text/plain": [
-       "17397.960941876583"
+       "17397.96094194587"
       ]
      },
      "execution_count": 11,
@@ -242,7 +241,7 @@
     }
    ],
    "source": [
-    "lineResolution = sensor_utils.line_resolution(image_pt, camera)\n",
+    "lineResolution = sensor_utils.line_resolution(image_pt, camera, shape)\n",
     "\n",
     "lineResolution"
    ]
@@ -255,7 +254,7 @@
     {
      "data": {
       "text/plain": [
-       "17397.933700407997"
+       "17397.93370038153"
       ]
      },
      "execution_count": 12,
@@ -264,7 +263,7 @@
     }
    ],
    "source": [
-    "sampleResolution = sensor_utils.sample_resolution(image_pt, camera)\n",
+    "sampleResolution = sensor_utils.sample_resolution(image_pt, camera, shape)\n",
     "\n",
     "sampleResolution"
    ]
@@ -277,7 +276,7 @@
     {
      "data": {
       "text/plain": [
-       "17397.94732114229"
+       "17397.9473211637"
       ]
      },
      "execution_count": 13,
@@ -286,7 +285,7 @@
     }
    ],
    "source": [
-    "pixelResolution = sensor_utils.pixel_resolution(image_pt, camera)\n",
+    "pixelResolution = sensor_utils.pixel_resolution(image_pt, camera, shape)\n",
     "\n",
     "pixelResolution"
    ]
diff --git a/knoten/csm.py b/knoten/csm.py
index 0a569d30556e7eecc699121ca82751b1502600db..3cb7f535914ae567b6fbfdbff9ef4508536a4eb0 100644
--- a/knoten/csm.py
+++ b/knoten/csm.py
@@ -9,7 +9,6 @@ import numpy as np
 import requests
 import scipy.stats
 from functools import singledispatch
-import spiceypy as spice
 
 from knoten.surface import EllipsoidDem
 
@@ -42,28 +41,6 @@ def get_radii(camera):
     semi_minor = ellipsoid.getSemiMinorRadius()
     return semi_major, semi_minor
 
-def get_surface_normal(sensor, ground_pt):
-    """
-    Given a sensor model and ground point, calculate the surface normal.
-
-    Parameters
-    ----------
-    sensor : object
-             A CSM compliant sensor model object
-
-    ground_pt: tuple
-               The ground point as an (x, y, z) tuple
-
-    Returns
-    -------
-     : tuple
-       in the form (x, y, z)
-    """
-    semi_major, semi_minor = get_radii(sensor)
-
-    normal = spice.surfnm(semi_major, semi_major, semi_minor, np.array([ground_pt.x, ground_pt.y, ground_pt.z]))
-    return utils.Point(normal[0], normal[1], normal[2])
-
 def create_camera(label, url='http://pfeffer.wr.usgs.gov/api/1.0/pds/'):
     """
     Given an ALE supported label, create a CSM compliant ISD file. This func
@@ -114,6 +91,9 @@ def get_state(sensor, image_pt):
     : dict
         Dictionary containing lookVec, sensorPos, sensorTime, and imagePoint
     """
+    if not isinstance(sensor, csmapi.RasterGM):
+        raise TypeError("inputted sensor not a csm.RasterGM object")
+    
     sensor_time = sensor.getImageTime(image_pt)
     locus = sensor.imageToRemoteImagingLocus(image_pt)
     sensor_position = sensor.getSensorPosition(image_pt)
diff --git a/knoten/illuminator.py b/knoten/illuminator.py
new file mode 100644
index 0000000000000000000000000000000000000000..09292ae1234092c8160536308a4926e3671e5fd0
--- /dev/null
+++ b/knoten/illuminator.py
@@ -0,0 +1,23 @@
+from knoten import csm, utils
+import spiceypy as spice
+import numpy as np
+
+class Illuminator:
+
+    def __init__(self, position=None, velocity=None):
+        self.position = position
+        self.velocity = velocity
+
+    def get_position_from_csm_sensor(self, sensor, ground_pt):
+        sunEcefVec = sensor.getIlluminationDirection(ground_pt)
+        self.position = utils.Point(ground_pt.x - sunEcefVec.x, ground_pt.y - sunEcefVec.y, ground_pt.z - sunEcefVec.z)
+        return self.position 
+    
+    # def get_position_from_time(self, sensor_state):
+    #     return 0
+    
+    # def get_velocity(self, sensor_state):
+    #     return 0
+
+
+        
\ No newline at end of file
diff --git a/knoten/sensor_utils.py b/knoten/sensor_utils.py
index 812fecf32804cbab0b408e8bb68821f4611119de..cfadaee2aa1b6d37d50e5d6e9d44161310103681 100644
--- a/knoten/sensor_utils.py
+++ b/knoten/sensor_utils.py
@@ -2,7 +2,7 @@ from knoten import csm, utils, bundle
 from csmapi import csmapi
 import numpy as np
 
-def phase_angle(image_pt, sensor):
+def phase_angle(image_pt, sensor, shape, illuminator):
     """
     Computes and returns phase angle, in degrees for a given image point.
    
@@ -18,6 +18,12 @@ def phase_angle(image_pt, sensor):
     sensor : object
              A CSM compliant sensor model object
 
+    shape : object
+            A shape model object
+
+    illuminator: object
+            An illuminator object
+
     Returns
     -------
      : np.ndarray
@@ -27,10 +33,9 @@ def phase_angle(image_pt, sensor):
         image_pt = csmapi.ImageCoord(*image_pt)
 
     sensor_state = csm.get_state(sensor, image_pt)
-    ground_pt = csm.generate_ground_point(0.0, image_pt, sensor)
+    ground_pt = shape.intersect_surface(sensor_state["sensorPos"], sensor_state["lookVec"])
   
-    sunEcefVec = sensor.getIlluminationDirection(ground_pt)
-    illum_pos = utils.Point(ground_pt.x - sunEcefVec.x, ground_pt.y - sunEcefVec.y, ground_pt.z - sunEcefVec.z)
+    illum_pos = illuminator.get_position_from_csm_sensor(sensor, ground_pt)
 
     vec_a = utils.Point(sensor_state["sensorPos"].x - ground_pt.x, 
                         sensor_state["sensorPos"].y - ground_pt.y, 
@@ -42,7 +47,7 @@ def phase_angle(image_pt, sensor):
   
     return np.rad2deg(utils.sep_angle(vec_a, vec_b))
 
-def emission_angle(image_pt, sensor):
+def emission_angle(image_pt, sensor, shape):
     """
     Computes and returns emission angle, in degrees, for a given image point.
 
@@ -62,6 +67,9 @@ def emission_angle(image_pt, sensor):
     sensor : object
              A CSM compliant sensor model object
 
+    shape : object
+            A shape model object
+
     Returns
     -------
      : np.ndarray
@@ -71,9 +79,9 @@ def emission_angle(image_pt, sensor):
         image_pt = csmapi.ImageCoord(*image_pt)
 
     sensor_state = csm.get_state(sensor, image_pt)
-    ground_pt = csm.generate_ground_point(0.0, image_pt, sensor)
+    ground_pt = shape.intersect_surface(sensor_state["sensorPos"], sensor_state["lookVec"])
 
-    normal = csm.get_surface_normal(sensor, ground_pt)
+    normal = shape.get_surface_normal(ground_pt)
 
     sensor_diff = utils.Point(sensor_state["sensorPos"].x - ground_pt.x, 
                               sensor_state["sensorPos"].y - ground_pt.y,
@@ -81,7 +89,7 @@ def emission_angle(image_pt, sensor):
     
     return np.rad2deg(utils.sep_angle(normal, sensor_diff))
 
-def slant_distance(image_pt, sensor):
+def slant_distance(image_pt, sensor, shape):
     """
     Computes the slant distance from the sensor to the ground point.
 
@@ -93,6 +101,9 @@ def slant_distance(image_pt, sensor):
     sensor : object
              A CSM compliant sensor model object
 
+    shape : object
+            A shape model object
+
     Returns
     -------
      : np.ndarray 
@@ -102,7 +113,7 @@ def slant_distance(image_pt, sensor):
         image_pt = csmapi.ImageCoord(*image_pt)
 
     sensor_state = csm.get_state(sensor, image_pt)
-    ground_pt = csm.generate_ground_point(0.0, image_pt, sensor)
+    ground_pt = shape.intersect_surface(sensor_state["sensorPos"], sensor_state["lookVec"])
 
     return utils.distance(sensor_state["sensorPos"], ground_pt)
 
@@ -154,7 +165,7 @@ def sub_spacecraft_point(image_pt, sensor):
 
     return utils.radians_to_degrees(lat_lon_rad)
 
-def local_radius(image_pt, sensor):
+def local_radius(image_pt, sensor, shape):
     """
     Gets the local radius for a given image point.
 
@@ -166,6 +177,9 @@ def local_radius(image_pt, sensor):
     sensor : object
              A CSM compliant sensor model object
 
+    shape : object
+            A shape model object
+
     Returns
     -------
      : np.ndarray
@@ -174,7 +188,9 @@ def local_radius(image_pt, sensor):
     if not isinstance(image_pt, csmapi.ImageCoord):
         image_pt = csmapi.ImageCoord(*image_pt)
 
-    ground_pt = csm.generate_ground_point(0.0, image_pt, sensor)
+    sensor_state = csm.get_state(sensor, image_pt)
+    ground_pt = shape.intersect_surface(sensor_state["sensorPos"], sensor_state["lookVec"])
+
     return utils.magnitude(ground_pt)
 
 def right_ascension_declination(image_pt, sensor):
@@ -204,7 +220,8 @@ def right_ascension_declination(image_pt, sensor):
 
     return (ra_dec.lon, ra_dec.lat)
 
-def line_resolution(image_pt, sensor):
+
+def line_resolution(image_pt, sensor, shape):
     """
     Compute the line resolution in meters per pixel for the current set point.
 
@@ -224,6 +241,9 @@ def line_resolution(image_pt, sensor):
     sensor : object
              A CSM compliant sensor model object
 
+    shape : object
+            A shape model object
+
     Returns
     -------
      : np.ndarray
@@ -232,14 +252,16 @@ def line_resolution(image_pt, sensor):
     if not isinstance(image_pt, csmapi.ImageCoord):
         image_pt = csmapi.ImageCoord(*image_pt)
 
-    ground_pt = csm.generate_ground_point(0.0, image_pt, sensor)
+    sensor_state = csm.get_state(sensor, image_pt)
+    ground_pt = shape.intersect_surface(sensor_state["sensorPos"], sensor_state["lookVec"])
+
     image_partials = bundle.compute_image_partials(sensor, ground_pt)
 
     return np.sqrt(image_partials[0] * image_partials[0] +
                    image_partials[2] * image_partials[2] +
                    image_partials[4] * image_partials[4])
 
-def sample_resolution(image_pt, sensor):
+def sample_resolution(image_pt, sensor, shape):
     """
     Compute the sample resolution in meters per pixel for the current set point.
 
@@ -255,6 +277,9 @@ def sample_resolution(image_pt, sensor):
     sensor : object
              A CSM compliant sensor model object
 
+    shape : object
+            A shape model object
+
     Returns
     -------
      : np.ndarray
@@ -263,14 +288,16 @@ def sample_resolution(image_pt, sensor):
     if not isinstance(image_pt, csmapi.ImageCoord):
         image_pt = csmapi.ImageCoord(*image_pt)
 
-    ground_pt = csm.generate_ground_point(0.0, image_pt, sensor)
+    sensor_state = csm.get_state(sensor, image_pt)
+    ground_pt = shape.intersect_surface(sensor_state["sensorPos"], sensor_state["lookVec"])
+
     image_partials = bundle.compute_image_partials(sensor, ground_pt)
 
     return np.sqrt(image_partials[1] * image_partials[1] +
                    image_partials[3] * image_partials[3] +
                    image_partials[5] * image_partials[5])
 
-def pixel_resolution(image_pt, sensor):
+def pixel_resolution(image_pt, sensor, shape):
     """
     Returns the pixel resolution at the current position in meters/pixel.
 
@@ -282,15 +309,80 @@ def pixel_resolution(image_pt, sensor):
     sensor : object
              A CSM compliant sensor model object
 
+    shape : object
+            A shape model object
+
     Returns
     -------
      : np.ndarray
         pixel resolution in meters/pixel
     """
-    line_res = line_resolution(image_pt, sensor)
-    samp_res = sample_resolution(image_pt, sensor)
+    line_res = line_resolution(image_pt, sensor, shape)
+    samp_res = sample_resolution(image_pt, sensor, shape)
     if (line_res < 0.0):
         return None
     if (samp_res < 0.0):
         return None
-    return (line_res + samp_res) / 2.0
\ No newline at end of file
+    return (line_res + samp_res) / 2.0
+
+
+# def sub_solar_point(image_pt, sensor, illuminator, shape):
+#     sensor_state = csm.get_state(sensor, image_pt)
+
+#     illum_pos = illuminator.get_position_from_time(sensor_state)
+#     lookVec = utils.Point(-illum_pos.x, -illum_pos.y, -illum_pos.z)
+
+#     pt = shape.intersect_surface(illum_pos, lookVec)
+#     return utils.Point(pt.x, pt.y, pt.z)
+
+
+# def local_solar_time(image_pt, sensor, illuminator, shape):
+#     if not isinstance(image_pt, csmapi.ImageCoord):
+#         image_pt = csmapi.ImageCoord(*image_pt)
+
+#     sensor_state = csm.get_state(sensor, image_pt)
+
+#     sub_solar_pt = sub_solar_point(image_pt, sensor, illuminator, shape)
+    
+#     sub_solar_pt_degrees = utils.radians_to_degrees(utils.rect_to_spherical(sub_solar_pt))
+  
+#     ground_pt = shape.intersect_surface(sensor_state["sensorPos"], sensor_state["lookVec"])
+#     spherical_pt = utils.rect_to_spherical(ground_pt)
+
+#     spherical_pt_degrees = utils.radians_to_degrees(spherical_pt)
+
+#     lst = spherical_pt_degrees.lon - sub_solar_pt_degrees.lon + 180.0
+#     lst = lst / 15.0
+#     if (lst < 0.0):
+#         lst += 24.0
+#     if (lst > 24.0):
+#         lst -= 24.0
+#     return lst
+
+# def solar_longitude(image_pt, sensor, illuminator, body):
+#     sensor_state = csm.get_state(sensor, image_pt)
+
+#     illum_pos = illuminator.get_position_from_time(sensor_state)
+#     illum_vel = illuminator.get_velocity(sensor_state)
+
+#     body_rot = body.rotation(sensor_state)
+
+#     sun_av = utils.unit_vector(utils.cross_product(illum_pos, illum_vel))
+    
+#     npole = [body_rot[6], body_rot[7], body_rot[8]]
+
+#     z = sun_av
+#     x = utils.unit_vector(utils.cross_product(npole, z))
+#     y = utils.unit_vector(utils.cross_product(z, x))
+
+#     trans = np.matrix([[x.x, x.y, x.z], [y.x, y.y, y.z], [z.x, z.y, z.z]])
+
+#     pos = np.matmul(trans, illum_pos)
+#     spherical_pos = utils.rect_to_spherical(pos)
+
+#     longitude360 = np.rad2deg(spherical_pos.lon)
+
+#     if (longitude360 != 360.0):
+#       longitude360 -= 360 * np.floor(longitude360 / 360)
+
+#     return longitude360
\ No newline at end of file
diff --git a/knoten/shape.py b/knoten/shape.py
new file mode 100644
index 0000000000000000000000000000000000000000..509e15da1768058955f1c813758bbed561d08b8b
--- /dev/null
+++ b/knoten/shape.py
@@ -0,0 +1,58 @@
+from knoten import csm, utils
+import spiceypy as spice
+import numpy as np
+import csmapi
+
+class Ellipsoid:
+    """
+    A biaxial ellipsoid shape model.
+    """
+
+    def __init__(self, semi_major, semi_minor = None):
+        """
+        Create an ellipsoid shape model from a set of radii
+
+        Parameters
+        ----------
+        semi_major : float
+                     The equatorial semi-major radius of the ellipsoid.
+        semi_minor : float
+                     The polar semi-minor radius of the ellipsoid.
+        """
+        self.a = semi_major
+        self.b = semi_major
+        self.c = semi_major
+
+        if semi_minor is not None:
+            self.c = semi_minor
+
+    @classmethod
+    def from_csm_sensor(cls, sensor):
+        semi_major, semi_minor = csm.get_radii(sensor)
+        return cls(semi_major, semi_minor)
+    
+    def get_surface_normal(self, ground_pt):
+        """
+        Given a ground point, calculate the surface normal.
+
+        Parameters
+        ----------
+        ground_pt: tuple
+                The ground point as an (x, y, z) tuple
+
+        Returns
+        -------
+        : tuple
+        in the form (x, y, z)
+        """
+        normal = spice.surfnm(self.a, self.b, self.c, np.array([ground_pt.x, ground_pt.y, ground_pt.z]))
+        return utils.Point(normal[0], normal[1], normal[2])
+
+
+    def intersect_surface(self, sensor_pos, look_vec):
+        sensor_pos = np.array([sensor_pos.x, sensor_pos.y, sensor_pos.z])
+        look_vec = np.array([look_vec.x, look_vec.y, look_vec.z])
+        
+        ground_pt = spice.surfpt(sensor_pos, look_vec, self.a, self.b, self.c)
+        return csmapi.EcefCoord(ground_pt[0], ground_pt[1], ground_pt[2])
+