Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/Support/bskReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ Version |release|
:ref:`scenarioPrescribedMotionWithTranslationBranching` and :ref:`scenarioPrescribedMotionWithRotationBranching`.
- Fixed a bug where :ref:`spinningBodyOneDOFStateEffector` and :ref:`spinningBodyNDOFStateEffector`
both registered their states under the same name, resulting in overwriting and a ``BSK_ERROR``.
- Avoid reloading and re-unloading SPICE kernels when multiple simulations run in the same process. This
fixes the problem with Spice becoming very slow when multiple simulation are run in parallel, addresses
the Spice kernel load limit of 5000 kernels, and prevents a rare bug where kernels are corrupted when
loaded from multiple simulations at the same time.


Version 2.8.0 (August 30, 2025)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from Basilisk.utilities import SimulationBaseClass
from Basilisk.simulation import spiceInterface

from Basilisk import __path__
bskPath = __path__[0]


def createOneSim():
"""
Create a minimal Basilisk simulation containing a single SpiceInterface.

Returns
-------
TotalSim : Basilisk SimulationBaseClass instance
The newly created simulation object.
SpiceObject : SpiceInterface
The SpiceInterface instance inside the created simulation.
"""
TotalSim = SimulationBaseClass.SimBaseClass()
DynUnitTestProc = TotalSim.CreateNewProcess("process")
DynUnitTestProc.addTask(TotalSim.CreateNewTask("task", 1))

# Create and register the SpiceInterface
SpiceObject = spiceInterface.SpiceInterface()
SpiceObject.SPICEDataPath = bskPath + '/supportData/EphemerisData/'
TotalSim.AddModelToTask("task", SpiceObject)

# Run long enough for the SpiceInterface to furnish its kernels
TotalSim.ConfigureStopTime(2)
TotalSim.InitializeSimulation()
TotalSim.ExecuteSimulation()

return TotalSim, SpiceObject


def test_multipleInterfaces():
"""
Verify that SPICE kernels loaded through SpiceInterface are correctly
reference-counted and unloaded when all SpiceInterface instances are gone.

The test performs the following high-level checks:

1. Before creating any SpiceInterface objects, the target kernel must not
be loaded in SPICE.

2. Creating the first simulation should cause the kernel to be furnished.

3. Creating many additional simulations must *not* load the kernel again.

4. After all simulations have loaded, the number of loaded kernels should
be the same as after loading one sim.

5. After all simulations fall out of scope and Python's garbage collector
runs, the kernel must be fully unloaded from SPICE.

This guarantees that:
- furnsh_c() is only called once per unique kernel file
- unload_c() is only called when the last user disappears
- the shared-pointer-based lifetime system works correctly
"""
kernel = f"{bskPath}/supportData/EphemerisData/de430.bsp"

# Step 1 - Kernel not yet loaded
assert not spiceInterface.isKernelLoaded(kernel)

def smallScope():
# Step 2 - First SpiceInterface loads the kernel
firstSim, firstSpice = createOneSim()
assert spiceInterface.isKernelLoaded(kernel)

kernelsLoadedWithOneSim = spiceInterface.countKernelsLoaded()

# Step 3 - Many more SpiceInterfaces do NOT reload the kernel
cacheSims = []
N = 20
for _ in range(N):
cacheSims.append(createOneSim())

kernelsLoadedWithNSims = spiceInterface.countKernelsLoaded()

# Step 4 - check kernels are not being loaded again
assert kernelsLoadedWithOneSim == kernelsLoadedWithNSims

# sanity check kernel is still loaded
assert spiceInterface.isKernelLoaded(kernel)

# Everything in smallScope is destroyed once we leave the function
smallScope()

import gc
gc.collect()

# Step 5 - Kernel must now be fully unloaded
assert not spiceInterface.isKernelLoaded(kernel)


if __name__ == "__main__":
test_multipleInterfaces()
Loading
Loading