From 31930c8fdbcaab618c4ba4936db9c6b06304bffb Mon Sep 17 00:00:00 2001 From: Juan Garcia Bonilla Date: Mon, 24 Nov 2025 17:05:40 -0800 Subject: [PATCH 1/4] Add DragGeometryMsgPayload --- .../msgPayloadDefC/DragGeometryMsgPayload.h | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/architecture/msgPayloadDefC/DragGeometryMsgPayload.h diff --git a/src/architecture/msgPayloadDefC/DragGeometryMsgPayload.h b/src/architecture/msgPayloadDefC/DragGeometryMsgPayload.h new file mode 100644 index 0000000000..40ac9210a1 --- /dev/null +++ b/src/architecture/msgPayloadDefC/DragGeometryMsgPayload.h @@ -0,0 +1,31 @@ +/* + ISC License + + Copyright (c) 2025, 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. + + */ + +#ifndef DRAG_GEOMETRY_MSG_PAYLOAD_H +#define DRAG_GEOMETRY_MSG_PAYLOAD_H + + +//! @brief Container for basic drag parameters. +typedef struct { + double projectedArea; //!< [m^2] Area of spacecraft projected in velocity direction + double dragCoeff; //!< [-] Nondimensional drag coefficient + double r_CP_S[3]; //!< [m] Position of center of pressure relative to the center of frame S, given in frame S [m] +}DragGeometryMsgPayload; + +#endif From 1168d900f6df1310ccde1cdd91afed0039e7e0f3 Mon Sep 17 00:00:00 2001 From: Juan Garcia Bonilla Date: Mon, 24 Nov 2025 17:06:39 -0800 Subject: [PATCH 2/4] Add AerodynamicDrag --- .../aerodynamicDrag/aerodynamicDrag.cpp | 104 +++++++++++++++ .../aerodynamicDrag/aerodynamicDrag.h | 118 ++++++++++++++++++ .../aerodynamicDrag/aerodynamicDrag.i | 52 ++++++++ 3 files changed, 274 insertions(+) create mode 100644 src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.cpp create mode 100644 src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.h create mode 100644 src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.i diff --git a/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.cpp b/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.cpp new file mode 100644 index 0000000000..d24aad286c --- /dev/null +++ b/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.cpp @@ -0,0 +1,104 @@ +/* + ISC License + + Copyright (c) 2025, 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. + + */ +#include "aerodynamicDrag.h" + +#include + +void +AerodynamicDrag::UpdateState(uint64_t CurrentSimNanos) +{ + const double density = this->atmoDensInMsg().neutralDensity; + + const auto geomPayload = this->dragGeometryInMsg(); + const double cd = geomPayload.dragCoeff; + const double area = geomPayload.projectedArea; + const Eigen::Map r_CP_s(geomPayload.r_CP_S); + + // 'S' denotes the 'site' reference frame + const auto siteStatePayload = this->referenceFrameStateInMsg(); + Eigen::MRPd sigma_SN; + sigma_SN = Eigen::Map(siteStatePayload.sigma_BN); + const Eigen::Matrix3d dcm_SN = sigma_SN.toRotationMatrix().transpose(); + + // inertial velocity of the center of the S frame, expressed in the S frame + const auto v_S = dcm_SN*Eigen::Map(siteStatePayload.v_BN_N); + const auto v = v_S.norm(); + + if (v < 1e-10) + { + this->forceOutMsg.write(&this->forceOutMsg.zeroMsgPayload, this->moduleID, CurrentSimNanos); + this->torqueOutMsg.write(&this->torqueOutMsg.zeroMsgPayload, this->moduleID, CurrentSimNanos); + return; + } + + const Eigen::Vector3d force_S = -0.5 * cd * area * density * v * v_S; + const Eigen::Vector3d torque_S = r_CP_s.cross(force_S); + + ForceAtSiteMsgPayload forcePayload = this->forceOutMsg.zeroMsgPayload; + TorqueAtSiteMsgPayload torquePayload = this->torqueOutMsg.zeroMsgPayload; + std::copy_n(force_S.data(), 3, forcePayload.force_S); + std::copy_n(torque_S.data(), 3, torquePayload.torque_S); + + this->forceOutMsg.write(&forcePayload, this->moduleID, CurrentSimNanos); + this->torqueOutMsg.write(&torquePayload, this->moduleID, CurrentSimNanos);} + +void AerodynamicDrag::Reset(uint64_t /*CurrentSimNanos*/) +{ + if (!this->atmoDensInMsg.isLinked()) { + bskLogger.bskLog(BSK_ERROR, + "AerodynamicDrag: atmoDensInMsg is not linked. " + "Call atmoDensInMsg.subscribeTo() before simulation."); + } + + if (!this->dragGeometryInMsg.isLinked()) { + bskLogger.bskLog(BSK_ERROR, + "AerodynamicDrag: dragGeometryInMsg is not linked. " + "Call dragGeometryInMsg.subscribeTo() before simulation."); + } + + if (!this->referenceFrameStateInMsg.isLinked()) { + bskLogger.bskLog(BSK_ERROR, + "AerodynamicDrag: referenceFrameStateInMsg is not linked. " + "Call referenceFrameStateInMsg.subscribeTo() before simulation."); + } +} + + +MJForceTorqueActuator& +AerodynamicDrag::applyTo(MJBody& body) +{ + return applyTo(body.getCenterOfMass()); +} + +MJForceTorqueActuator& +AerodynamicDrag::applyTo(MJSite& site) +{ + this->referenceFrameStateInMsg.subscribeTo( &site.stateOutMsg ); + const auto actuatorName = ModelTag + "_aerodynamicDrag_" + site.getName(); + auto& actuator = site.getBody().getSpec().getScene().addForceTorqueActuator(actuatorName, site); + return applyTo(actuator); +} + +MJForceTorqueActuator& +AerodynamicDrag::applyTo(MJForceTorqueActuator& actuator) +{ + actuator.forceInMsg.subscribeTo( &this->forceOutMsg ); + actuator.torqueInMsg.subscribeTo( &this->torqueOutMsg ); + return actuator; +} diff --git a/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.h b/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.h new file mode 100644 index 0000000000..5370485b31 --- /dev/null +++ b/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.h @@ -0,0 +1,118 @@ +/* + ISC License + + Copyright (c) 2025, 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. + + */ +#ifndef AERODYNAMIC_DRAG_H +#define AERODYNAMIC_DRAG_H + +#include "architecture/utilities/bskLogging.h" +#include "architecture/messaging/messaging.h" +#include "architecture/_GeneralModuleFiles/sys_model.h" +#include "simulation/mujocoDynamics/_GeneralModuleFiles/MJScene.h" + +#include "architecture/msgPayloadDefC/DragGeometryMsgPayload.h" +#include "architecture/msgPayloadDefC/AtmoPropsMsgPayload.h" +#include "architecture/msgPayloadDefC/ForceAtSiteMsgPayload.h" +#include "architecture/msgPayloadDefC/TorqueAtSiteMsgPayload.h" + +/** + * @brief Aerodynamic drag model that computes force and torque at a MuJoCo site. + * + * Updates a force and a torque to represent quasi-steady aerodynamic drag acting + * at a site fixed on a body. Force direction is opposite the relative flow. + * Magnitude follows \f$ F = \tfrac{1}{2}\rho v^2 C_D A \f$. + * Torque is \f$ \tau = r_{CP/S} \times F \f$ in the site frame. + * + * The force and torque output in `forceOutMsg` and `torqueOutMsg` are given + * in the site reference frame whose state is given in `referenceFrameStateInMsg`. + */ +class AerodynamicDrag : public SysModel +{ +public: + /** + * @brief Bind this model to an MJBody target. + * + * If this method is used, only the `dragGeometryInMsg` and + * `atmoDensInMsg` must be linked. + * + * @param body MuJoCo body that will receive the drag force and torque at it's center of mass. + * @return Reference to the actuator on which the forceand torque is applied. + */ + MJForceTorqueActuator& applyTo(MJBody& body); + + /** + * @brief Bind this model to an MJSite target. + * + * If this method is used, only the `dragGeometryInMsg` and + * `atmoDensInMsg` must be linked. + * + * @param site MuJoCo site where the force application point is defined. + * @return Reference to the actuator on which the forceand torque is applied. + */ + MJForceTorqueActuator& applyTo(MJSite& site); + + /** + * @brief Bind this model to an existing force/torque actuator. + * + * If this method is used, the `dragGeometryInMsg`, + * `atmoDensInMsg`, and `referenceFrameStateInMsg` must be linked. + * + * @param actuator Precreated actuator that will receive the computed wrench. + * @return Reference to the same actuator. + * + * @note You + */ + MJForceTorqueActuator& applyTo(MJForceTorqueActuator& actuator); + + + /** + * @brief Advance the model and publish outputs. + * @param CurrentSimNanos Current simulation time in nanoseconds. + */ + void UpdateState(uint64_t CurrentSimNanos) override; + + /** + * @brief Validate all input messages are linked. + * @param CurrentSimNanos Current simulation time in nanoseconds. + */ + void Reset(uint64_t CurrentSimNanos) override; + + +public: + + /** @brief Geometry input. */ + ReadFunctor dragGeometryInMsg; + + /** @brief Atmospheric properties input. */ + ReadFunctor atmoDensInMsg; + + /** @brief State of the reference frame . */ + ReadFunctor referenceFrameStateInMsg; + + /** @brief Output force at the site in the site reference frame. */ + Message forceOutMsg; + + /** @brief Output torque in the site reference frame.*/ + Message torqueOutMsg; + + + /** @brief Logger */ + BSKLogger bskLogger; +}; + + +#endif diff --git a/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.i b/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.i new file mode 100644 index 0000000000..60677b456c --- /dev/null +++ b/src/simulation/mujocoDynamics/aerodynamicDrag/aerodynamicDrag.i @@ -0,0 +1,52 @@ +/* + ISC License + + Copyright (c) 2025, Autonomous Vehicle Systems Lab, 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. + +*/ + +%module MJAerodynamicDrag + +%include "architecture/utilities/bskException.swg" +%default_bsk_exception(); + +%{ + #include "aerodynamicDrag.h" +%} + +%pythoncode %{ + from Basilisk.architecture.swig_common_model import * +%} +%include "std_string.i" +%include "swig_conly_data.i" + +%include "sys_model.i" +%include "aerodynamicDrag.h" + +%include "architecture/msgPayloadDefC/DragGeometryMsgPayload.h" +struct DragGeometryMsg_C; +%include "architecture/msgPayloadDefC/AtmoPropsMsgPayload.h" +struct AtmoPropsMsg_C; +%include "architecture/msgPayloadDefC/SCStatesMsgPayload.h" +struct SCStatesMsg_C; +%include "architecture/msgPayloadDefC/ForceAtSiteMsgPayload.h" +struct ForceAtSiteMsg_C; +%include "architecture/msgPayloadDefC/TorqueAtSiteMsgPayload.h" +struct TorqueAtSiteMsg_C; + +%pythoncode %{ +import sys +protectAllClasses(sys.modules[__name__]) +%} From 189b1d6d2bd9bbfe05a40527d8e6ec8b43abc51f Mon Sep 17 00:00:00 2001 From: Juan Garcia Bonilla Date: Mon, 24 Nov 2025 17:06:48 -0800 Subject: [PATCH 3/4] Add test for AerodynamicDrag --- .../_UnitTest/test_aerodynamicDrag.py | 385 ++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 src/simulation/mujocoDynamics/aerodynamicDrag/_UnitTest/test_aerodynamicDrag.py diff --git a/src/simulation/mujocoDynamics/aerodynamicDrag/_UnitTest/test_aerodynamicDrag.py b/src/simulation/mujocoDynamics/aerodynamicDrag/_UnitTest/test_aerodynamicDrag.py new file mode 100644 index 0000000000..1fb75a60dd --- /dev/null +++ b/src/simulation/mujocoDynamics/aerodynamicDrag/_UnitTest/test_aerodynamicDrag.py @@ -0,0 +1,385 @@ +# +# ISC License +# +# Copyright (c) 2025, 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. +from typing import Literal + +import pytest +import numpy as np +import numpy.testing as npt + +from Basilisk.utilities import SimulationBaseClass +from Basilisk.architecture import messaging +from Basilisk.utilities import macros +from Basilisk.utilities import RigidBodyKinematics as rbk +from Basilisk.utilities import simIncludeGravBody +from Basilisk.utilities import orbitalMotion +from Basilisk.simulation import pointMassGravityModel +from Basilisk.simulation import exponentialAtmosphere + +try: + from Basilisk.simulation import mujoco + from Basilisk.simulation import MJAerodynamicDrag + from Basilisk.simulation import NBodyGravity + + couldImportMujoco = True +except: + couldImportMujoco = False + +@pytest.mark.skipif(not couldImportMujoco, reason="Compiled Basilisk without --mujoco") +def test_aerodynamicDrag(): + """ + Unit test for verification of AerodynamicDrag. + """ + dt = 1 # s + + scSim = SimulationBaseClass.SimBaseClass() + dynProcess = scSim.CreateNewProcess("test") + dynProcess.addTask(scSim.CreateNewTask("test", macros.sec2nano(dt))) + + # Create the MJScene from a simple cannonball body + scene = mujoco.MJScene("") # empty scene, no multi-body dynamics + scSim.AddModelToTask("test", scene) + + density = 2 # kg/m^3 + cd = 1.5 + area = 2.75 # m^2 + r_CP_S = [1, 0, 0] # m + + v_SN_N = [0, 0.5, 0] # m/s + sigma_SN = [0.1, 0.2, 0.3] + + atmoMsg = messaging.AtmoPropsMsg() + atmoMsg.write(messaging.AtmoPropsMsgPayload(neutralDensity=density)) + + dragGeometryMsg = messaging.DragGeometryMsg() + dragGeometryMsg.write(messaging.DragGeometryMsgPayload( + projectedArea = area, + dragCoeff = cd, + r_CP_S = r_CP_S + )) + + siteFrameMsg = messaging.SCStatesMsg() + siteFrameMsg.write(messaging.SCStatesMsgPayload( + sigma_BN = sigma_SN, + v_BN_N = v_SN_N + )) + + drag = MJAerodynamicDrag.AerodynamicDrag() + drag.dragGeometryInMsg.subscribeTo(dragGeometryMsg) + drag.atmoDensInMsg.subscribeTo(atmoMsg) + drag.referenceFrameStateInMsg.subscribeTo(siteFrameMsg) + scene.AddModelToDynamicsTask(drag) + + scSim.InitializeSimulation() + scSim.ConfigureStopTime(macros.sec2nano(dt)) + scSim.ExecuteSimulation() + + force_S = drag.forceOutMsg.read().force_S + torque_S = drag.torqueOutMsg.read().torque_S + + # Expected + # Rotate velocity from N to S using MRPs + C_BN = np.array(rbk.MRP2C(sigma_SN)) # maps N components to B=S components + v_S = C_BN.dot(v_SN_N) + v_mag = np.linalg.norm(v_SN_N) # drag magnitude depends on speed, frame invariant + if v_mag <= 1e-12: + F_exp = np.zeros(3) + T_exp = np.zeros(3) + else: + F_mag = 0.5 * density * (v_mag ** 2) * cd * area + v_hat_S = v_S / np.linalg.norm(v_S) + F_exp = -F_mag * v_hat_S + T_exp = np.cross(r_CP_S, F_exp) + + npt.assert_allclose(force_S, F_exp, rtol=0.0, atol=1e-12) + npt.assert_allclose(torque_S, T_exp, rtol=0.0, atol=1e-12) + +@pytest.mark.skipif(not couldImportMujoco, reason="Compiled Basilisk without --mujoco") +@pytest.mark.parametrize("orbitCase", ["LPO", "LTO"]) +@pytest.mark.parametrize("planetCase", ["earth", "mars"]) +def test_orbit(orbitCase: Literal["LPO", "LTO"], planetCase: Literal["earth", "mars"], showPlots = False): + """Test equivalent to `simulation/dynamics/dragEffector/_UnitTest/test_atmoDrag.py`""" + def cannonballDragBFrame(dragCoeff, density, area, velInertial, sigmaBN): + speed = np.linalg.norm(velInertial) + if speed <= 0.0: + return np.zeros(3) + + dragDirInertial = -velInertial / speed + dcmBN = rbk.MRP2C(sigmaBN) + dragDirBody = dcmBN.dot(dragDirInertial) + + return 0.5 * dragCoeff * density * area * speed**2 * dragDirBody + + + scSim = SimulationBaseClass.SimBaseClass() + processName = "dragProcess" + taskName = "dragTask" + dynamicsProcess = scSim.CreateNewProcess(processName) + stepNanos = macros.sec2nano(1.0) + dynamicsProcess.addTask(scSim.CreateNewTask(taskName, stepNanos)) + + bodyName = "satellite" + dragSceneXml = fr""" + + + + + + + + + + + """ + scene = mujoco.MJScene(dragSceneXml) + scene.extraEoMCall = True + scSim.AddModelToTask(taskName, scene) + + mjBody = scene.getBody(bodyName) + + planetData = simIncludeGravBody.BODY_DATA[planetCase] + gravitationalParameter = planetData.mu + planetRadius = planetData.radEquator + + gravityModel = NBodyGravity.NBodyGravity() + gravityModel.ModelTag = "gravity" + scene.AddModelToDynamicsTask(gravityModel) + + pointMassModel = pointMassGravityModel.PointMassGravityModel() + pointMassModel.muBody = gravitationalParameter + gravityModel.addGravitySource(planetCase, pointMassModel, True) + gravityModel.addGravityTarget(bodyName, mjBody) + + if planetCase == "earth": + baseDensity = 1.217 + scaleHeight = 8500.0 + else: + baseDensity = 0.020 + scaleHeight = 11100.0 + + atmoModel = exponentialAtmosphere.ExponentialAtmosphere() + atmoModel.ModelTag = "ExpAtmo" + atmoModel.planetRadius = planetRadius + atmoModel.baseDensity = baseDensity + atmoModel.scaleHeight = scaleHeight + scene.AddModelToDynamicsTask(atmoModel) + + comStateMsg = mjBody.getCenterOfMass().stateOutMsg + atmoModel.addSpacecraftToModel(comStateMsg) + + projectedArea = 10.0 + dragCoeff = 2.2 + + dragGeometryMsg = messaging.DragGeometryMsg() + dragGeometryMsg.write( + messaging.DragGeometryMsgPayload( + projectedArea=projectedArea, + dragCoeff=dragCoeff, + r_CP_S=[1.0, 0.0, 0.0], + ) + ) + + dragModel = MJAerodynamicDrag.AerodynamicDrag() + dragModel.ModelTag = "drag" + scene.AddModelToDynamicsTask(dragModel) + forceTorqueDrag: mujoco.MJForceTorqueActuator = dragModel.applyTo(mjBody) + dragModel.atmoDensInMsg.subscribeTo(atmoModel.envOutMsgs[0]) + dragModel.dragGeometryInMsg.subscribeTo(dragGeometryMsg) + + if orbitCase == "LPO": + orbAltMin = 300e3 # m + orbAltMax = orbAltMin + elif orbitCase == "LTO": + orbAltMin = 300e3 # m + orbAltMax = 800e3 # m + + rMin = planetRadius + orbAltMin + rMax = planetRadius + orbAltMax + + elements = orbitalMotion.ClassicElements() + elements.a = (rMin+rMax)/2.0 + elements.e = 1.0 - rMin/elements.a + elements.i = 0.0 + elements.Omega = 0.0 + elements.omega = 0.0 + elements.f = 0.0 + + meanMotion = np.sqrt(gravitationalParameter / elements.a**3) + orbitPeriod = 2.0 * np.pi / meanMotion + finalTimeNanos = macros.sec2nano(orbitPeriod) + + stateRecorder = comStateMsg.recorder() + atmoRecorder = atmoModel.envOutMsgs[0].recorder() + dragForceRecorder = forceTorqueDrag.forceInMsg.recorder() + + scSim.AddModelToTask(taskName, stateRecorder) + scSim.AddModelToTask(taskName, atmoRecorder) + scSim.AddModelToTask(taskName, dragForceRecorder) + + scSim.InitializeSimulation() + + positionInertial, velocityInertial = orbitalMotion.elem2rv(gravitationalParameter, elements) + mjBody.setPosition(positionInertial) + mjBody.setVelocity(velocityInertial) + + scSim.ConfigureStopTime(finalTimeNanos) + scSim.ExecuteSimulation() + + positionData = stateRecorder.r_BN_N + velocityData = stateRecorder.v_BN_N + attitudeData = stateRecorder.sigma_BN + densityData = atmoRecorder.neutralDensity + dragForceData = dragForceRecorder.force_S + + numSteps = dragForceData.shape[0] + referenceDrag = np.zeros_like(dragForceData) + + for timeIndex in range(numSteps): + referenceDrag[timeIndex, :] = cannonballDragBFrame( + dragCoeff=dragCoeff, + density=densityData[timeIndex], + area=projectedArea, + velInertial=velocityData[timeIndex, :], + sigmaBN=attitudeData[timeIndex, :], + ) + + npt.assert_allclose( + dragForceData, + referenceDrag, + rtol=0, + atol=1e-12, + ) + + if showPlots: + import matplotlib.pyplot as plt + + timesSec = stateRecorder.times() * macros.NANO2SEC + timesOrbit = timesSec / orbitPeriod + + # Inertial position components + plt.figure() + fig = plt.gcf() + ax = fig.gca() + ax.ticklabel_format(useOffset=False, style="plain") + for idx in range(3): + plt.plot( + timesOrbit, + positionData[:, idx] / 1e3, + label=f"$r_{{BN,{idx}}}$", + ) + plt.legend(loc="lower right") + plt.xlabel("Time [orbits]") + plt.ylabel("Inertial position [km]") + + # Orbit in perifocal frame + elementsInit = orbitalMotion.rv2elem(gravitationalParameter, positionInertial, velocityInertial) + bParam = elementsInit.a * np.sqrt(1.0 - elementsInit.e**2) + pParam = elementsInit.a * (1.0 - elementsInit.e**2) + + plt.figure(figsize=tuple(np.array((1.0, bParam / elementsInit.a)) * 4.75), dpi=100) + axisLimits = np.array( + [-elementsInit.rApoap, elementsInit.rPeriap, -bParam, bParam] + ) / 1e3 * 1.25 + plt.axis(axisLimits) + + fig = plt.gcf() + ax = fig.gca() + planetColor = "#008800" + planetRadiusKm = planetRadius / 1e3 + ax.add_artist(plt.Circle((0.0, 0.0), planetRadiusKm, color=planetColor)) + + rDataList = [] + fDataList = [] + for idx in range(positionData.shape[0]): + elementsStep = orbitalMotion.rv2elem( + gravitationalParameter, + positionData[idx, 0:3], + velocityData[idx, 0:3], + ) + rDataList.append(elementsStep.rmag) + fDataList.append(elementsStep.f + elementsStep.omega - elementsInit.omega) + + rDataArray = np.array(rDataList) + fDataArray = np.array(fDataList) + + plt.plot( + rDataArray * np.cos(fDataArray) / 1e3, + rDataArray * np.sin(fDataArray) / 1e3, + color="#aa0000", + linewidth=3.0, + ) + + fGrid = np.linspace(0.0, 2.0 * np.pi, 200) + rGrid = pParam / (1.0 + elementsInit.e * np.cos(fGrid)) + plt.plot( + rGrid * np.cos(fGrid) / 1e3, + rGrid * np.sin(fGrid) / 1e3, + "--", + color="#555555", + ) + plt.xlabel("$i_e$ coord [km]") + plt.ylabel("$i_p$ coord [km]") + plt.grid() + + # Semi major axis vs time + plt.figure() + fig = plt.gcf() + ax = fig.gca() + ax.ticklabel_format(useOffset=False, style="plain") + smaData = [] + for idx in range(positionData.shape[0]): + elementsStep = orbitalMotion.rv2elem( + gravitationalParameter, + positionData[idx, 0:3], + velocityData[idx, 0:3], + ) + smaData.append(elementsStep.a / 1e3) + smaData = np.array(smaData) + plt.plot(timesOrbit, smaData, color="#aa0000") + plt.xlabel("Time [orbits]") + plt.ylabel("SMA [km]") + + # Density vs time + plt.figure() + fig = plt.gcf() + ax = fig.gca() + ax.ticklabel_format(useOffset=False, style="sci") + timesAtmoSec = atmoRecorder.times() * macros.NANO2SEC + plt.plot(timesAtmoSec, densityData) + plt.title("Density vs time") + plt.xlabel("Time [s]") + plt.ylabel("Density [kg/m^3]") + + # Drag vs reference + plt.figure() + fig = plt.gcf() + ax = fig.gca() + ax.ticklabel_format(useOffset=False, style="plain") + for idx in range(3): + plt.plot(timesOrbit, dragForceData[:, idx], label=f"F_mod_{idx}") + plt.plot(timesOrbit, referenceDrag[:, idx], "--", label=f"F_ref_{idx}") + plt.xlabel("Time [orbits]") + plt.ylabel("Drag force [N]") + plt.legend() + plt.title("Drag force vs reference") + + plt.show() + + +if __name__ == "__main__": + assert couldImportMujoco + # test_aerodynamicDrag() + test_orbit("LTO","earth",True) From c59b1f065c7374b4f4e387b1ef628a393665c492 Mon Sep 17 00:00:00 2001 From: Juan Garcia Bonilla Date: Mon, 24 Nov 2025 17:09:19 -0800 Subject: [PATCH 4/4] Update release notes --- docs/source/Support/bskReleaseNotes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/Support/bskReleaseNotes.rst b/docs/source/Support/bskReleaseNotes.rst index 1545321ebc..1768f9c2d0 100644 --- a/docs/source/Support/bskReleaseNotes.rst +++ b/docs/source/Support/bskReleaseNotes.rst @@ -108,6 +108,10 @@ Version |release| ``pyStatefulSysModel.i`` to use this mixin. - Added support for setting the position and velocity of :ref:`MJBody` with pure translational motion. - Added ``getAxis`` and ``isHinge`` to :ref:`MJJoint`. +- Added :ref:`DragGeometryMsgPayload`, which contains geometry information of a body necessary to + compute the drag acting on such body. +- Added :ref:`AerodynamicDrag`, a model intended to be used with MuJoCo dynamics to model + cannonball aerodynamic drag forces and torques acting on bodies. Version 2.8.0 (August 30, 2025)