diff --git a/docs/source/Support/bskReleaseNotes.rst b/docs/source/Support/bskReleaseNotes.rst index 1533ecfa28..33002bd070 100644 --- a/docs/source/Support/bskReleaseNotes.rst +++ b/docs/source/Support/bskReleaseNotes.rst @@ -68,6 +68,8 @@ Version |release| - ``ConfigureStopTime`` now supports specifying the stop condition as ``<=`` (default, prior behavior) or ``>=`` (new). The new option is useful when the user wants to ensure that the simulation runs for at least the specified time, instead of at most the specified time. +- Add the ``fuelLeakRate`` parameter to the :ref:`FuelTank` module to simulate fuel leaks that cause a loss of fuel mass + without imparting momentum. Version 2.8.0 (August 30, 2025) diff --git a/src/simulation/dynamics/FuelTank/_UnitTest/test_mass_depletion.py b/src/simulation/dynamics/FuelTank/_UnitTest/test_mass_depletion.py index e56f9b3cd3..37cd33929f 100644 --- a/src/simulation/dynamics/FuelTank/_UnitTest/test_mass_depletion.py +++ b/src/simulation/dynamics/FuelTank/_UnitTest/test_mass_depletion.py @@ -194,5 +194,52 @@ def test_massDepletionTest(show_plots, thrusterConstructor): err_msg="Thruster mass depletion not ramped up") np.testing.assert_allclose(fuelMassDot[-1],0, rtol=1e-12, err_msg="Thruster mass depletion not ramped down") +def test_leakyTank(): + """Module Unit Test""" + scObject = spacecraft.Spacecraft() + scObject.ModelTag = "spacecraftBody" + unitTaskName = "unitTask" + unitProcessName = "TestProcess" + + # Create a sim module as an empty container + unitTestSim = SimulationBaseClass.SimBaseClass() + + # Create test thread + testProcessRate = macros.sec2nano(0.1) + testProc = unitTestSim.CreateNewProcess(unitProcessName) + testProc.addTask(unitTestSim.CreateNewTask(unitTaskName, testProcessRate)) + + # Make Fuel Tank + unitTestSim.fuelTankStateEffector = fuelTank.FuelTank() + tankModel = fuelTank.FuelTankModelConstantVolume() + unitTestSim.fuelTankStateEffector.setTankModel(tankModel) + tankModel.propMassInit = 40.0 + + # Add tank + scObject.addStateEffector(unitTestSim.fuelTankStateEffector) + + # Make the tank leaky + leakRate = 1e-5 # kg/s + unitTestSim.fuelTankStateEffector.fuelLeakRate = leakRate # kg/s + + # Add test module to runtime call list + unitTestSim.AddModelToTask(unitTaskName, unitTestSim.fuelTankStateEffector) + unitTestSim.AddModelToTask(unitTaskName, scObject) + + fuelLog = unitTestSim.fuelTankStateEffector.fuelTankOutMsg.recorder() + unitTestSim.AddModelToTask(unitTaskName, fuelLog) + unitTestSim.InitializeSimulation() + + stopTime = 1000.0 + unitTestSim.ConfigureStopTime(macros.sec2nano(stopTime)) + unitTestSim.ExecuteSimulation() + + fuelMass = fuelLog.fuelMass + fuelMassDot = fuelLog.fuelMassDot + + assert np.allclose(fuelMassDot, -leakRate, rtol=1e-6) + assert np.isclose(fuelMass[-1], 40.0 - stopTime * leakRate, rtol=1e-6) + + if __name__ == "__main__": test_massDepletionTest(True, thrusterDynamicEffector.ThrusterDynamicEffector) diff --git a/src/simulation/dynamics/FuelTank/fuelTank.cpp b/src/simulation/dynamics/FuelTank/fuelTank.cpp index bab02b0e73..32f7932f21 100644 --- a/src/simulation/dynamics/FuelTank/fuelTank.cpp +++ b/src/simulation/dynamics/FuelTank/fuelTank.cpp @@ -106,6 +106,7 @@ void FuelTank::updateEffectorMassProps(double integTime) { // Mass depletion (call thrusters attached to this tank to get their mDot, and contributions) this->fuelConsumption = 0.0; + this->fuelConsumption += this->fuelLeakRate; for (auto &dynEffector: this->thrDynEffectors) { dynEffector->computeStateContribution(integTime); this->fuelConsumption += dynEffector->stateDerivContribution(0); diff --git a/src/simulation/dynamics/FuelTank/fuelTank.h b/src/simulation/dynamics/FuelTank/fuelTank.h index 22cf883ca5..5224ad0819 100755 --- a/src/simulation/dynamics/FuelTank/fuelTank.h +++ b/src/simulation/dynamics/FuelTank/fuelTank.h @@ -270,6 +270,7 @@ class FuelTank : bool updateOnly = true; //!< -- Sets whether to use update only mass depletion Message fuelTankOutMsg{}; //!< -- fuel tank output message name FuelTankMsgPayload fuelTankMassPropMsg{}; //!< instance of messaging system message struct + double fuelLeakRate{}; //!< [kg/s] rate of fuel leaking from tank; does not produce force private: StateData *omegaState{}; //!< -- state data for omega_BN of the hub diff --git a/src/simulation/environment/spiceInterface/spiceInterface.cpp b/src/simulation/environment/spiceInterface/spiceInterface.cpp index bdcff1241c..d8cca5bb62 100755 --- a/src/simulation/environment/spiceInterface/spiceInterface.cpp +++ b/src/simulation/environment/spiceInterface/spiceInterface.cpp @@ -426,6 +426,8 @@ void SpiceInterface::pullSpiceData(std::vector *spic } } +const int MAXLEN = 256; + /*! This method loads a requested SPICE kernel into the system memory. It is its own method because we have to load several SPICE kernels in for our application. Note that they are stored in the SPICE library and are not @@ -439,6 +441,33 @@ int SpiceInterface::loadSpiceKernel(char *kernelName, const char *dataPath) char *fileName = new char[this->charBufferSize]; SpiceChar *name = new SpiceChar[this->charBufferSize]; + // Check if the kernel is already loaded + SpiceChar fileCompare [MAXLEN]; + SpiceChar filtyp [MAXLEN]; + SpiceChar srcfil [MAXLEN]; + SpiceInt handle; + SpiceBoolean found = SPICEFALSE; + SpiceInt total_kernels; + ktotal_c( "ALL", &total_kernels ); + + for (SpiceInt i = 0; i < total_kernels; i++ ) + { + // Get the i-th loaded kernel information + kdata_c(i, "ALL", MAXLEN, MAXLEN, MAXLEN, fileCompare, filtyp, srcfil, &handle, &found); + + if (found){ + // Check if kernelName is at the end of the file string + size_t fileLen = strlen(fileCompare); + size_t kernelLen = strlen(kernelName); + if (fileLen >= kernelLen && + strcmp(fileCompare + fileLen - kernelLen, kernelName) == 0) + { + // The kernel_to_check has already been successfully loaded + return 0; + } + } + } + //! - The required calls come from the SPICE documentation. //! - The most critical call is furnsh_c strcpy(name, "REPORT");