Source code for test_unitStripLocation

# ISC License
#
# Copyright (c) 2026, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import inspect
import os

import numpy as np
import pytest
from Basilisk import __path__
from Basilisk.architecture import messaging
from Basilisk.simulation import stripLocation
from Basilisk.simulation import spacecraft
from Basilisk.utilities import RigidBodyKinematics as rbk
from Basilisk.utilities import SimulationBaseClass
from Basilisk.utilities import macros
from Basilisk.utilities import orbitalMotion
from Basilisk.utilities import simIncludeGravBody
from Basilisk.utilities import unitTestSupport

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
bskName = 'Basilisk'
splitPath = path.split(bskName)
bskPath = __path__[0]


[docs] def test_range_degenerate_strip(show_plots): """ Tests stripLocation with a degenerate strip (start == end): 1. Computes range correctly by evaluating slantRange; 2. Tests whether elevation is correctly evaluated; 3. Tests whether range limits impact access; 4. Tests whether multiple spacecraft are supported in parallel. This is the stripLocation equivalent of the groundLocation test_range test, verifying that a strip with identical start and end points behaves like a single ground location. :return: """ simTaskName = "simTask" simProcessName = "simProcess" scSim = SimulationBaseClass.SimBaseClass() dynProcess = scSim.CreateNewProcess(simProcessName) simulationTime = macros.sec2nano(10.) simulationTimeStep = macros.sec2nano(1.) dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep)) # Create a strip target with start == end stripTarget = stripLocation.StripLocation() stripTarget.ModelTag = "stripTarget" stripTarget.planetRadius = orbitalMotion.REQ_EARTH * 1000. stripTarget.maximumRange = 100e3 # meters stripTarget.minimumElevation = np.radians(80.) stripTarget.specifyLocationStart(np.radians(0.), np.radians(0.), 0.) stripTarget.specifyLocationEnd(np.radians(0.), np.radians(0.), 0.) stripTarget.acquisitionSpeed = 0 # stationary scSim.AddModelToTask(simTaskName, stripTarget) # Write out mock spacecraft position messages sc1_message = messaging.SCStatesMsgPayload() sc1_message.r_BN_N = [orbitalMotion.REQ_EARTH * 1e3 + 100e3, 0, 0] # SC1 directly overhead, in range sc1Msg = messaging.SCStatesMsg().write(sc1_message) sc2_message = messaging.SCStatesMsgPayload() sc2_message.r_BN_N = [orbitalMotion.REQ_EARTH * 1e3 + 101e3, 0, 0] # SC2 out of range sc2Msg = messaging.SCStatesMsg().write(sc2_message) sc3_message = messaging.SCStatesMsgPayload() # SC3 is inside the altitude limit, but outside the visibility cone sc3_message.r_BN_N = rbk.euler3(np.radians(11.)).dot(np.array([100e3, 0, 0])) + np.array( [orbitalMotion.REQ_EARTH * 1e3, 0, 0]) sc3Msg = messaging.SCStatesMsg().write(sc3_message) stripTarget.addSpacecraftToModel(sc1Msg) stripTarget.addSpacecraftToModel(sc2Msg) stripTarget.addSpacecraftToModel(sc3Msg) # Log the access indicator numDataPoints = 2 samplingTime = unitTestSupport.samplingTime(simulationTime, simulationTimeStep, numDataPoints) dataLog0 = stripTarget.accessOutMsgs[0].recorder(samplingTime) dataLog1 = stripTarget.accessOutMsgs[1].recorder(samplingTime) dataLog2 = stripTarget.accessOutMsgs[2].recorder(samplingTime) scSim.AddModelToTask(simTaskName, dataLog0) scSim.AddModelToTask(simTaskName, dataLog1) scSim.AddModelToTask(simTaskName, dataLog2) # Run the sim scSim.InitializeSimulation() scSim.ConfigureStopTime(simulationTime) scSim.ExecuteSimulation() # Get the logged data sc1_access = dataLog0.hasAccess sc1_slant = dataLog0.slantRange sc1_elevation = dataLog0.elevation sc2_access = dataLog1.hasAccess sc2_slant = dataLog1.slantRange sc2_elevation = dataLog1.elevation sc3_access = dataLog2.hasAccess sc3_slant = dataLog2.slantRange sc3_elevation = dataLog2.elevation # Compare to expected values accuracy = 1e-8 ref_ranges = [100e3, 101e3, 100e3] ref_elevation = [np.radians(90.), np.radians(90.), np.radians(79.)] ref_access = [1, 0, 0] test_ranges = [sc1_slant[1], sc2_slant[1], sc3_slant[1]] test_elevation = [sc1_elevation[1], sc2_elevation[1], sc3_elevation[1]] test_access = [sc1_access[1], sc2_access[1], sc3_access[1]] range_worked = test_ranges == pytest.approx(ref_ranges, accuracy) elevation_worked = test_elevation == pytest.approx(ref_elevation, accuracy) access_worked = test_access == pytest.approx(ref_access, abs=1e-16) assert range_worked, f"Range check failed: {test_ranges} vs {ref_ranges}" assert elevation_worked, f"Elevation check failed: {test_elevation} vs {ref_elevation}" assert access_worked, f"Access check failed: {test_access} vs {ref_access}"
[docs] def test_rotation_degenerate_strip(show_plots): """ Tests whether stripLocation correctly accounts for planet rotation: 1. Computes the current strip location based on the initial position and the rotation state of the planet it is attached to. A strip at (0, 10deg) with the planet rotated by -10deg around z should appear at (0, 0) in inertial space, directly beneath a spacecraft on the x-axis. :return: """ simTime = 1. simTaskName = "simTask" simProcessName = "simProcess" scSim = SimulationBaseClass.SimBaseClass() dynProcess = scSim.CreateNewProcess(simProcessName) simulationTime = macros.sec2nano(simTime) simulationTimeStep = macros.sec2nano(1.) dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep)) # Create a degenerate strip at (0, 10deg) stripTarget = stripLocation.StripLocation() stripTarget.ModelTag = "stripTarget" stripTarget.planetRadius = orbitalMotion.REQ_EARTH * 1000. stripTarget.maximumRange = 200e3 # meters stripTarget.minimumElevation = np.radians(10.) stripTarget.specifyLocationStart(np.radians(0.), np.radians(10.), 0.) stripTarget.specifyLocationEnd(np.radians(0.), np.radians(10.), 0.) stripTarget.acquisitionSpeed = 0 scSim.AddModelToTask(simTaskName, stripTarget) # Write out mock planet rotation and spacecraft position messages sc1_message = messaging.SCStatesMsgPayload() sc1_message.r_BN_N = np.array([orbitalMotion.REQ_EARTH * 1e3 + 90e3, 0, 0]) scMsg = messaging.SCStatesMsg().write(sc1_message) stripTarget.addSpacecraftToModel(scMsg) # Rotate planet by -10 deg so the (0, 10deg) ground point aligns with inertial x-axis planet_message = messaging.SpicePlanetStateMsgPayload() planet_message.J20002Pfix = rbk.euler3(np.radians(-10.)).tolist() planetMsg = messaging.SpicePlanetStateMsg().write(planet_message) stripTarget.planetInMsg.subscribeTo(planetMsg) # Log the access indicator numDataPoints = 2 samplingTime = unitTestSupport.samplingTime(simulationTime, simulationTimeStep, numDataPoints) dataLog = stripTarget.accessOutMsgs[0].recorder(samplingTime) scSim.AddModelToTask(simTaskName, dataLog) # Run the sim scSim.InitializeSimulation() scSim.ConfigureStopTime(simulationTime) scSim.ExecuteSimulation() # Get the logged data sc1_access = dataLog.hasAccess sc1_slant = dataLog.slantRange sc1_elevation = dataLog.elevation # Compare to expected values accuracy = 1e-8 ref_ranges = [90e3] ref_elevation = [np.radians(90.)] ref_access = [1] test_ranges = [sc1_slant[1]] test_elevation = [sc1_elevation[1]] test_access = [sc1_access[1]] range_worked = test_ranges == pytest.approx(ref_ranges, accuracy) elevation_worked = test_elevation == pytest.approx(ref_elevation, accuracy) access_worked = test_access == pytest.approx(ref_access, abs=1e-16) assert range_worked, f"Range check failed: {test_ranges} vs {ref_ranges}" assert elevation_worked, f"Elevation check failed: {test_elevation} vs {ref_elevation}" assert access_worked, f"Access check failed: {test_access} vs {ref_access}"
[docs] def test_strip_target_motion(show_plots): """ Tests that the strip target position moves along the great-circle arc from start to end as time progresses (No pre-imaging is considered in this test): 1. Verifies that the target position at the midpoint of the traversal matches the expected SLERP interpolation on the sphere. 2. Verifies that the target starts at the start point and finishes at the end point. 3. Verifies that a spacecraft placed above the midpoint sees ~90deg elevation when the target arrives there. :return: """ simTaskName = "simTask" simProcessName = "simProcess" scSim = SimulationBaseClass.SimBaseClass() dynProcess = scSim.CreateNewProcess(simProcessName) dt = 1.0 simulationTimeStep = macros.sec2nano(dt) totalTimeSec = 10.0 simulationTime = macros.sec2nano(totalTimeSec) dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep)) R = orbitalMotion.REQ_EARTH * 1000. # planet radius in meters # Create a strip from (lat=0, lon=0) to (lat=0, lon=90deg) on the equator stripTarget = stripLocation.StripLocation() stripTarget.ModelTag = "stripTarget" stripTarget.planetRadius = R stripTarget.maximumRange = -1 # unlimited range stripTarget.minimumElevation = np.radians(10.) stripTarget.specifyLocationStart(np.radians(0.), np.radians(0.), 0.) stripTarget.specifyLocationEnd(np.radians(0.), np.radians(90.), 0.) # Set acquisition speed so traversal takes exactly totalTimeSec seconds strip_length = (np.pi / 2.0) * R # 90-degree arc length acquisitionSpeed = strip_length / macros.sec2nano(totalTimeSec) # m/ns stripTarget.acquisitionSpeed = acquisitionSpeed stripTarget.preImagingTime = 0 scSim.AddModelToTask(simTaskName, stripTarget) # Place SC above the midpoint (lon=45deg on the equator) midpoint_dir = np.array([np.cos(np.radians(45.)), np.sin(np.radians(45.)), 0.]) sc_altitude = 200e3 # meters sc1_message = messaging.SCStatesMsgPayload() sc1_message.r_BN_N = (R + sc_altitude) * midpoint_dir sc1Msg = messaging.SCStatesMsg().write(sc1_message) stripTarget.addSpacecraftToModel(sc1Msg) # Log strip state and access at every simulation step stateLog = stripTarget.currentStripStateOutMsg.recorder(simulationTimeStep) accessLog = stripTarget.accessOutMsgs[0].recorder(simulationTimeStep) scSim.AddModelToTask(simTaskName, stateLog) scSim.AddModelToTask(simTaskName, accessLog) # Run the sim scSim.InitializeSimulation() scSim.ConfigureStopTime(simulationTime) scSim.ExecuteSimulation() # --- Check target positions at key time points --- r_LP_N_logged = stateLog.r_LP_N pos_accuracy = 1.0 # 1 meter tolerance # At t=0s (index 0): target should be at start (lon=0) -> PCPF = [R, 0, 0] expected_start = np.array([R, 0., 0.]) start_error = np.linalg.norm(r_LP_N_logged[0] - expected_start) assert start_error < pos_accuracy, \ f"Target position error at start: {start_error:.6f} m (expected < {pos_accuracy} m)" # At t=5s (index 5): target should be at midpoint (lon=45) -> [R/sqrt(2), R/sqrt(2), 0] expected_mid = R * midpoint_dir mid_error = np.linalg.norm(r_LP_N_logged[5] - expected_mid) assert mid_error < pos_accuracy, \ f"Target position error at midpoint: {mid_error:.6f} m (expected < {pos_accuracy} m)" # At t=10s (index 10): target should be at end (lon=90) -> [0, R, 0] expected_end = np.array([0., R, 0.]) end_error = np.linalg.norm(r_LP_N_logged[10] - expected_end) assert end_error < pos_accuracy, \ f"Target position error at end: {end_error:.6f} m (expected < {pos_accuracy} m)" # At midpoint, SC should be directly overhead -> elevation ~ 90 deg elevation_at_midpoint = accessLog.elevation[5] assert elevation_at_midpoint == pytest.approx(np.radians(90.), abs=0.01), \ f"Elevation at midpoint should be ~90 deg, got {np.degrees(elevation_at_midpoint):.2f} deg" # SC above the midpoint should have access when the target is at the midpoint assert accessLog.hasAccess[5] == 1, "SC should have access when target is at midpoint"
[docs] def test_pre_imaging_blocks_access(show_plots): """ Tests that access is blocked during the pre-imaging phase: 1. A strip target with preImagingTime > 0 should deny access during the initial pre-imaging period, even if the spacecraft is geometrically visible. 2. After the pre-imaging time has elapsed, access should be granted if the elevation/range criteria are met. :return: """ simTaskName = "simTask" simProcessName = "simProcess" scSim = SimulationBaseClass.SimBaseClass() dynProcess = scSim.CreateNewProcess(simProcessName) dt = 1.0 simulationTimeStep = macros.sec2nano(dt) totalTimeSec = 10.0 simulationTime = macros.sec2nano(totalTimeSec) dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep)) R = orbitalMotion.REQ_EARTH * 1000. # Create a stationary strip (start == end) with non-zero preImagingTime stripTarget = stripLocation.StripLocation() stripTarget.ModelTag = "stripTarget" stripTarget.planetRadius = R stripTarget.maximumRange = -1 # unlimited stripTarget.minimumElevation = np.radians(10.) stripTarget.specifyLocationStart(np.radians(0.), np.radians(0.), 0.) stripTarget.specifyLocationEnd(np.radians(0.), np.radians(0.), 0.) stripTarget.acquisitionSpeed = 0 # stationary stripTarget.preImagingTime = 5.0e9 # 5 seconds in nanoseconds scSim.AddModelToTask(simTaskName, stripTarget) # Place SC directly overhead sc1_message = messaging.SCStatesMsgPayload() sc1_message.r_BN_N = [R + 100e3, 0, 0] sc1Msg = messaging.SCStatesMsg().write(sc1_message) stripTarget.addSpacecraftToModel(sc1Msg) # Log access at every step accessLog = stripTarget.accessOutMsgs[0].recorder(simulationTimeStep) scSim.AddModelToTask(simTaskName, accessLog) # Run the sim scSim.InitializeSimulation() scSim.ConfigureStopTime(simulationTime) scSim.ExecuteSimulation() access = accessLog.hasAccess # During pre-imaging (t=0s to t=4s): duration < preImagingTime -> no access # At t=0: duration_strip_imaging = 0 < 5e9 -> access = 0 # At t=4: duration_strip_imaging = 4e9 < 5e9 -> access = 0 for i in range(5): # indices 0..4 correspond to t=0s..t=4s assert access[i] == 0, \ f"Access should be blocked at t={i}s (pre-imaging), got {access[i]}" # After pre-imaging (t >= 5s): duration >= preImagingTime -> access granted # At t=5: duration_strip_imaging = 5e9 >= 5e9 -> access = 1 for i in range(5, 11): # indices 5..10 correspond to t=5s..t=10s assert access[i] == 1, \ f"Access should be granted at t={i}s (post pre-imaging), got {access[i]}"
[docs] def test_strip_velocity_output(show_plots): """ Tests that the strip state output message correctly reports the velocity of the moving target along the strip: 1. For a non-degenerate strip, velocity magnitude should equal the acquisition speed. 2. The velocity vector should be perpendicular to the position vector (tangent to the sphere). 3. The velocity vector should lie in the great-circle plane defined by the strip start and end points (i.e. perpendicular to the orbit normal). 4. The velocity vector should be oriented in the direction of motion (from start toward end), verified by checking that r x v is parallel to the orbit normal. :return: """ simTaskName = "simTask" simProcessName = "simProcess" scSim = SimulationBaseClass.SimBaseClass() dynProcess = scSim.CreateNewProcess(simProcessName) dt = 1.0 simulationTimeStep = macros.sec2nano(dt) totalTimeSec = 10.0 simulationTime = macros.sec2nano(totalTimeSec) dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep)) R = orbitalMotion.REQ_EARTH * 1000. # Create a moving strip from (lat=0, lon=0) to (lat=0, lon=90) stripTarget = stripLocation.StripLocation() stripTarget.ModelTag = "stripTarget" stripTarget.planetRadius = R stripTarget.maximumRange = -1 stripTarget.minimumElevation = np.radians(10.) stripTarget.specifyLocationStart(np.radians(0.), np.radians(0.), 0.) stripTarget.specifyLocationEnd(np.radians(0.), np.radians(90.), 0.) strip_length = (np.pi / 2.0) * R acquisitionSpeed = strip_length / macros.sec2nano(totalTimeSec) # m/ns stripTarget.acquisitionSpeed = acquisitionSpeed stripTarget.preImagingTime = 0 scSim.AddModelToTask(simTaskName, stripTarget) # Need at least one spacecraft for the module to function sc1_message = messaging.SCStatesMsgPayload() sc1_message.r_BN_N = [R + 200e3, 0, 0] sc1Msg = messaging.SCStatesMsg().write(sc1_message) stripTarget.addSpacecraftToModel(sc1Msg) # Log the strip state stateLog = stripTarget.currentStripStateOutMsg.recorder(simulationTimeStep) scSim.AddModelToTask(simTaskName, stateLog) # Run the sim scSim.InitializeSimulation() scSim.ConfigureStopTime(simulationTime) scSim.ExecuteSimulation() v_LP_N_logged = stateLog.v_LP_N r_LP_N_logged = stateLog.r_LP_N # Compute the orbit-plane normal from the strip endpoints # Start: (lat=0, lon=0) -> R * [1, 0, 0] # End: (lat=0, lon=90) -> R * [0, 1, 0] r_start = R * np.array([1.0, 0.0, 0.0]) r_end = R * np.array([0.0, 1.0, 0.0]) orbit_normal = np.cross(r_start, r_end) orbit_normal = orbit_normal / np.linalg.norm(orbit_normal) # [0, 0, 1] # Check at several time indices (skip index 0 which is before motion starts) for idx in [1, 3, 5, 7, 9]: v = v_LP_N_logged[idx] r = r_LP_N_logged[idx] v_mag = np.linalg.norm(v) r_mag = np.linalg.norm(r) # 1. Velocity magnitude should equal acquisitionSpeed (in m/ns), convert to m/s for comparison assert v_mag == pytest.approx(acquisitionSpeed * 1e9, rel=1e-6), \ f"[idx={idx}] Velocity magnitude {v_mag} doesn't match acquisition speed {acquisitionSpeed * 1e9} (m/s)" # 2. Velocity should be perpendicular to the position vector dot_rv = np.dot(r, v) / (r_mag * v_mag) assert abs(dot_rv) < 1e-6, \ f"[idx={idx}] Velocity should be perpendicular to position, dot product = {dot_rv}" # 3. Velocity should lie in the great-circle (orbit) plane: # v . n_orbit ≈ 0 dot_vn = np.dot(v, orbit_normal) / v_mag assert abs(dot_vn) < 1e-6, \ f"[idx={idx}] Velocity should lie in the orbit plane, v · n = {dot_vn}" # 4. Velocity should be oriented in the direction of motion: # r x v should be parallel to orbit_normal (same sign) rxv = np.cross(r, v) rxv_hat = rxv / np.linalg.norm(rxv) dot_direction = np.dot(rxv_hat, orbit_normal) assert dot_direction > 0.99, \ f"[idx={idx}] Velocity should point in direction of motion (start->end), " \ f"(r x v) · n = {dot_direction}"
[docs] def test_new_start_pre_imaging(show_plots): """ Tests that ``newpstart()`` correctly extends the strip backward so that the target reaches the *original* start point after exactly ``preImagingTime`` seconds of travel: 1. Verifies that at t=0 the target position equals the extended (new) start point, which sits ``preImagingTime * speed`` arc-length behind the original start along the great circle. 2. Verifies that at t=preImagingTime the target arrives at the original start point (lat=0, lon=0). 3. Verifies that at the end of the updated traversal the target reaches the original end point (lat=0, lon=90°). :return: """ simTaskName = "simTask" simProcessName = "simProcess" scSim = SimulationBaseClass.SimBaseClass() dynProcess = scSim.CreateNewProcess(simProcessName) dt = 1.0 simulationTimeStep = macros.sec2nano(dt) dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep)) R = orbitalMotion.REQ_EARTH * 1000. # planet radius in metres # ---- strip geometry ---- # Original strip along the equator from lon=0 to lon=90 deg lat_start, lon_start = 0., 0. lat_end, lon_end = 0., 90. theta_orig = np.radians(lon_end - lon_start) # 90 deg arc strip_length_orig = theta_orig * R # original arc length # Speeds chosen so the *original* strip takes 10 s to traverse totalOriginalTimeSec = 10.0 acquisitionSpeed = strip_length_orig / macros.sec2nano(totalOriginalTimeSec) # m / ns # Pre-imaging time = 2 s ⇒ target is extended 2 s of travel behind start pre_imaging_sec = 2.0 pre_imaging_ns = macros.sec2nano(pre_imaging_sec) # Expected arc-length behind the original start arc_behind = acquisitionSpeed * pre_imaging_ns # metres angle_behind = arc_behind / R # radians (≈ 18 deg) # Expected new start position: lon = -angle_behind on the equator expected_new_start = R * np.array([np.cos(-angle_behind), np.sin(-angle_behind), 0.]) # Expected time to traverse the *updated* strip # Updated arc covers (theta_orig + angle_behind) radians theta_updated = theta_orig + angle_behind totalUpdatedTimeSec = (theta_updated * R) / (acquisitionSpeed * 1e9) # seconds # Run sim long enough to cover the extended strip simulationTime = macros.sec2nano(np.ceil(totalUpdatedTimeSec)) # ---- module setup ---- stripTarget = stripLocation.StripLocation() stripTarget.ModelTag = "stripTargetNewStart" stripTarget.planetRadius = R stripTarget.maximumRange = -1 stripTarget.minimumElevation = np.radians(10.) stripTarget.specifyLocationStart(np.radians(lat_start), np.radians(lon_start), 0.) stripTarget.specifyLocationEnd(np.radians(lat_end), np.radians(lon_end), 0.) stripTarget.acquisitionSpeed = acquisitionSpeed stripTarget.preImagingTime = pre_imaging_ns scSim.AddModelToTask(simTaskName, stripTarget) # A spacecraft is needed to drive the module (placed arbitrarily) sc_msg = messaging.SCStatesMsgPayload() sc_msg.r_BN_N = [(R + 200e3), 0, 0] scMsg = messaging.SCStatesMsg().write(sc_msg) stripTarget.addSpacecraftToModel(scMsg) # Log strip state at every step stateLog = stripTarget.currentStripStateOutMsg.recorder(simulationTimeStep) scSim.AddModelToTask(simTaskName, stateLog) # ---- run ---- scSim.InitializeSimulation() scSim.ConfigureStopTime(simulationTime) scSim.ExecuteSimulation() # ---- assertions ---- r_LP_N_logged = stateLog.r_LP_N pos_tol = 1.0 # 1 metre tolerance # 1) At t = 0 s (index 0): target should be at the *new* start (lon ≈ -18 deg) new_start_error = np.linalg.norm(r_LP_N_logged[0] - expected_new_start) assert new_start_error < pos_tol, \ f"New start position error: {new_start_error:.4f} m (tol {pos_tol} m). " \ f"Expected lon ≈ {np.degrees(-angle_behind):.2f} deg" # 2) At t = pre_imaging_sec (index = pre_imaging_sec / dt): target should be # at the *original* start (lat=0, lon=0) -> PCPF [R, 0, 0] idx_orig_start = int(pre_imaging_sec / dt) expected_orig_start = np.array([R, 0., 0.]) orig_start_error = np.linalg.norm(r_LP_N_logged[idx_orig_start] - expected_orig_start) assert orig_start_error < pos_tol, \ f"Target should be at original start at t={pre_imaging_sec}s, " \ f"error = {orig_start_error:.4f} m (tol {pos_tol} m)" # 3) At the final step the target should be at the original end (lat=0, lon=90 deg) # -> PCPF [0, R, 0] idx_end = int(np.ceil(totalUpdatedTimeSec / dt)) expected_end = np.array([0., R, 0.]) end_error = np.linalg.norm(r_LP_N_logged[idx_end] - expected_end) assert end_error < pos_tol, \ f"Target should be at end at t={totalUpdatedTimeSec:.1f}s, " \ f"error = {end_error:.4f} m (tol {pos_tol} m)" # 4) Verify the angular offset between new start and original start ≈ angle_behind new_start_pos = r_LP_N_logged[0] cos_angle_actual = np.dot(new_start_pos, expected_orig_start) / ( np.linalg.norm(new_start_pos) * np.linalg.norm(expected_orig_start)) angle_actual = np.arccos(np.clip(cos_angle_actual, -1.0, 1.0)) assert angle_actual == pytest.approx(angle_behind, abs=1e-6), \ f"Angle between new start and original start: {np.degrees(angle_actual):.4f} deg, " \ f"expected {np.degrees(angle_behind):.4f} deg"
[docs] def test_AzElR_rates(): """ Tests that the Az, El, range rates are correct by using 1-step Euler integration. Uses a stationary strip (start == end at Boulder, CO) to test rate computation, analogous to the groundLocation Az/El/R rate test. :return: """ simTaskName = "simTask" simProcessName = "simProcess" scSim = SimulationBaseClass.SimBaseClass() dynProcess = scSim.CreateNewProcess(simProcessName) dt = 1.0 simulationTimeStep = macros.sec2nano(dt) simulationTime = simulationTimeStep dynProcess.addTask(scSim.CreateNewTask(simTaskName, simulationTimeStep)) gravFactory = simIncludeGravBody.gravBodyFactory() planet = gravFactory.createEarth() mu = planet.mu planet.isCentralBody = True timeInitString = '2021 MAY 04 06:47:48.965 (UTC)' gravFactory.createSpiceInterface(bskPath + '/supportData/EphemerisData/', timeInitString) gravFactory.spiceObject.zeroBase = 'Earth' scSim.AddModelToTask(simTaskName, gravFactory.spiceObject, -1) scObject = spacecraft.Spacecraft() scObject.gravField.gravBodies = spacecraft.GravBodyVector(list(gravFactory.gravBodies.values())) scSim.AddModelToTask(simTaskName, scObject) oe = orbitalMotion.ClassicElements() r_sc = planet.radEquator + 100 * 1E3 oe.a = r_sc oe.e = 0.00001 oe.i = 53.0 * macros.D2R oe.Omega = 115.0 * macros.D2R oe.omega = 5.0 * macros.D2R oe.f = 75. * macros.D2R rN, vN = orbitalMotion.elem2rv(mu, oe) scObject.hub.r_CN_NInit = rN scObject.hub.v_CN_NInit = vN # Use a stationary strip (start == end) at Boulder, CO stripStation = stripLocation.StripLocation() stripStation.planetRadius = planet.radEquator stripStation.specifyLocationStart(np.radians(40.009971), np.radians(-105.243895), 1624) stripStation.specifyLocationEnd(np.radians(40.009971), np.radians(-105.243895), 1624) stripStation.acquisitionSpeed = 0 stripStation.planetInMsg.subscribeTo(gravFactory.spiceObject.planetStateOutMsgs[0]) stripStation.minimumElevation = np.radians(60.) stripStation.addSpacecraftToModel(scObject.scStateOutMsg) scSim.AddModelToTask(simTaskName, stripStation) # Log the Az, El, R and rates info numDataPoints = 2 samplingTime = unitTestSupport.samplingTime(simulationTime, simulationTimeStep, numDataPoints) dataLog = stripStation.accessOutMsgs[0].recorder(samplingTime) scSim.AddModelToTask(simTaskName, dataLog) # Run the sim scSim.InitializeSimulation() scSim.ConfigureStopTime(simulationTime) scSim.ExecuteSimulation() # Get logged data sc_range = dataLog.slantRange sc_elevation = dataLog.elevation sc_azimuth = dataLog.azimuth sc_range_rate = dataLog.range_dot sc_el_rate = dataLog.el_dot sc_az_rate = dataLog.az_dot # Euler integration: value(t+dt) ≈ value(t) + rate(t)*dt sc_Euler_range = sc_range[0] + sc_range_rate[0] * dt sc_Euler_elev = sc_elevation[0] + sc_el_rate[0] * dt sc_Euler_azimuth = sc_azimuth[0] + sc_az_rate[0] * dt range_rate_worked = sc_range[1] == pytest.approx(sc_Euler_range, rel=1e-5) el_rate_worked = sc_elevation[1] == pytest.approx(sc_Euler_elev, rel=1e-5) az_rate_worked = sc_azimuth[1] == pytest.approx(sc_Euler_azimuth, rel=1e-5) assert range_rate_worked, "Range rate check failed" assert el_rate_worked, "Elevation rate check failed" assert az_rate_worked, "Azimuth rate check failed"
if __name__ == '__main__': test_range_degenerate_strip(False) test_rotation_degenerate_strip(False) test_strip_target_motion(False) test_pre_imaging_blocks_access(False) test_strip_velocity_output(False) test_new_start_pre_imaging(False) test_AzElR_rates()