Source code for test_facetedSpacecraftModel


# 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:        facetedSpacecraftModel
#   Author:             Leah Kiner
#   Creation Date:      Feb 10 2026
#   Last Updated:       Feb 10 2026
#

import numpy as np
import os
import pytest
from Basilisk import __path__

bskPath = __path__[0]
fileName = os.path.basename(os.path.splitext(__file__)[0])

from Basilisk.utilities import SimulationBaseClass
from Basilisk.utilities import macros
from Basilisk.utilities import RigidBodyKinematics as rbk
from Basilisk.simulation import facetedSpacecraftModel
from Basilisk.architecture import messaging


[docs] @pytest.mark.parametrize("articulated_facet_1_initial_angle", [macros.D2R * 0.0, macros.D2R * 10.0, macros.D2R * 75.0, macros.D2R * -90.0]) @pytest.mark.parametrize("articulated_facet_1_intermediate_angle", [macros.D2R * -33.3, macros.D2R * 45.2, macros.D2R * 90.0, macros.D2R * 180.0]) @pytest.mark.parametrize("articulated_facet_1_final_angle", [macros.D2R * 0.0, macros.D2R * -5.4, macros.D2R * 22.5]) @pytest.mark.parametrize("fixed_facet_2_initial_angle", [macros.D2R * 0.0, macros.D2R * 10.0, macros.D2R * 75.0, macros.D2R * -90.0]) def test_facetedSpacecraftModel(show_plots, articulated_facet_1_initial_angle, articulated_facet_1_intermediate_angle, articulated_facet_1_final_angle, fixed_facet_2_initial_angle): r""" **Verification Test Description** This unit test verifies that the faceted spacecraft BSK module correctly transforms facet geometry data from the local facet frames to the spacecraft hub body frame. The module is configured to map either fixed or single-axis articulating facets to the spacecraft hub frame. The facet geometry input data to the BSK module is provided in each local facet frame. The facet geometry data is transformed and output from the BSK module in the spacecraft hub body frame. Articulating facets must be configured by calling the method addArticulatedFacet(Message<HingedRigidBodyMsgPayload> \*tmpMsg) with a HingedRigidBodyMsgPayload message for each articulating facet. The module assumes the articulating facets are added first to the module, followed by any fixed facets. This test sets up the faceted spacecraft BSK module with two facets. The first facet articulates while the second is fixed. A stand-alone HingedRigidBodyMsgPayload is provided for the articulated facet. To ensure the module correctly handles articulating facets, the simulation is broken up into two segments. The first segment provides an intermediate articulation angle while the second segment provides a final articulation angle. The test varies the articulated facet initial angle, intermediate angle, and final angle. The initial angle for the initial facet is also varied in the test. **Test Parameters** Args: articulated_facet_1_initial_angle (float): [rad] Initial articulated facet 1 angle articulated_facet_1_intermediate_angle (float): [rad] Intermediate articulated facet 1 angle articulated_facet_1_final_angle (float): [rad] Final articulated facet 1 angle fixed_facet_2_initial_angle (float): [rad] Initial fixed facet 2 angle **Description of Variables Being Tested** The test checks that the facet geometry data is correctly transformed to the spacecraft hub body frame. The specific variables checked are the facet center of pressure locations ``r_CopB_B``, the facet normal vectors ``nHat_B``, and the facet articulation axes ``rotHat_B``. """ 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_CopF_F_list = [np.array([-0.1, 0.1, -0.1]), np.array([0.1, -0.1, -0.1])] # [m] facet_nHat_F_list = [np.array([1.0, 0.0, 0.0]), np.array([0.0, 1.0, 0.0])] facet_rotHat_F_list = [np.array([0.0, 1.0, 0.0]), np.array([0.0, 0.0, 1.0])] facet_r_FB_B_list = [np.array([0.0, 1.0, 0.0]), np.array([0.0, 0.0, -1.0])] # [m] facet_diffuse_coeff_list = [0.1, 0.1] facet_specular_coeff_list = [0.9, 0.9] prv_F01B = articulated_facet_1_initial_angle * facet_rotHat_F_list[0] prv_F02B = fixed_facet_2_initial_angle * facet_rotHat_F_list[1] facet_dcm_F0B_list = [rbk.PRV2C(prv_F01B), rbk.PRV2C(prv_F02B)] # Create the facet element input messages articulated_facet_1_element_message_data = messaging.FacetElementMsgPayload( area = facet_area_list[0], r_CopF_F = facet_r_CopF_F_list[0], nHat_F = facet_nHat_F_list[0], rotHat_F = facet_rotHat_F_list[0], dcm_F0B = facet_dcm_F0B_list[0], r_FB_B = facet_r_FB_B_list[0], c_diffuse = facet_diffuse_coeff_list[0], c_specular = facet_specular_coeff_list[0], ) fixed_facet_2_element_message_data = messaging.FacetElementMsgPayload( area = facet_area_list[1], r_CopF_F = facet_r_CopF_F_list[1], nHat_F = facet_nHat_F_list[1], rotHat_F = facet_rotHat_F_list[1], dcm_F0B = facet_dcm_F0B_list[1], r_FB_B = facet_r_FB_B_list[1], c_diffuse = facet_diffuse_coeff_list[1], c_specular = facet_specular_coeff_list[1], ) articulated_facet_1_element_message = messaging.FacetElementMsg().write(articulated_facet_1_element_message_data) fixed_facet_2_element_message = messaging.FacetElementMsg().write(fixed_facet_2_element_message_data) # Create the articulated facet angle message (facet 1) articulated_facet_angle_message_data = messaging.HingedRigidBodyMsgPayload() articulated_facet_angle_message_data.theta = articulated_facet_1_intermediate_angle # [rad] articulated_facet_angle_message_data.thetaDot = 0.0 # [rad/s] articulated_facet_angle_message = messaging.HingedRigidBodyMsg().write(articulated_facet_angle_message_data) # Create the faceted spacecraft module faceted_sc_model = facetedSpacecraftModel.FacetedSpacecraftModel() faceted_sc_model.ModelTag = "facetedSCModel" faceted_sc_model.setNumTotalFacets(num_facets) faceted_sc_model.addArticulatedFacet(articulated_facet_angle_message) faceted_sc_model.facetElementInMsgs[0].subscribeTo(articulated_facet_1_element_message) faceted_sc_model.facetElementInMsgs[1].subscribeTo(fixed_facet_2_element_message) test_sim.AddModelToTask(task_name, faceted_sc_model) # Set up data logging facet_element_body_data_log = [] for outMsg in faceted_sc_model.facetElementBodyOutMsgs: facet_element_body_data_log.append(outMsg.recorder()) test_sim.AddModelToTask(task_name, facet_element_body_data_log[-1]) # Execute simulation chunk 1 test_sim.InitializeSimulation() sim_time_1 = macros.sec2nano(2.0) # [ns] test_sim.ConfigureStopTime(sim_time_1) test_sim.ExecuteSimulation() # Create the second articulated facet angle message (facet 1) articulated_facet_angle_message_data = messaging.HingedRigidBodyMsgPayload() articulated_facet_angle_message_data.theta = articulated_facet_1_final_angle # [rad] articulated_facet_angle_message_data.thetaDot = 0.0 # [rad/s] articulated_facet_angle_message = messaging.HingedRigidBodyMsg().write(articulated_facet_angle_message_data) faceted_sc_model.articulatedFacetDataInMsgs[0].subscribeTo(articulated_facet_angle_message) # Execute simulation chunk 2 sim_time_2 = macros.sec2nano(3.0) # [ns] test_sim.ConfigureStopTime(sim_time_1 + sim_time_2) test_sim.ExecuteSimulation() # Retrieve the logged data facet_r_CopB_B_list_sim = [] facet_nHat_B_list_sim = [] facet_rotHat_B_list_sim = [] for data in facet_element_body_data_log: facet_r_CopB_B_list_sim.append(data.r_CopB_B) facet_nHat_B_list_sim.append(data.nHat_B) facet_rotHat_B_list_sim.append(data.rotHat_B) # Compute truth data (facet_r_CopB_B_list_truth, facet_nHat_B_list_truth, facet_rotHat_B_list_truth) = compute_facet_body_data(facet_r_CopF_F_list, facet_nHat_F_list, facet_rotHat_F_list, facet_r_FB_B_list, facet_dcm_F0B_list, articulated_facet_1_intermediate_angle, articulated_facet_1_final_angle) # Unit test check accuracy = 1e-12 # Check facet 1 data articulated_facet_1_data_to_check = [ (facet_r_CopB_B_list_sim[0][0:3], facet_r_CopB_B_list_truth[0]), (facet_nHat_B_list_sim[0][0:3], facet_nHat_B_list_truth[0]), (facet_rotHat_B_list_sim[0][0:3], facet_rotHat_B_list_truth[0]), (facet_r_CopB_B_list_sim[0][3:6], facet_r_CopB_B_list_truth[1]), (facet_nHat_B_list_sim[0][3:6], facet_nHat_B_list_truth[1]), (facet_rotHat_B_list_sim[0][3:6], facet_rotHat_B_list_truth[1]) ] for sim_data, truth_data in articulated_facet_1_data_to_check: for row in sim_data: np.testing.assert_allclose( row, truth_data, atol=accuracy, verbose=True ) # Check facet 2 data fixed_facet_2_data_to_check = [ (facet_r_CopB_B_list_sim[1], facet_r_CopB_B_list_truth[2]), (facet_nHat_B_list_sim[1], facet_nHat_B_list_truth[2]), (facet_rotHat_B_list_sim[1], facet_rotHat_B_list_truth[2]), ] for sim_data, truth_data in fixed_facet_2_data_to_check: for row in sim_data: np.testing.assert_allclose( row, truth_data, atol=accuracy, verbose=True )
def compute_facet_body_data(facet_r_CopF_F_list, facet_nHat_F_list, facet_rotHat_F_list, facet_r_FB_B_list, facet_dcm_F0B_list, articulated_facet_1_intermediate_angle, articulated_facet_1_final_angle): # Compute articulated facet 1 truth data for simulation segment 1 prv_FF0 = articulated_facet_1_intermediate_angle * facet_rotHat_F_list[0] dcm_FF0 = rbk.PRV2C(prv_FF0) dcm_FB = np.matmul(dcm_FF0, facet_dcm_F0B_list[0]) articulated_facet_1_r_CopB_B_intermediate = np.matmul(dcm_FB.transpose(), facet_r_CopF_F_list[0]) + facet_r_FB_B_list[0] articulated_facet_1_nHat_B_intermediate = np.matmul(dcm_FB.transpose(), facet_nHat_F_list[0]) articulated_facet_1_rotHat_B_intermediate = np.matmul(dcm_FB.transpose(), facet_rotHat_F_list[0]) # Compute articulated facet 1 truth data for simulation segment 2 prv_FF0 = articulated_facet_1_final_angle * facet_rotHat_F_list[0] dcm_FF0 = rbk.PRV2C(prv_FF0) dcm_FB = np.matmul(dcm_FF0, facet_dcm_F0B_list[0]) articulated_facet_1_r_CopB_B_final = np.matmul(dcm_FB.transpose(), facet_r_CopF_F_list[0]) + facet_r_FB_B_list[0] articulated_facet_1_nHat_B_final = np.matmul(dcm_FB.transpose(), facet_nHat_F_list[0]) articulated_facet_1_rotHat_B_final = np.matmul(dcm_FB.transpose(), facet_rotHat_F_list[0]) # Compute fixed facet 2 truth data fixed_facet_2_r_CopB_B = np.matmul(facet_dcm_F0B_list[1].transpose(), facet_r_CopF_F_list[1]) + facet_r_FB_B_list[1] fixed_facet_2_nHat_B = np.matmul(facet_dcm_F0B_list[1].transpose(), facet_nHat_F_list[1]) fixed_facet_2_rotHat_B = np.matmul(facet_dcm_F0B_list[1].transpose(), facet_rotHat_F_list[1]) # Format and output truth data as lists facet_r_CopB_B_list_truth = [articulated_facet_1_r_CopB_B_intermediate, articulated_facet_1_r_CopB_B_final, fixed_facet_2_r_CopB_B] facet_nHat_B_list_truth = [articulated_facet_1_nHat_B_intermediate, articulated_facet_1_nHat_B_final, fixed_facet_2_nHat_B] facet_rotHat_B_list_truth = [articulated_facet_1_rotHat_B_intermediate, articulated_facet_1_rotHat_B_final, fixed_facet_2_rotHat_B] return facet_r_CopB_B_list_truth, facet_nHat_B_list_truth, facet_rotHat_B_list_truth if __name__=="__main__": test_facetedSpacecraftModel( True, # show plots 10.0 * macros.D2R, # [rad] Articulated facet 1 initial angle 45.0 * macros.D2R, # [rad] Articulated facet 1 intermediate angle 110.0 * macros.D2R, # [rad] Articulated facet 1 final angle -30.0 * macros.D2R, # [rad] Fixed facet 2 initial angle )