Skip to content

Commit e9e7bba

Browse files
tbd
1 parent 984d102 commit e9e7bba

File tree

4 files changed

+353
-48
lines changed

4 files changed

+353
-48
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from Basilisk.utilities import SimulationBaseClass
2+
from Basilisk.simulation import spiceInterface
3+
4+
from Basilisk import __path__
5+
bskPath = __path__[0]
6+
7+
8+
def createOneSim():
9+
"""
10+
Create a minimal Basilisk simulation containing a single SpiceInterface.
11+
12+
Returns
13+
-------
14+
TotalSim : Basilisk SimulationBaseClass instance
15+
The newly created simulation object.
16+
SpiceObject : SpiceInterface
17+
The SpiceInterface instance inside the created simulation.
18+
"""
19+
TotalSim = SimulationBaseClass.SimBaseClass()
20+
DynUnitTestProc = TotalSim.CreateNewProcess("process")
21+
DynUnitTestProc.addTask(TotalSim.CreateNewTask("task", 1))
22+
23+
# Create and register the SpiceInterface
24+
SpiceObject = spiceInterface.SpiceInterface()
25+
SpiceObject.SPICEDataPath = bskPath + '/supportData/EphemerisData/'
26+
TotalSim.AddModelToTask("task", SpiceObject)
27+
28+
# Run long enough for the SpiceInterface to furnish its kernels
29+
TotalSim.ConfigureStopTime(2)
30+
TotalSim.InitializeSimulation()
31+
TotalSim.ExecuteSimulation()
32+
33+
return TotalSim, SpiceObject
34+
35+
36+
def test_multipleInterfaces():
37+
"""
38+
Verify that SPICE kernels loaded through SpiceInterface are correctly
39+
reference-counted and unloaded when all SpiceInterface instances are gone.
40+
41+
The test performs the following high-level checks:
42+
43+
1. Before creating any SpiceInterface objects, the target kernel must not
44+
be loaded in SPICE.
45+
46+
2. Creating the first simulation should cause the kernel to be furnished.
47+
48+
3. Creating many additional simulations must *not* load the kernel again.
49+
SPICE can only ever load 5000 kernels, so if we can load > 5000 simulations
50+
it means that we're not reloading kernels unnecessarily.
51+
52+
4. After all simulations fall out of scope and Python's garbage collector
53+
runs, the kernel must be fully unloaded from SPICE.
54+
55+
This guarantees that:
56+
- furnsh_c() is only called once per unique kernel file
57+
- unload_c() is only called when the last user disappears
58+
- the shared-pointer-based lifetime system works correctly
59+
"""
60+
kernel = f"{bskPath}/supportData/EphemerisData/de430.bsp"
61+
62+
# Step 1 - Kernel not yet loaded
63+
assert not spiceInterface.isKernelLoaded(kernel)
64+
65+
def smallScope():
66+
# Step 2 - First SpiceInterface loads the kernel
67+
firstSim, firstSpice = createOneSim()
68+
assert spiceInterface.isKernelLoaded(kernel)
69+
70+
# Step 3 - Many more SpiceInterfaces do NOT reload the kernel
71+
cacheSims = []
72+
N = 5005
73+
for _ in range(N):
74+
cacheSims.append(createOneSim())
75+
76+
# Still loaded exactly once
77+
assert spiceInterface.isKernelLoaded(kernel)
78+
79+
# Everything in smallScope is destroyed once we leave the function
80+
smallScope()
81+
82+
# Force Python to release all cached SpiceInterface objects
83+
# just in case, probably not needed with CPython
84+
import gc
85+
gc.collect()
86+
87+
# Step 4 - Kernel must now be fully unloaded
88+
assert not spiceInterface.isKernelLoaded(kernel)
89+
90+
91+
if __name__ == "__main__":
92+
test_multipleInterfaces()

src/simulation/environment/spiceInterface/spiceInterface.cpp

