# ISC License
#
# Copyright (c) 2026, PIC4SeR & AVS Lab, Politecnico di Torino & Argotec S.R.L., University of Colorado 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.
"""Test that the atmosphere base class evaluates the spacecraft state extrapolated to the step midpoint."""
import numpy as np
import pytest
from Basilisk.architecture import messaging
from Basilisk.simulation import exponentialAtmosphere
from Basilisk.utilities import SimulationBaseClass, macros, orbitalMotion, simSetPlanetEnvironment
STEP = 10.0 # [s] module update period
def _density(r, v, timeNanos, stopSeconds=2 * STEP):
"""Return the exponential atmosphere density at the third module update.
The state message is rewritten one step after ``timeNanos``, so that the module observes the spacecraft task period
and the extrapolation applies.
Args:
r (list): [m] spacecraft position written to the state message.
v (list): [m/s] spacecraft velocity written to the state message.
timeNanos (int): [ns] time at which the state message is written.
stopSeconds (float): [s] simulation stop time; the density of the last module update is returned.
"""
scSim = SimulationBaseClass.SimBaseClass()
proc = scSim.CreateNewProcess("p")
proc.addTask(scSim.CreateNewTask("t", macros.sec2nano(STEP)))
atmo = exponentialAtmosphere.ExponentialAtmosphere()
atmo.setExtrapolateScStateToStepMidpoint(True)
simSetPlanetEnvironment.exponentialAtmosphere(atmo, "earth")
payload = messaging.SCStatesMsgPayload()
payload.r_BN_N = list(r)
payload.v_BN_N = list(v)
scMsg = messaging.SCStatesMsg().write(payload, timeNanos)
atmo.addSpacecraftToModel(scMsg)
scSim.AddModelToTask("t", atmo)
recorder = atmo.envOutMsgs[0].recorder()
scSim.AddModelToTask("t", recorder)
scSim.InitializeSimulation()
scSim.ConfigureStopTime(macros.sec2nano(STEP))
scSim.ExecuteSimulation()
scMsg.write(payload, timeNanos + macros.sec2nano(STEP))
scSim.ConfigureStopTime(macros.sec2nano(stopSeconds))
scSim.ExecuteSimulation()
return recorder.neutralDensity[-1]
def _densityWithRewrittenState(moduleStep, writeTimes, stopSeconds, r, v, writeBeforeUpdate=False):
"""Return the density of the last module update for a state message that is rewritten during the run.
The state is re-written with the same position at each time in ``writeTimes``, emulating a spacecraft that is
updated at a different task period than the atmosphere module.
Args:
moduleStep (float): [s] task period of the atmosphere module.
writeTimes (list): [s] times at which the spacecraft state message is written, in increasing order.
stopSeconds (float): [s] simulation stop time.
r (list): [m] spacecraft position written to the state message.
v (list): [m/s] spacecraft velocity written to the state message.
writeBeforeUpdate (bool): if True the state message is written just before the module update at the same
time, as a spacecraft that runs before the module in the task does.
"""
scSim = SimulationBaseClass.SimBaseClass()
proc = scSim.CreateNewProcess("p")
proc.addTask(scSim.CreateNewTask("t", macros.sec2nano(moduleStep)))
atmo = exponentialAtmosphere.ExponentialAtmosphere()
atmo.setExtrapolateScStateToStepMidpoint(True)
simSetPlanetEnvironment.exponentialAtmosphere(atmo, "earth")
payload = messaging.SCStatesMsgPayload()
payload.r_BN_N = list(r)
payload.v_BN_N = list(v)
scMsg = messaging.SCStatesMsg().write(payload, macros.sec2nano(writeTimes[0]))
atmo.addSpacecraftToModel(scMsg)
scSim.AddModelToTask("t", atmo)
recorder = atmo.envOutMsgs[0].recorder()
scSim.AddModelToTask("t", recorder)
scSim.InitializeSimulation()
for writeTime in writeTimes[1:]:
scSim.ConfigureStopTime(macros.sec2nano(writeTime) - (1 if writeBeforeUpdate else 0)) # [ns]
scSim.ExecuteSimulation()
scMsg.write(payload, macros.sec2nano(writeTime))
scSim.ConfigureStopTime(macros.sec2nano(stopSeconds))
scSim.ExecuteSimulation()
return recorder.neutralDensity[-1]
[docs]
def test_translating_planet_does_not_change_the_density():
"""Verify the spacecraft and the planet are evaluated at the same epoch at every update, including the startup.
The spacecraft and the planet move together at 30 km/s, so the altitude is constant. At every update the planet
message is written at the current time while the spacecraft message is one step old. Before the task period of
the spacecraft is known (the first updates) the spacecraft is not extrapolated, so the planet must be moved back
to the epoch of the spacecraft message. Extrapolating only one of them would shift the relative position by up to
v * dt and change the density. The density is checked at the update at 0 s, at the first positive-time update
and at all the following ones."""
speed = 3.0e4 # [m/s] common velocity of the spacecraft and the planet
r0 = np.array([(orbitalMotion.REQ_EARTH + 400.0) * 1000.0, 0.0, 0.0]) # [m]
updates = 6 # [-] number of module updates
def run(moving):
velocity = np.array([0.0, speed if moving else 0.0, 0.0]) # [m/s]
scSim = SimulationBaseClass.SimBaseClass()
proc = scSim.CreateNewProcess("p")
proc.addTask(scSim.CreateNewTask("t", macros.sec2nano(STEP)))
atmo = exponentialAtmosphere.ExponentialAtmosphere()
atmo.setExtrapolateScStateToStepMidpoint(True)
simSetPlanetEnvironment.exponentialAtmosphere(atmo, "earth")
def scPayload(time):
payload = messaging.SCStatesMsgPayload()
payload.r_BN_N = (r0 + velocity * time).tolist()
payload.v_BN_N = velocity.tolist()
return payload
def planetPayload(time):
payload = messaging.SpicePlanetStateMsgPayload()
payload.PositionVector = (velocity * time).tolist()
payload.VelocityVector = velocity.tolist()
payload.J20002Pfix = np.eye(3).tolist()
return payload
scMsg = messaging.SCStatesMsg().write(scPayload(0.0), 0)
atmo.addSpacecraftToModel(scMsg)
planetMsg = messaging.SpicePlanetStateMsg().write(planetPayload(0.0), 0)
atmo.planetPosInMsg.subscribeTo(planetMsg)
scSim.AddModelToTask("t", atmo)
recorder = atmo.envOutMsgs[0].recorder()
scSim.AddModelToTask("t", recorder)
scSim.InitializeSimulation()
for k in range(updates):
time = k * STEP # [s]
# the planet is updated before the module, the spacecraft after it
planetMsg.write(planetPayload(time), macros.sec2nano(time))
scSim.ConfigureStopTime(macros.sec2nano(time))
scSim.ExecuteSimulation()
scMsg.write(scPayload(time), macros.sec2nano(time))
return np.array(recorder.neutralDensity)
moving = run(True)
static = run(False)
assert len(moving) == updates
for k in range(updates):
assert moving[k] == pytest.approx(static[k], rel=1e-9, abs=0.0), f"update {k}"
def _densityOfFirstOfTwoSpacecraft(secondWriteSeconds, r, v):
"""Return the density at the second module update after STEP seconds for the first of two spacecraft.
Args:
secondWriteSeconds (float): [s] time at which the state message of the second spacecraft is written.
r (list): [m] position of both spacecraft.
v (list): [m/s] velocity of the first spacecraft; the second one is at rest.
"""
scSim = SimulationBaseClass.SimBaseClass()
proc = scSim.CreateNewProcess("p")
proc.addTask(scSim.CreateNewTask("t", macros.sec2nano(STEP)))
atmo = exponentialAtmosphere.ExponentialAtmosphere()
atmo.setExtrapolateScStateToStepMidpoint(True)
simSetPlanetEnvironment.exponentialAtmosphere(atmo, "earth")
payloads = []
for velocity in (v, [0.0, 0.0, 0.0]):
payload = messaging.SCStatesMsgPayload()
payload.r_BN_N = list(r)
payload.v_BN_N = list(velocity)
payloads.append(payload)
firstMsg = messaging.SCStatesMsg().write(payloads[0], 0)
secondMsg = messaging.SCStatesMsg().write(payloads[1], 0)
atmo.addSpacecraftToModel(firstMsg)
atmo.addSpacecraftToModel(secondMsg)
scSim.AddModelToTask("t", atmo)
recorder = atmo.envOutMsgs[0].recorder()
scSim.AddModelToTask("t", recorder)
scSim.InitializeSimulation()
scSim.ConfigureStopTime(macros.sec2nano(STEP))
scSim.ExecuteSimulation()
# both spacecraft rewrite their state; the second one at secondWriteSeconds instead of the previous module update
firstMsg.write(payloads[0], macros.sec2nano(STEP))
secondMsg.write(payloads[1], macros.sec2nano(secondWriteSeconds))
scSim.ConfigureStopTime(macros.sec2nano(2 * STEP))
scSim.ExecuteSimulation()
return recorder.neutralDensity[-1]
if __name__ == "__main__":
test_density_is_evaluated_at_extrapolated_position()
test_stale_message_is_never_extrapolated()
test_translating_planet_does_not_change_the_density()
test_one_mismatched_spacecraft_disables_the_extrapolation_of_all()