diff --git a/examples/sensor_utils.ipynb b/examples/sensor_utils.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..582920f5e5d1fe9b9198397569fdc75cc32af720 --- /dev/null +++ b/examples/sensor_utils.ipynb @@ -0,0 +1,316 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sensor Utils\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from csmapi import csmapi\n", + "from knoten import csm, sensor_utils\n", + "\n", + "import ale\n", + "import json" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a usgscsm sensor model" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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" + ] + } + ], + "source": [ + "fileName = \"data/N1573082850_1.cub\"\n", + "\n", + "kernels = ale.util.generate_kernels_from_cube(fileName, expand=True)\n", + "isd_string = ale.loads(fileName, props={'kernels': kernels})\n", + "csm_isd = os.path.splitext(fileName)[0] + '.json'\n", + "\n", + "with open(csm_isd, 'w') as isd_file:\n", + " isd_file.write(isd_string)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run Sensor Utils with usgscsm sensor model and image point" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "camera = csm.create_csm(csm_isd)\n", + "image_pt = csmapi.ImageCoord(511.5, 511.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "38.87212509629893" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "phaseAngle = sensor_utils.phase_angle(image_pt, camera)\n", + "\n", + "phaseAngle" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "49.60309924896046" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "emissionAngle = sensor_utils.emission_angle(image_pt, camera)\n", + "\n", + "emissionAngle" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2903512972.146144" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "slantDistance = sensor_utils.slant_distance(image_pt, camera)\n", + "\n", + "slantDistance" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2943536048.858226" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "targetCenterDistance = sensor_utils.target_center_distance(image_pt, camera)\n", + "\n", + "targetCenterDistance" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LatLon(lat=3.2229625890973583, lon=258.6197326526089)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "subSpacecraftPoint = sensor_utils.sub_spacecraft_point(image_pt, camera)\n", + "\n", + "subSpacecraftPoint" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "59096282.02424558" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "localRadius = sensor_utils.local_radius(image_pt, camera)\n", + "\n", + "localRadius" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(79.34815579474038, -2.7790780986459485)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rightAscDec = sensor_utils.right_ascension_declination(image_pt, camera)\n", + "\n", + "rightAscDec" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "17397.960941876583" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lineResolution = sensor_utils.line_resolution(image_pt, camera)\n", + "\n", + "lineResolution" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "17397.933700407997" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sampleResolution = sensor_utils.sample_resolution(image_pt, camera)\n", + "\n", + "sampleResolution" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "17397.94732114229" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pixelResolution = sensor_utils.pixel_resolution(image_pt, camera)\n", + "\n", + "pixelResolution" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/knoten/bundle.py b/knoten/bundle.py index d59250afe73e279038ce3ec60fb33562ef001632..9d1389c8b9ac497abf5d8805de7c3f96555667e4 100644 --- a/knoten/bundle.py +++ b/knoten/bundle.py @@ -212,6 +212,35 @@ def compute_ground_partials(sensor, ground_pt): partials = np.array(sensor.computeGroundPartials(csm_ground)) return np.reshape(partials, (2, 3)) +def compute_image_partials(sensor, ground_pt): + """ + Compute the partial derivatives of the ground point with respect to + the line and sample at a ground point. + + These are not normally available from the CSM model, so we use + csm::RasterGM::computeGroundPartials to get the Jacobian of the ground to + image transformation. Then we use the pseudoinverse of that to get the + Jacobian of the image to ground transformation. + + Parameters + ---------- + sensor : CSM sensor + The CSM sensor model + ground_pt : array + The (x, y, z) ground point to compute the partial derivatives W.R.T. + + Returns + ------- + : array + The partial derivatives of the image to ground transformation + """ + if isinstance(ground_pt, csmapi.EcefCoord): + ground_pt = [ground_pt.x, ground_pt.y, ground_pt.z] + ground_matrix = compute_ground_partials(sensor, ground_pt) + image_matrix = np.linalg.pinv(ground_matrix) + + return image_matrix.flatten() + def compute_coefficient_columns(network, sensors, parameters): """ Compute the columns for different coefficients diff --git a/knoten/csm.py b/knoten/csm.py index 9912283d00736e9d645dcc9e2ef22a37d0a1ccb9..0a569d30556e7eecc699121ca82751b1502600db 100644 --- a/knoten/csm.py +++ b/knoten/csm.py @@ -9,6 +9,7 @@ import numpy as np import requests import scipy.stats from functools import singledispatch +import spiceypy as spice from knoten.surface import EllipsoidDem @@ -41,6 +42,28 @@ 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 @@ -73,6 +96,35 @@ def create_camera(label, url='http://pfeffer.wr.usgs.gov/api/1.0/pds/'): if plugin.canModelBeConstructedFromISD(isd, model_name): model = plugin.constructModelFromISD(isd, model_name) return model + +def get_state(sensor, image_pt): + """ + Get the state of the sensor model at a given image point. + + Parameters + ---------- + sensor : object + A CSM compliant sensor model object + + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + Returns + ------- + : dict + Dictionary containing lookVec, sensorPos, sensorTime, and imagePoint + """ + sensor_time = sensor.getImageTime(image_pt) + locus = sensor.imageToRemoteImagingLocus(image_pt) + sensor_position = sensor.getSensorPosition(image_pt) + + sensor_state = { + "lookVec": locus.direction, + "sensorPos": sensor_position, + "sensorTime": sensor_time, + "imagePoint": image_pt + } + return sensor_state def _from_state(state, verbose): with open(state, 'r') as stream: diff --git a/knoten/sensor_utils.py b/knoten/sensor_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..812fecf32804cbab0b408e8bb68821f4611119de --- /dev/null +++ b/knoten/sensor_utils.py @@ -0,0 +1,296 @@ +from knoten import csm, utils, bundle +from csmapi import csmapi +import numpy as np + +def phase_angle(image_pt, sensor): + """ + Computes and returns phase angle, in degrees for a given image point. + + Phase Angle: The angle between the vector from the intersection point to + the observer (usually the spacecraft) and the vector from the intersection + point to the illuminator (usually the sun). + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + phase angle in degrees + """ + if not isinstance(image_pt, csmapi.ImageCoord): + 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) + + sunEcefVec = sensor.getIlluminationDirection(ground_pt) + illum_pos = utils.Point(ground_pt.x - sunEcefVec.x, ground_pt.y - sunEcefVec.y, ground_pt.z - sunEcefVec.z) + + vec_a = utils.Point(sensor_state["sensorPos"].x - ground_pt.x, + sensor_state["sensorPos"].y - ground_pt.y, + sensor_state["sensorPos"].z - ground_pt.z) + + vec_b = utils.Point(illum_pos.x - ground_pt.x, + illum_pos.y - ground_pt.y, + illum_pos.z - ground_pt.z) + + return np.rad2deg(utils.sep_angle(vec_a, vec_b)) + +def emission_angle(image_pt, sensor): + """ + Computes and returns emission angle, in degrees, for a given image point. + + Emission Angle: The angle between the surface normal vector at the + intersection point and the vector from the intersection point to the + observer (usually the spacecraft). The emission angle varies from 0 degrees + when the observer is viewing the sub-spacecraft point (nadir viewing) to 90 + degrees when the intercept is tangent to the surface of the target body. + Thus, higher values of emission angle indicate more oblique viewing of the + target. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + emission angle in degrees + """ + if not isinstance(image_pt, csmapi.ImageCoord): + 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) + + normal = csm.get_surface_normal(sensor, ground_pt) + + sensor_diff = utils.Point(sensor_state["sensorPos"].x - ground_pt.x, + sensor_state["sensorPos"].y - ground_pt.y, + sensor_state["sensorPos"].z - ground_pt.z) + + return np.rad2deg(utils.sep_angle(normal, sensor_diff)) + +def slant_distance(image_pt, sensor): + """ + Computes the slant distance from the sensor to the ground point. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + slant distance in meters + """ + if not isinstance(image_pt, csmapi.ImageCoord): + 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) + + return utils.distance(sensor_state["sensorPos"], ground_pt) + +def target_center_distance(image_pt, sensor): + """ + Calculates and returns the distance from the spacecraft to the target center. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + target center distance in meters + """ + if not isinstance(image_pt, csmapi.ImageCoord): + image_pt = csmapi.ImageCoord(*image_pt) + + sensor_state = csm.get_state(sensor, image_pt) + return utils.distance(sensor_state["sensorPos"], utils.Point(0,0,0)) + +def sub_spacecraft_point(image_pt, sensor): + """ + Get the latitude and longitude of the sub-spacecraft point. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + sub spacecraft point in degrees + """ + if not isinstance(image_pt, csmapi.ImageCoord): + image_pt = csmapi.ImageCoord(*image_pt) + + sensor_state = csm.get_state(sensor, image_pt) + lat_lon_rad = utils.rect_to_spherical(sensor_state["sensorPos"]) + + return utils.radians_to_degrees(lat_lon_rad) + +def local_radius(image_pt, sensor): + """ + Gets the local radius for a given image point. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + local radius in meters + """ + if not isinstance(image_pt, csmapi.ImageCoord): + image_pt = csmapi.ImageCoord(*image_pt) + + ground_pt = csm.generate_ground_point(0.0, image_pt, sensor) + return utils.magnitude(ground_pt) + +def right_ascension_declination(image_pt, sensor): + """ + Computes the right ascension and declination for a given image point. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : tuple + in the form (ra, dec) in degrees + """ + if not isinstance(image_pt, csmapi.ImageCoord): + image_pt = csmapi.ImageCoord(*image_pt) + + sensor_state = csm.get_state(sensor, image_pt) + spherical_pt = utils.rect_to_spherical(sensor_state["lookVec"]) + + ra_dec = utils.radians_to_degrees(spherical_pt) + + return (ra_dec.lon, ra_dec.lat) + +def line_resolution(image_pt, sensor): + """ + Compute the line resolution in meters per pixel for the current set point. + + CSM sensor models do not expose all of the necessary parameters to do the + same calculation as ISIS sensor models, so this uses a more time consuming but + more accurate method and thus is equivalent to the oblique line resolution. + + For time dependent sensor models, this may also be the line-to-line resolution + and not the resolution within a line or framelet. This is determined by the + CSM model's ground computeGroundPartials method. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + line resolution in meters/pixel + """ + if not isinstance(image_pt, csmapi.ImageCoord): + image_pt = csmapi.ImageCoord(*image_pt) + + ground_pt = csm.generate_ground_point(0.0, image_pt, sensor) + 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): + """ + Compute the sample resolution in meters per pixel for the current set point. + + CSM sensor models do not expose all of the necessary parameters to do the + same calculation as ISIS sensor models, so this uses a more time consuming but + more accurate method and thus is equivalent to the oblique sample resolution. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + sample resolution in meters/pixel + """ + if not isinstance(image_pt, csmapi.ImageCoord): + image_pt = csmapi.ImageCoord(*image_pt) + + ground_pt = csm.generate_ground_point(0.0, image_pt, sensor) + 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): + """ + Returns the pixel resolution at the current position in meters/pixel. + + Parameters + ---------- + image_pt : tuple + Pair of x, y (sample, line) coordinates in pixel space + + sensor : object + A CSM compliant sensor model object + + Returns + ------- + : np.ndarray + pixel resolution in meters/pixel + """ + line_res = line_resolution(image_pt, sensor) + samp_res = sample_resolution(image_pt, sensor) + 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 diff --git a/tests/test_bundle.py b/tests/test_bundle.py index 6b95d67fe5c4be7cd01e8131dec88d5d0f4f1e95..eac4c17ac891b406f3f1d08d1c0635a42aef2691 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -121,6 +121,15 @@ def test_compute_ground_partials(): partials = bundle.compute_ground_partials(sensor, ground_pt) np.testing.assert_array_equal(partials, [[1, 2, 3], [4, 5, 6]]) +def test_compute_image_partials(): + ground_pt = [9, 8, 10] + sensor = mock.MagicMock(spec=csmapi.RasterGM) + sensor.computeGroundPartials.return_value = (1, 2, 3, 4, 5, 6) + partials = bundle.compute_image_partials(sensor, ground_pt) + np.testing.assert_allclose(partials, np.array([-0.94444444, 0.44444444, + -0.11111111, 0.11111111, + 0.72222222, -0.22222222])) + def test_compute_jacobian(control_network, sensors): parameters = {sn: [mock.MagicMock()]*2 for sn in sensors} sensor_partials = [(i+1) * np.ones((2, 2)) for i in range(9)] diff --git a/tests/test_csm.py b/tests/test_csm.py index d10320788fca4bb8e7b6985b49eea95e02d6b534..ede5308ccf6b3a3763287a0f0e32847b74ac84dc 100644 --- a/tests/test_csm.py +++ b/tests/test_csm.py @@ -5,7 +5,7 @@ import pytest from plio.io.io_gdal import GeoDataset import csmapi -from knoten import csm, surface +from knoten import csm, surface, utils @pytest.fixture def mock_dem(): @@ -66,4 +66,31 @@ def test__compute_intersection_distance(): pt1 = Point(0,0,0) pt2 = Point(1,1,1) dist = csm._compute_intersection_distance(pt1, pt2) - assert dist == 1 \ No newline at end of file + assert dist == 1 + + +def test_get_state(mock_sensor, pt): + Locus = namedtuple("Locus", 'point direction') + + mock_sensor.getImageTime.return_value = 0.0 + mock_sensor.imageToRemoteImagingLocus.return_value = Locus(utils.Point(0, 1, 2), utils.Point(0, 1, 2)) + mock_sensor.getSensorPosition.return_value = utils.Point(2, 2, 2) + + state = csm.get_state(mock_sensor, pt) + + expected = { + "lookVec": utils.Point(0, 1, 2), + "sensorPos": utils.Point(2, 2, 2), + "sensorTime": 0.0, + "imagePoint": pt + } + + assert state == expected + + +@mock.patch.object(csm, 'get_radii', return_value=(10, 10)) +def test_get_surface_normal(mock_sensor): + ground_pt = utils.Point(1, 0, 0) + normal = csm.get_surface_normal(mock_sensor, ground_pt) + + assert normal == (1.0, 0.0, 0.0) \ No newline at end of file diff --git a/tests/test_sensorutils.py b/tests/test_sensorutils.py new file mode 100644 index 0000000000000000000000000000000000000000..c7be63a93e4a8a05f167cfc3b95750d0c371586e --- /dev/null +++ b/tests/test_sensorutils.py @@ -0,0 +1,103 @@ +from unittest import mock +import pytest + +import numpy as np +import csmapi +from knoten import csm, sensor_utils, utils, bundle + +@pytest.fixture +def mock_sensor(): + mock_sensor = mock.MagicMock(spec=csmapi.RasterGM) + return mock_sensor + +@pytest.fixture +def pt(): + return csmapi.ImageCoord(0.0, 0.0) + +@mock.patch.object(csm, 'get_state', return_value={'sensorPos': utils.Point(100.0, 0.0, 0.0)}) +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(0,0,0)) +def test_phase_angle(mock_sensor, pt): + mock_sensor.getIlluminationDirection.return_value = utils.Point(100.0, 100.0, 0.0) + phase_angle = sensor_utils.phase_angle(pt, mock_sensor) + + np.testing.assert_array_equal(phase_angle, 135.0) + + +@mock.patch.object(csm, 'get_state', return_value={'sensorPos': utils.Point(100.0, 0.0, 0.0)}) +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(0,0,0)) +def test_emission_angle(mock_sensor, pt): + with mock.patch.object(csm, 'get_surface_normal', return_value=utils.Point(-1,0,0)) as mock_normal: + emission_angle = sensor_utils.emission_angle(pt, mock_sensor) + + mock_normal.assert_called() + + np.testing.assert_array_equal(emission_angle, 180.0) + + +@mock.patch.object(csm, 'get_state', return_value={'sensorPos': utils.Point(-100.0, 0.0, 0.0)}) +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(0,0,0)) +def test_slant_distance(mock_sensor, pt): + slant_distance = sensor_utils.slant_distance(pt, mock_sensor) + + np.testing.assert_array_equal(slant_distance, 100.0) + + +@mock.patch.object(csm, 'get_state', return_value={'sensorPos': utils.Point(-100.0, 0.0, 0.0)}) +def test_target_center_distance(mock_sensor, pt): + target_center_distance = sensor_utils.target_center_distance(pt, mock_sensor) + + np.testing.assert_array_equal(target_center_distance, 100.0) + + +@mock.patch.object(csm, 'get_state', return_value={'sensorPos': utils.Point(0.0, 0.0, 100.0)}) +def test_sub_spacecraft_point(mock_sensor, pt): + sub_spacecraft_point = sensor_utils.sub_spacecraft_point(pt, mock_sensor) + + np.testing.assert_array_equal(sub_spacecraft_point, [90.0, 0.0]) + + +@mock.patch.object(csm, 'get_state', return_value={'sensorPos': utils.Point(100.0, 0.0, 0.0)}) +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(10.0, 0.0, 0.0)) +def test_local_radius_intersection(mock_sensor, pt): + local_radius = sensor_utils.local_radius(pt, mock_sensor) + + np.testing.assert_array_equal(local_radius, 10.0) + + +@mock.patch.object(csm, 'get_state', return_value={'sensorPos': utils.Point(-1000.0, 0.0, 0.0)}) +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(10.0, 0.0, 0.0)) +def test_local_radius_ground(mock_sensor, pt): + local_radius = sensor_utils.local_radius(pt, mock_sensor) + + np.testing.assert_array_equal(local_radius, 10.0) + + +@mock.patch.object(csm, 'get_state', return_value={'lookVec': utils.Point(-1.0, 0.0, 0.0)}) +def test_right_ascension_declination(mock_sensor, pt): + right_ascension_declination = sensor_utils.right_ascension_declination(pt, mock_sensor) + + np.testing.assert_array_equal(right_ascension_declination, [180.0, 0.0]) + + +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(0.0, 0.0, 0.0)) +@mock.patch.object(bundle, 'compute_image_partials', return_value=np.array([2, 1, 4, 4, 4, 8])) +def test_line_resolution(mock_sensor, pt): + line_resolution = sensor_utils.line_resolution(pt, mock_sensor) + + np.testing.assert_array_equal(line_resolution, 6.0) + + +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(0.0, 0.0, 0.0)) +@mock.patch.object(bundle, 'compute_image_partials', return_value=np.array([2, 1, 4, 4, 4, 8])) +def test_sample_resolution(mock_sensor, pt): + sample_resolution = sensor_utils.sample_resolution(pt, mock_sensor) + + np.testing.assert_array_equal(sample_resolution, 9.0) + + +@mock.patch.object(csm, 'generate_ground_point', return_value=utils.Point(0.0, 0.0, 0.0)) +@mock.patch.object(bundle, 'compute_image_partials', return_value=np.array([2, 1, 4, 4, 4, 8])) +def test_pixel_resolution(mock_sensor, pt): + pixel_resolution = sensor_utils.pixel_resolution(pt, mock_sensor) + + np.testing.assert_array_equal(pixel_resolution, 7.5) \ No newline at end of file