Lines changed: 132 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,49 @@
2323
#include "architecture/utilities/simDefinitions.h"
2424
#include "architecture/utilities/macroDefinitions.h"
2525
#include "architecture/utilities/rigidBodyKinematics.h"
26+
#include "spiceInterface.h"
27+
28+
namespace {
29+
/**
30+
* RAII guard for SPICE error mode.
31+
*
32+
* Sets SPICE error action to RETURN while the guard is alive so that
33+
* calls report failures via failed_c() instead of aborting. Restores
34+
* the previous error action and print settings on destruction.
35+
*/
36+
struct SpiceErrorModeGuard
37+
{
38+
char oldAction[32];
39+
char oldPrint[32];
40+
41+
SpiceErrorModeGuard()
42+
{
43+
erract_c("GET", sizeof(oldAction), oldAction);
44+
errprt_c("GET", sizeof(oldPrint), oldPrint);
45+
46+
// Only override the abort behavior
47+
erract_c("SET", 0, const_cast<char*>("RETURN"));
48+
// DO NOT suppress printing: errprt is left untouched
49+
}
50+
51+
~SpiceErrorModeGuard()
52+
{
53+
erract_c("SET", 0, oldAction);
54+
errprt_c("SET", 0, oldPrint);
55+
}
56+
};
57+
58+
/**
59+
* Normalize a file system path to a canonical absolute string.
60+
*
61+
* Used to key kernels so that one physical file maps to a single
62+
* cache entry even if referenced through different relative paths.
63+
*/
64+
std::string absolutize(const std::filesystem::path& path)
65+
{
66+
return std::filesystem::absolute(path).lexically_normal().string();
67+
}
68+
}
2669

2770
/*! This constructor initializes the variables that spice uses. Most of them are
2871
not intended to be changed, but a couple are user configurable.
@@ -426,63 +469,46 @@ void SpiceInterface::pullSpiceData(std::vector<SpicePlanetStateMsgPayload> *spic
426469
}
427470
}
428471

429-
/*! This method loads a requested SPICE kernel into the system memory. It is
430-
its own method because we have to load several SPICE kernels in for our
431-
application. Note that they are stored in the SPICE library and are not
432-
held locally in this object.
433-
@return int Zero for success one for failure
434-
@param kernelName The name of the kernel we are loading
435-
@param dataPath The path to the data area on the filesystem
472+
/**
473+
* Load a SPICE kernel for use by this interface.
474+
*
475+
* This function takes a kernel file name and a base directory and
476+
* ensures that the corresponding SPICE kernel is available to the
477+
* simulation. Internally the module keeps track of which kernels it
478+
* has already loaded so that the same file is not loaded multiple
479+
* times.
480+
*
481+
* @param kernelName File name of the kernel inside dataPath.
482+
* @param dataPath Directory where the kernel is located.
483+
* @return 0 on success, 1 if loading the kernel failed.
436484
*/
437485
int SpiceInterface::loadSpiceKernel(char *kernelName, const char *dataPath)
438486
{
439-
char *fileName = new char[this->charBufferSize];
440-
SpiceChar *name = new SpiceChar[this->charBufferSize];
441-
442-
//! - The required calls come from the SPICE documentation.
443-
//! - The most critical call is furnsh_c
444-
strcpy(name, "REPORT");
445-
erract_c("SET", this->charBufferSize, name);
446-
strcpy(fileName, dataPath);
447-
strcat(fileName, kernelName);
448-
furnsh_c(fileName);
449-
450-
//! - Check to see if we had trouble loading a kernel and alert user if so
451-
strcpy(name, "DEFAULT");
452-
erract_c("SET", this->charBufferSize, name);
453-
delete[] fileName;
454-
delete[] name;
455-
if(failed_c()) {
456-
return 1;
457-
}
487+
std::filesystem::path base(dataPath);
488+
std::filesystem::path fullPath = base / kernelName;
489+
auto kernel = SpiceKernel::request(fullPath.string());
490+
if (!kernel->wasLoadSuccesful()) return 1;
491+
this->loadedKernels[kernel->getPath()] = kernel;
458492
return 0;
459493
}
460494

