Source code for test_gravityNonRotatingWarning

#
#  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:        gravityEffector
#   Purpose:            Verify the orientation-dependence predicate that gates the
#                       non-rotating-planet warning, that GravityEffector::Reset()
#                       actually emits (and suppresses) the BSK_WARNING
#                       accordingly, and that spherical-harmonic gravity without a
#                       planet-orientation message still runs (issue #1352).
#   Author:             robotrocketscience (https://github.com/robotrocketscience)
#   Creation Date:      Jun. 25, 2026
#

import os

import pytest
from Basilisk import __path__
from Basilisk.simulation import spacecraft
from Basilisk.simulation.pointMassGravityModel import PointMassGravityModel
from Basilisk.simulation.polyhedralGravityModel import PolyhedralGravityModel
from Basilisk.simulation.sphericalHarmonicsGravityModel import SphericalHarmonicsGravityModel
from Basilisk.utilities import SimulationBaseClass, macros, simIncludeGravBody

bskPath = __path__[0]
GGM03S = bskPath + "/supportData/LocalGravData/GGM03S.txt"
GGM03S_J2 = bskPath + "/supportData/LocalGravData/GGM03S-J2-only.txt"
# Polyhedral shape model (Eros) shipped alongside this test.
EROS_POLY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "EROS856Vert1708Fac.txt")
EROS_MU = 4.46275472004e5   # [m^3/s^2] Eros gravitational parameter

# A distinctive, stable fragment of the GravityEffector::Reset() warning text.
WARNING_SUBSTR = "no planet-orientation message"


def _buildShSim(gravFile, gravDeg):
    """Build a single-body spacecraft sim under a spherical-harmonic Earth field
    with NO planet-orientation message connected.

    The (unstarted) simulation is returned so the caller can run
    ``InitializeSimulation`` itself and capture the reset-time warning.
    """
    scSim = SimulationBaseClass.SimBaseClass()
    proc = scSim.CreateNewProcess("p")
    proc.addTask(scSim.CreateNewTask("t", macros.sec2nano(10.0)))

    scObject = spacecraft.Spacecraft()
    scObject.ModelTag = "sc"
    scSim.AddModelToTask("t", scObject)

    gravFactory = simIncludeGravBody.gravBodyFactory()
    earth = gravFactory.createEarth()
    earth.isCentralBody = True
    earth.useSphericalHarmonicsGravityModel(gravFile, gravDeg)
    scObject.gravField.gravBodies = spacecraft.GravBodyVector(
        list(gravFactory.gravBodies.values())
    )
    scObject.hub.r_CN_NInit = [-138514.048884, -888553.853077, 6818674.278964]   # [m]
    scObject.hub.v_CN_NInit = [-7306.6601, 2126.208457, 128.787296]              # [m/s]
    return scSim


def _buildPolySim():
    """Build a single-body spacecraft sim under a polyhedral (Eros) field with NO
    planet-orientation message connected.

    The (unstarted) simulation is returned so the caller can run
    ``InitializeSimulation`` itself and capture the reset-time warning.
    """
    scSim = SimulationBaseClass.SimBaseClass()
    proc = scSim.CreateNewProcess("p")
    proc.addTask(scSim.CreateNewTask("t", macros.sec2nano(10.0)))

    scObject = spacecraft.Spacecraft()
    scObject.ModelTag = "sc"
    scSim.AddModelToTask("t", scObject)

    gravFactory = simIncludeGravBody.gravBodyFactory()
    eros = gravFactory.createCustomGravObject("eros", mu=EROS_MU)
    eros.isCentralBody = True
    eros.usePolyhedralGravityModel(EROS_POLY)
    scObject.gravField.gravBodies = spacecraft.GravBodyVector(
        list(gravFactory.gravBodies.values())
    )
    scObject.hub.r_CN_NInit = [40.0e3, 0.0, 0.0]   # [m] just outside the Eros body
    scObject.hub.v_CN_NInit = [0.0, 3.0, 0.0]      # [m/s]
    return scSim


[docs] def test_tesseralFieldDependsOnOrientation(): """A spherical-harmonic field that retains order >= 1 terms varies with the body's longitude, so it must report that it depends on the body orientation. GravityEffector::Reset() uses this to decide whether to warn about a missing planet-orientation message (issue #1352).""" model = SphericalHarmonicsGravityModel() model.loadFromFile(GGM03S, 4) assert model.dependsOnOrientation() is True
[docs] def test_zonalFieldDoesNotDependOnOrientation(): """A purely zonal field (J2 only, no order >= 1 terms) depends on latitude alone, so it must report no orientation dependence (no false warning).""" model = SphericalHarmonicsGravityModel() model.loadFromFile(GGM03S_J2, 2) assert model.dependsOnOrientation() is False
[docs] def test_pointMassDoesNotDependOnOrientation(): """Point-mass gravity is spherically symmetric and never depends on the body orientation (the base-class default).""" assert PointMassGravityModel().dependsOnOrientation() is False
[docs] def test_polyhedralDependsOnOrientation(): """A polyhedral shape model is generally non-axisymmetric, so its field is always orientation-dependent (issue #1352). This holds independently of the loaded vertex/facet data, so the bare model must report True.""" assert PolyhedralGravityModel().dependsOnOrientation() is True
[docs] def test_resetEmitsWarningForTesseralFieldWithoutRotation(capfd): """GravityEffector::Reset() must actually emit the BSK_WARNING when a body whose field depends on orientation (GGM03S order 4, which retains tesseral terms) has no planet-orientation message connected. This is the user-facing alert for issue #1352, so the test fails if the warning block is removed. The warning is read from stdout (where ``bskLog`` writes it). It is observable here only because ``bskLog`` flushes warning-level output; without the flush the C runtime buffers it past pytest's capture point. Capturing the warning also exercises the full reset path, and propagating a few steps afterwards confirms the non-rotating field still initializes and runs (it must warn, not fail).""" scSim = _buildShSim(GGM03S, 4) scSim.InitializeSimulation() out, _ = capfd.readouterr() assert WARNING_SUBSTR in out, ( "Reset() did not emit the missing-planet-rotation warning; captured " f"stdout was:\n{out}" ) scSim.ConfigureStopTime(macros.sec2nano(60.0)) scSim.ExecuteSimulation()
[docs] def test_resetEmitsWarningForPolyhedralWithoutRotation(capfd): """GravityEffector::Reset() must also emit the warning for a polyhedral body without a planet-orientation message. This is the case schaubh flagged: a polyhedral field is evaluated in the body-fixed frame and was previously falling back to non-rotating silently (issue #1352). Init-only to stay fast.""" scSim = _buildPolySim() scSim.InitializeSimulation() out, _ = capfd.readouterr() assert WARNING_SUBSTR in out, ( "Reset() did not emit the missing-planet-rotation warning for a polyhedral " f"body; captured stdout was:\n{out}" )
[docs] def test_resetSilentForZonalFieldWithoutRotation(capfd): """A purely zonal field (GGM03S J2-only) does not depend on orientation, so Reset() must NOT warn even without a planet-orientation message. This guards against a false alarm on the common J2-only configuration (issue #1352).""" scSim = _buildShSim(GGM03S_J2, 2) scSim.InitializeSimulation() out, _ = capfd.readouterr() assert WARNING_SUBSTR not in out, ( "Reset() warned about a purely zonal (orientation-independent) field; " f"captured stdout was:\n{out}" )
if __name__ == "__main__": pytest.main([__file__, "-v"])