# 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.
#
# Unit Test Script
# Module Name: facetedSpacecraftProjectedArea
# Author: Leah Kiner
#
import numpy as np
import pytest
from Basilisk.utilities import SimulationBaseClass
from Basilisk.utilities import macros
from Basilisk.utilities import RigidBodyKinematics as rbk
from Basilisk.simulation import facetedSpacecraftProjectedArea
from Basilisk.architecture import messaging
[docs]
@pytest.mark.parametrize("general_heading, sun_heading, velocity_heading", [ (True, False, False),
(False, True, False),
(False, False, True)])
def test_facetedSpacecraftProjectedArea(show_plots,
general_heading,
sun_heading,
velocity_heading):
r"""
**Verification Test Description**
This unit test verifies that the faceted spacecraft projected area BSK module correctly computes the per-facet
and total projected area of a faceted spacecraft model using a provided heading direction vector. The module
can be configured for three different heading direction types. The first (most direct) option is to provide a
general heading direction using the module ``BodyHeadingMsgPayload`` input message. In this case, the heading vector
must be provided relative to and expressed in the hub body frame B. The second heading configuration option is for
the sun direction heading. In this case, the facet sunlit areas are computed. This option requires two module input
messages (``SpicePlanetStateMsgPayload`` and ``SCStatesMsgPayload``) to be configured. The spice input message
must contain the Sun inertial state information, while the spacecraft state message contains the spacecraft hub
inertial state information. The third heading option is to configure the velocity heading for the exposed area to
drag, which requires setting the module ``SCStatesMsgPayload`` input message. In this case, the heading vector
is set as the direction of the spacecraft hub's inertial velocity vector.
This test sets up a simulation for each of the three heading configuration options. All tests set up the faceted
spacecraft projected area BSK module identically with two different facets. The test checks that the computed
truth values match the values output from the module.
**Test Parameters**
Args:
general_heading (bool): Option 1: Direct general heading test
sun_heading (bool): Option 2: Sun heading for sunlit area calculation test
velocity_heading (bool): Option 3: Spacecraft inertial velocity heading for area exposed to drag test
**Description of Variables Being Tested**
The test checks that the module correctly computes the per-facet and total projected area of a faceted spacecraft
model using a provided heading direction vector. The specific variables checked are the per-facet and total
projected area(s).
"""
task_name = "unitTask"
process_name = "TestProcess"
test_sim = SimulationBaseClass.SimBaseClass()
test_time_step_sec = 1.0 # [s]
test_process_rate = macros.sec2nano(test_time_step_sec)
test_process = test_sim.CreateNewProcess(process_name)
test_process.addTask(test_sim.CreateNewTask(task_name, test_process_rate))
# Facet geometry information
num_facets = 2
facet_area_list = [0.5, 1.0] # [m^2]
facet_r_CopB_B_list = [np.array([-0.1, 0.1, -0.1]),
np.array([0.1, -0.1, -0.1])] # [m]
facet_nHat_B_list = [np.array([1.0, 0.0, 0.0]),
np.array([0.0, 1.0, 0.0])]
facet_rotHat_B_list = [np.array([0.0, 1.0, 0.0]),
np.array([1.0, 0.0, 0.0])]
facet_diffuse_coeff_list = [0.1, 0.1]
facet_specular_coeff_list = [0.9, 0.9]
# Create the facet element input messages
facet_1_message_data = messaging.FacetElementBodyMsgPayload(
area = facet_area_list[0],
r_CopB_B = facet_r_CopB_B_list[0],
nHat_B = facet_nHat_B_list[0],
rotHat_B = facet_rotHat_B_list[0],
c_diffuse = facet_diffuse_coeff_list[0],
c_specular = facet_specular_coeff_list[0],
)
facet_2_message_data = messaging.FacetElementBodyMsgPayload(
area = facet_area_list[1],
r_CopB_B = facet_r_CopB_B_list[1],
nHat_B = facet_nHat_B_list[1],
rotHat_B = facet_rotHat_B_list[1],
c_diffuse = facet_diffuse_coeff_list[1],
c_specular = facet_specular_coeff_list[1],
)
facet_1_message = messaging.FacetElementBodyMsg().write(facet_1_message_data)
facet_2_message = messaging.FacetElementBodyMsg().write(facet_2_message_data)
# Create the faceted spacecraft projected area module
faceted_sc_projected_area = facetedSpacecraftProjectedArea.FacetedSpacecraftProjectedArea()
faceted_sc_projected_area.ModelTag = "facetedSpacecraftProjectedArea"
faceted_sc_projected_area.setNumFacets(num_facets)
faceted_sc_projected_area.facetElementBodyInMsgs[0].subscribeTo(facet_1_message)
faceted_sc_projected_area.facetElementBodyInMsgs[1].subscribeTo(facet_2_message)
test_sim.AddModelToTask(task_name, faceted_sc_projected_area)
rHat_XB_B = []
r_BN_N = []
v_BN_N = []
sigma_BN = []
r_SN_N = []
if general_heading:
rHat_XB_B = np.array([1.0, 0.0, 0.0])
body_heading_message_data = messaging.BodyHeadingMsgPayload()
body_heading_message_data.rHat_XB_B = rHat_XB_B
body_heading_message = messaging.BodyHeadingMsg().write(body_heading_message_data)
faceted_sc_projected_area.bodyHeadingInMsg.subscribeTo(body_heading_message)
elif sun_heading or velocity_heading:
# Create the spacecraft state message
r_BN_N = np.array([-4020338.690396649, 7490566.741852513, 5248299.211589362]) # [m]
v_BN_N = np.array([-5199.77710904224, -3436.681645356935, 1041.576797498721]) # [m/s]
theta_init = 10.0 * macros.D2R # [rad]
rot_axis_N = np.array([1.0, 0.0, 0.0])
prv_BN = theta_init * rot_axis_N
sigma_BN = rbk.PRV2MRP(prv_BN)
spacecraft_state_message_data = messaging.SCStatesMsgPayload()
spacecraft_state_message_data.r_BN_N = r_BN_N # [m]
spacecraft_state_message_data.v_BN_N = v_BN_N # [m/s]
spacecraft_state_message_data.sigma_BN = sigma_BN
spacecraft_state_message = messaging.SCStatesMsg().write(spacecraft_state_message_data)
faceted_sc_projected_area.spacecraftStateInMsg.subscribeTo(spacecraft_state_message)
if sun_heading:
# Create the Sun state ephemeris message
r_SN_N = np.array([1.0, -10.0, 50.0]) # [m]
sun_state_message_data = messaging.SpicePlanetStateMsgPayload()
sun_state_message_data.PositionVector = r_SN_N # [m]
sun_state_message = messaging.SpicePlanetStateMsg().write(sun_state_message_data)
faceted_sc_projected_area.sunStateInMsg.subscribeTo(sun_state_message)
# Set up data logging
total_projected_area_data_log = faceted_sc_projected_area.totalProjectedAreaOutMsg.recorder()
facet_element_projected_area_data_log = []
for outMsg in faceted_sc_projected_area.facetProjectedAreaOutMsgs:
facet_element_projected_area_data_log.append(outMsg.recorder())
test_sim.AddModelToTask(task_name, facet_element_projected_area_data_log[-1])
test_sim.AddModelToTask(task_name, total_projected_area_data_log)
# Execute simulation
test_sim.InitializeSimulation()
sim_time_1 = macros.sec2nano(2.0) # [ns]
test_sim.ConfigureStopTime(sim_time_1)
test_sim.ExecuteSimulation()
# Retrieve the logged data
total_projected_area_sim = total_projected_area_data_log.area # [m^2]
facet_element_projected_area_list_sim = []
for data in facet_element_projected_area_data_log:
facet_element_projected_area_list_sim.append(data.area)
# Compute truth data
(facet_element_projected_area_list_truth,
total_projected_area_truth) = compute_facet_projected_area(general_heading,
sun_heading,
velocity_heading,
num_facets,
rHat_XB_B,
r_BN_N,
v_BN_N,
sigma_BN,
r_SN_N,
facet_area_list,
facet_nHat_B_list)
# Unit test check
accuracy = 1e-12
# Check per-facet projected area
for idx in range(num_facets):
np.testing.assert_allclose(
facet_element_projected_area_list_sim[idx],
facet_element_projected_area_list_truth[idx],
atol=accuracy,
verbose=True
)
# Check total projected area
np.testing.assert_allclose(
total_projected_area_sim,
total_projected_area_truth,
atol=accuracy,
verbose=True
)
def compute_facet_projected_area(general_heading,
sun_heading,
velocity_heading,
num_facets,
rHat_XB_B,
r_BN_N,
v_BN_N,
sigma_BN,
r_SN_N,
facet_area_list,
facet_nHat_B_list):
# Compute the heading vector
heading_hat_B = np.array([0.0, 0.0, 0.0])
if general_heading:
heading_hat_B = rHat_XB_B
if sun_heading or velocity_heading:
dcm_BN = rbk.MRP2C(sigma_BN)
if velocity_heading:
v_BN_B = dcm_BN @ v_BN_N # [m/s]
norm = np.linalg.norm(v_BN_B) # [m/s]
if norm > 1e-12:
heading_hat_B = v_BN_B / norm
else:
r_SB_B = dcm_BN @ (r_SN_N - r_BN_N) # [m]
norm = np.linalg.norm(r_SB_B) # [m]
if norm > 1e-12:
heading_hat_B = r_SB_B / norm
# Compute the total and per-facet projected area
projected_area_list_truth = []
total_projected_area_truth = 0.0
for idx in range(num_facets):
cos_theta = np.dot(facet_nHat_B_list[idx], heading_hat_B)
projected_area = facet_area_list[idx] * max(0.0, cos_theta) # [m^2]
projected_area_list_truth.append(projected_area)
total_projected_area_truth += projected_area
return projected_area_list_truth, total_projected_area_truth
if __name__=="__main__":
test_facetedSpacecraftProjectedArea(
True, # show plots
False, # general_heading
True, # sun_heading
False # velocity_heading
)