461-
/*! This method unloads a requested SPICE kernel into the system memory. It is
462-
its own method because we have to load several SPICE kernels in for our
463-
application. Note that they are stored in the SPICE library and are not
464-
held locally in this object.
465-
@return int Zero for success one for failure
466-
@param kernelName The name of the kernel we are unloading
467-
@param dataPath The path to the data area on the filesystem
495+
/**
496+
* Tell this interface that a SPICE kernel is no longer needed.
497+
*
498+
* This function removes the kernel from the set of kernels managed
499+
* by this interface. Once no users remain, the underlying kernel is
500+
* also removed from SPICE so it no longer affects future queries.
501+
*
502+
* @param kernelName File name of the kernel inside dataPath.
503+
* @param dataPath Directory where the kernel is located.
504+
* @return always 0.
468505
*/
469506
int SpiceInterface::unloadSpiceKernel(char *kernelName, const char *dataPath)
470507
{
471-
char *fileName = new char[this->charBufferSize];
472-
SpiceChar *name = new SpiceChar[this->charBufferSize];
473-
474-
//! - The required calls come from the SPICE documentation.
475-
//! - The most critical call is furnsh_c
476-
strcpy(name, "REPORT");
477-
erract_c("SET", this->charBufferSize, name);
478-
strcpy(fileName, dataPath);
479-
strcat(fileName, kernelName);
480-
unload_c(fileName);
481-
delete[] fileName;
482-
delete[] name;
483-
if(failed_c()) {
484-
return 1;
485-
}
508+
std::filesystem::path base(dataPath);
509+
std::filesystem::path fullPath = base / kernelName;
510+
auto key = absolutize(fullPath);
511+
this->loadedKernels.erase(key);
486512
return 0;
487513
}
488514

@@ -506,3 +532,61 @@ std::string SpiceInterface::getCurrentTimeString()
506532
delete[] spiceOutputBuffer;
507533
return(returnTimeString);
508534
}
535+
536+
std::mutex SpiceKernel::mutex;
537+
std::unordered_map<std::string, std::weak_ptr<SpiceKernel>> SpiceKernel::cache;
538+
539+
std::shared_ptr<SpiceKernel>
540+
SpiceKernel::request(const std::filesystem::path& path)
541+
{
542+
const std::string key = absolutize(path);
543+
544+
std::lock_guard<std::mutex> lock(mutex);
545+
546+
auto it = cache.find(key);
547+
if (it != cache.end())
548+
{
549+
if (auto existing = it->second.lock())
550+
{
551+
// Already have a live handle to this kernel
552+
return existing;
553+
}
554+
// Weak pointer expired - fall through and create a new one
555+
}
556+
557+
// First live handle for this absolute path in this process
558+
auto handle = std::shared_ptr<SpiceKernel>(new SpiceKernel(key));
559+
560+
if (handle->loadSucceeded) cache[key] = handle;
561+
562+
return handle;
563+
}
564+
565+
SpiceKernel::~SpiceKernel() noexcept
566+
{
567+
if (!loadSucceeded) return;
568+
569+
SpiceErrorModeGuard guard;
570+
unload_c(path.c_str());
571+
if (failed_c())
572+
{
573+
reset_c(); // SPICE printed its own messages already
574+
}
575+
}
576+
577+
SpiceKernel::SpiceKernel(std::string path_)
578+
: path(std::move(path_))
579+
{
580+
SpiceErrorModeGuard guard;
581+
furnsh_c(path.c_str());
582+
583+
if (failed_c())
584+
{
585+
reset_c(); // SPICE already printed diagnostics
586+
loadSucceeded = false; // destructor will not unload
587+
}
588+
else
589+
{
590+
loadSucceeded = true;
591+
}
592+
}

0 commit comments

Comments
 (0)