From 2d08cd47802ce64326e335b7eee80c40bb05ac65 Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Thu, 19 Feb 2026 11:03:47 -0500 Subject: [PATCH 1/9] wip --- SpiceQL/include/io.h | 26 ++++++ SpiceQL/src/io.cpp | 119 ++++++++++++++++++++++++- bindings/python/CMakeLists.txt | 3 + bindings/python/io.i | 1 + bindings/python/io.py | 154 +++++++++++++++++++++++++++++++++ 5 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 bindings/python/io.py diff --git a/SpiceQL/include/io.h b/SpiceQL/include/io.h index 10d556d5..9b461b9e 100644 --- a/SpiceQL/include/io.h +++ b/SpiceQL/include/io.h @@ -6,6 +6,7 @@ * **/ +#include #include #include #include @@ -207,3 +208,28 @@ namespace SpiceQL { void writeTextKernel(std::string fileName, std::string type, nlohmann::json &keywords, std::string comment = ""); } + +#ifdef __cplusplus +extern "C" { +#endif +/** Returns 0 on success, -1 on error. On error, call writeCkFromBuffersLastError() for the message. */ +int writeCkFromBuffers( + const char* path, + const double* quats, + size_t n_quats, + const double* times, + size_t n_times, + int bodyCode, + const char* referenceFrame, + const char* segmentId, + const char* sclk, + const char* lsk, + const double* av, + size_t n_av, + const char* comment +); +/** Last error message from writeCkFromBuffers (valid until next writeCk call). */ +const char* writeCkFromBuffersLastError(void); +#ifdef __cplusplus +} +#endif diff --git a/SpiceQL/src/io.cpp b/SpiceQL/src/io.cpp index 0711a6bf..3c85287c 100644 --- a/SpiceQL/src/io.cpp +++ b/SpiceQL/src/io.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include @@ -8,6 +10,8 @@ #include "io.h" #include "utils.h" +#include + using namespace std; @@ -94,7 +98,6 @@ namespace SpiceQL { for(auto &et : times) { double sclkdp; checkNaifErrors(); - sce2c_c(bodyCode/1000, et, &sclkdp); checkNaifErrors(); et = sclkdp; } @@ -139,6 +142,22 @@ namespace SpiceQL { vector> stateVelocities, string segmentComment) { + if (stateTimes.empty() || statePositions.empty()) { + throw runtime_error("writeSpk: stateTimes and statePositions must be non-empty."); + } + + // NAIF spkw13_c requires segment start time < end time. Single-epoch (e.g. from ISD) has start == end. + if (stateTimes.size() == 1) { + stateTimes.push_back(stateTimes.front() + 1E-6); + statePositions.push_back(statePositions.front()); + if (!stateVelocities.empty()) { + stateVelocities.push_back(stateVelocities.front()); + } + } else if (stateTimes.front() >= stateTimes.back()) { + throw runtime_error( + "writeSpk: segment start time must be less than end time (got start == end or reversed order)."); + } + vector> states; if (stateVelocities.empty()) { @@ -352,4 +371,102 @@ namespace SpiceQL { textKernel.close(); } + namespace { + thread_local std::string g_writeCkFromBuffersLastError; + + std::vector split(const std::string& s, char delim) { + std::vector out; + std::istringstream ss(s); + std::string part; + while (std::getline(ss, part, delim)) { + auto start = part.find_first_not_of(" \t"); + if (start == std::string::npos) continue; + auto end = part.find_last_not_of(" \t"); + out.push_back(part.substr(start, end == std::string::npos ? part.size() : end - start + 1)); + } + return out; + } + } + + extern "C" int writeCkFromBuffers( + const char* path, + const double* quats, + size_t n_quats, + const double* times, + size_t n_times, + int bodyCode, + const char* referenceFrame, + const char* segmentId, + const char* sclk, + const char* lsk, + const double* av, + size_t n_av, + const char* comment + ) { + g_writeCkFromBuffersLastError.clear(); + if (path == nullptr || quats == nullptr || times == nullptr || n_quats == 0 || n_times == 0) { + g_writeCkFromBuffersLastError = "writeCkFromBuffers: path/quats/times must be non-null and non-empty."; + return -1; + } + try { + std::string commentStr(comment ? comment : ""); + if (commentStr.empty()) commentStr = "CK Kernel"; + + // sclk: single path or comma-separated list; must keep Kernel objects alive or destructors unload them + std::vector> sclkKernels; + if (sclk && *sclk) { + for (const std::string& sclkPath : split(sclk, ',')) + sclkKernels.push_back(std::make_unique(sclkPath)); + } + Kernel lskKernel(lsk ? lsk : ""); + + int clockId = bodyCode / 1000; + std::vector sclkTimes(n_times); + for (size_t i = 0; i < n_times; i++) { + double sclkdp; + checkNaifErrors(); + sce2c_c(clockId, times[i], &sclkdp); + checkNaifErrors(); + sclkTimes[i] = sclkdp; + } + checkNaifErrors(); + + SpiceInt handle; + ckopn_c(path, "CK", (SpiceInt)commentStr.size(), &handle); + checkNaifErrors(); + ckw03_c( + handle, + sclkTimes[0], + sclkTimes[n_times - 1], + bodyCode, + referenceFrame ? referenceFrame : "", + (av != nullptr && n_av > 0) ? SPICETRUE : SPICEFALSE, + segmentId ? segmentId : "", + (SpiceInt)n_times, sclkTimes.data(), + quats, + (av != nullptr && n_av > 0) ? av : nullptr, + (SpiceInt)n_times, + sclkTimes.data() + ); + checkNaifErrors(); + + ckcls_c(handle); + checkNaifErrors(); + writeComment(path, commentStr); + return 0; + } catch (const std::exception& e) { + g_writeCkFromBuffersLastError = e.what(); + reset_c(); + return -1; + } catch (...) { + g_writeCkFromBuffersLastError = "writeCkFromBuffers: unknown exception"; + reset_c(); + return -1; + } + } + + extern "C" const char* writeCkFromBuffersLastError(void) { + return g_writeCkFromBuffersLastError.c_str(); + } + } diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index a0a915f3..07304eb7 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -44,6 +44,9 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in configure_file(${CMAKE_CURRENT_SOURCE_DIR}/__init__.py ${PYSPICEQL_OUTPUT_DIR}/__init__.py COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/io.py + ${PYSPICEQL_OUTPUT_DIR}/io.py + COPYONLY) # Setup to run setup tools on install install(CODE "execute_process(COMMAND pip install . WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})") diff --git a/bindings/python/io.i b/bindings/python/io.i index decff1ee..da5b773d 100644 --- a/bindings/python/io.i +++ b/bindings/python/io.i @@ -4,4 +4,5 @@ #include "io.h" %} +%ignore writeCkFromBuffers; %include "io.h" \ No newline at end of file diff --git a/bindings/python/io.py b/bindings/python/io.py new file mode 100644 index 00000000..e5fa644d --- /dev/null +++ b/bindings/python/io.py @@ -0,0 +1,154 @@ +import ctypes +import numpy as np +import os +import sys + + +def _find_lib(): + # Get the pyspiceql module extension + mod = sys.modules.get("pyspiceql._pyspiceql") + if mod is None: + try: + import pyspiceql + except ImportError: + pass + mod = sys.modules.get("pyspiceql._pyspiceql") + if not mod or not getattr(mod, "__file__", None): + return None, None, [] + + # Path to _pyspiceql.so + so_dir = os.path.normpath(os.path.dirname(os.path.abspath(mod.__file__))) + + # For local builds + build_root = os.path.normpath(os.path.join(so_dir, "..", "..", "..")) + env_libs = [os.path.join(p, "lib") for p in (os.environ.get("CONDA_PREFIX"), sys.prefix) if p] + lib_dirs = [*env_libs, so_dir, build_root] + + # Check extension for writeCkFromBuffers() + try: + lib = ctypes.CDLL(mod.__file__) + if hasattr(lib, "writeCkFromBuffers"): + return lib, mod.__file__, lib_dirs + except OSError: + pass + + # Search for libSpiceQL + for d in lib_dirs: + if not os.path.isdir(d): + continue + for name in os.listdir(d): + if name.startswith("libSpiceQL") and (name.endswith(".so") or name.endswith(".dylib")): + path = os.path.join(d, name) + try: + lib = ctypes.CDLL(path) + if hasattr(lib, "writeCkFromBuffers"): + return lib, path, lib_dirs + except OSError: + pass + + return None, None, lib_dirs + + +_ck_lib, _ck_lib_path, _ck_lib_search_dirs = _find_lib() + +if _ck_lib is not None: + try: + _ck_lib.writeCkFromBuffers.argtypes = [ + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_double), + ctypes.c_size_t, + ctypes.POINTER(ctypes.c_double), + ctypes.c_size_t, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_double), + ctypes.c_size_t, + ctypes.c_char_p, + ] + _ck_lib.writeCk.restype = ctypes.c_int + if hasattr(_ck_lib, "writeCkFromBuffersLastError"): + _ck_lib.writeCkFromBuffersLastError.restype = ctypes.c_char_p + _ck_lib.writeCkFromBuffersLastError.argtypes = [] + _ck_lib_ok = True + except AttributeError: + _ck_lib_ok = False +else: + _ck_lib_ok = False + + +def write_ck( + path, + quats, + times, + body_code, + reference_frame, + segment_id, + sclk, + lsk, + angular_velocities=None, + comment="", +): + if not _ck_lib_ok: + dirs = _ck_lib_search_dirs + hint = "" + if dirs: + hint = " Searched: " + ", ".join(dirs) + ". " + raise RuntimeError( + "writeCkFromBuffers not found. Rebuild SpiceQL and ensure libSpiceQL is on the library path." + + hint + ) + + quats = np.ascontiguousarray(np.asarray(quats, dtype=np.float64)) + times = np.ascontiguousarray(np.asarray(times, dtype=np.float64)) + n = quats.shape[0] + if quats.ndim != 2 or quats.shape[1] != 4: + raise ValueError("quats must have shape (n, 4)") + if times.shape[0] != n: + raise ValueError("times length must match quats rows") + + if angular_velocities is not None and len(angular_velocities) != 0: + angular_velocities = np.ascontiguousarray(np.asarray(angular_velocities, dtype=np.float64)) + if angular_velocities.shape[0] != n or angular_velocities.shape[1] != 3: + raise ValueError("angular_velocities must have shape (n, 3)") + av_ptr = angular_velocities.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + n_av = n + else: + av_ptr = None + n_av = 0 + + def _b(s): + """Convert value to bytes""" + if s is None: + return b"" + if isinstance(s, str): + return s.encode("utf-8") + return str(s).encode("utf-8") + + # Accept str or list of str (multiple SCLK kernels) for sclk, comma deliminated + sclk_arg = ",".join(sclk) if isinstance(sclk, (list, tuple)) else sclk + + rc = _ck_lib.writeCkFromBuffers( + _b(path), + quats.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + n, + times.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + n, + int(body_code), + _b(reference_frame), + _b(segment_id), + _b(sclk_arg), + _b(lsk), + av_ptr, + n_av, + _b(comment), + ) + if rc != 0: + err = "unknown error" + if hasattr(_ck_lib, "writeCkFromBuffersLastError"): + msg = _ck_lib.writeCkFromBuffersLastError() + if msg: + err = msg.decode("utf-8") if isinstance(msg, bytes) else msg + raise RuntimeError(f"writeCkFromBuffers failed: {err}") From b99b28a49d714792defd36b6f4d4554dcdf2f7d5 Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Fri, 27 Mar 2026 11:25:07 -0400 Subject: [PATCH 2/9] rename io.py to ck_writer.py --- SpiceQL/src/api.cpp | 2 + bindings/python/CMakeLists.txt | 4 +- bindings/python/{io.py => ck_writer.py} | 59 +++++++++---------------- 3 files changed, 25 insertions(+), 40 deletions(-) rename bindings/python/{io.py => ck_writer.py} (71%) diff --git a/SpiceQL/src/api.cpp b/SpiceQL/src/api.cpp index 5c226b14..b3f7b8cc 100644 --- a/SpiceQL/src/api.cpp +++ b/SpiceQL/src/api.cpp @@ -33,6 +33,7 @@ namespace SpiceQL { vector default_KernelQualities = {"smithed", "reconstructed"}; json aliasMap = { + {"A15_METRIC", "apollo"}, {"AMICA", "amica"}, {"CHANDRAYAAN-1_M3", "m3"}, {"CHANDRAYAAN-1_MRFFR", "mrffr"}, @@ -71,6 +72,7 @@ namespace SpiceQL { {"MRO_MARCI_UV", "marci"}, {"MRO_CTX", "ctx"}, {"MRO_HIRISE", "hirise"}, + {"MRO_HIRISE_LOOK_DIRECTION", "hirise"}, {"MRO_CRISM_VNIR", "crism"}, {"NEAR EARTH ASTEROID RENDEZVOUS", ""}, {"NH_LORRI", "lorri"}, diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index 07304eb7..5415744f 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -44,8 +44,8 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in configure_file(${CMAKE_CURRENT_SOURCE_DIR}/__init__.py ${PYSPICEQL_OUTPUT_DIR}/__init__.py COPYONLY) -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/io.py - ${PYSPICEQL_OUTPUT_DIR}/io.py +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ck_writer.py + ${PYSPICEQL_OUTPUT_DIR}/ck_writer.py COPYONLY) # Setup to run setup tools on install diff --git a/bindings/python/io.py b/bindings/python/ck_writer.py similarity index 71% rename from bindings/python/io.py rename to bindings/python/ck_writer.py index e5fa644d..2a7a20a2 100644 --- a/bindings/python/io.py +++ b/bindings/python/ck_writer.py @@ -3,50 +3,33 @@ import os import sys - def _find_lib(): - # Get the pyspiceql module extension - mod = sys.modules.get("pyspiceql._pyspiceql") - if mod is None: - try: - import pyspiceql - except ImportError: - pass - mod = sys.modules.get("pyspiceql._pyspiceql") - if not mod or not getattr(mod, "__file__", None): - return None, None, [] - - # Path to _pyspiceql.so - so_dir = os.path.normpath(os.path.dirname(os.path.abspath(mod.__file__))) - - # For local builds - build_root = os.path.normpath(os.path.join(so_dir, "..", "..", "..")) - env_libs = [os.path.join(p, "lib") for p in (os.environ.get("CONDA_PREFIX"), sys.prefix) if p] - lib_dirs = [*env_libs, so_dir, build_root] - - # Check extension for writeCkFromBuffers() + # Load library try: + from . import _pyspiceql as mod + lib = ctypes.CDLL(mod.__file__) if hasattr(lib, "writeCkFromBuffers"): - return lib, mod.__file__, lib_dirs - except OSError: + return lib, mod.__file__, [] + except (ImportError, AttributeError, OSError) as e: + # Log the error to your console/terminal for debugging + print(f"DEBUG: Internal import failed: {e}") pass - # Search for libSpiceQL - for d in lib_dirs: - if not os.path.isdir(d): - continue - for name in os.listdir(d): - if name.startswith("libSpiceQL") and (name.endswith(".so") or name.endswith(".dylib")): - path = os.path.join(d, name) - try: - lib = ctypes.CDLL(path) - if hasattr(lib, "writeCkFromBuffers"): - return lib, path, lib_dirs - except OSError: - pass + # Manual search and load library + try: + current_dir = os.path.dirname(os.path.abspath(__file__)) + # Look for the .so or .dylib file in the same folder as io.py + for f in os.listdir(current_dir): + if f.startswith("_pyspiceql") and (f.endswith(".so") or f.endswith(".dylib")): + path = os.path.join(current_dir, f) + lib = ctypes.CDLL(path) + if hasattr(lib, "writeCkFromBuffers"): + return lib, path, [] + except Exception as e: + print(f"DEBUG: Manual directory search failed: {e}") - return None, None, lib_dirs + return None, None, [] _ck_lib, _ck_lib_path, _ck_lib_search_dirs = _find_lib() @@ -68,7 +51,7 @@ def _find_lib(): ctypes.c_size_t, ctypes.c_char_p, ] - _ck_lib.writeCk.restype = ctypes.c_int + _ck_lib.writeCkFromBuffers.restype = ctypes.c_int if hasattr(_ck_lib, "writeCkFromBuffersLastError"): _ck_lib.writeCkFromBuffersLastError.restype = ctypes.c_char_p _ck_lib.writeCkFromBuffersLastError.argtypes = [] From e0a9fa4c5a5144d6819940613c6d7eae36c566de Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Wed, 1 Apr 2026 14:55:44 -0400 Subject: [PATCH 3/9] rename --- bindings/python/ck_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/python/ck_writer.py b/bindings/python/ck_writer.py index 2a7a20a2..eee6c46d 100644 --- a/bindings/python/ck_writer.py +++ b/bindings/python/ck_writer.py @@ -19,7 +19,7 @@ def _find_lib(): # Manual search and load library try: current_dir = os.path.dirname(os.path.abspath(__file__)) - # Look for the .so or .dylib file in the same folder as io.py + # Look for the .so or .dylib file in the same folder as ck_writer.py for f in os.listdir(current_dir): if f.startswith("_pyspiceql") and (f.endswith(".so") or f.endswith(".dylib")): path = os.path.join(current_dir, f) From 6264fb90252d2c96618f3ba8fd9ebbc09d5e60d0 Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Wed, 1 Apr 2026 15:02:00 -0400 Subject: [PATCH 4/9] remove comment --- bindings/python/ck_writer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bindings/python/ck_writer.py b/bindings/python/ck_writer.py index eee6c46d..641f448d 100644 --- a/bindings/python/ck_writer.py +++ b/bindings/python/ck_writer.py @@ -12,7 +12,6 @@ def _find_lib(): if hasattr(lib, "writeCkFromBuffers"): return lib, mod.__file__, [] except (ImportError, AttributeError, OSError) as e: - # Log the error to your console/terminal for debugging print(f"DEBUG: Internal import failed: {e}") pass From 7f3c545ed6b44bc794274035363b8ae9344af5a9 Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Wed, 1 Apr 2026 15:08:37 -0400 Subject: [PATCH 5/9] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64dfbfb9..ae87a79c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ release. ### Added - Added getTargetStatesRanged so a request can be made with start, end, and range of ETs instead of a list [#115](https://github.com/DOI-USGS/SpiceQL/pull/115) +- Added ck_writer.py and additional support for ISD to kernel generation [#116](https://github.com/DOI-USGS/SpiceQL/pull/116) ### 1.2.7 From 86a03782214b04f07a12d349c23ff3a0b4d56440 Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Fri, 3 Apr 2026 09:31:27 -0400 Subject: [PATCH 6/9] add kernel ext support --- SpiceQL/include/spice_types.h | 39 +++++++++++++++++++++++++++++++++++ SpiceQL/src/api.cpp | 1 + SpiceQL/src/spice_types.cpp | 36 ++++++++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/SpiceQL/include/spice_types.h b/SpiceQL/include/spice_types.h index 3b6d03a8..98441743 100644 --- a/SpiceQL/include/spice_types.h +++ b/SpiceQL/include/spice_types.h @@ -71,6 +71,45 @@ namespace SpiceQL { **/ static Type translateType(std::string type); + /** + * @brief Get Kernel extension + * + * @param type Kernel type string + * @return Kernel extension as string + */ + static std::string getExt(std::string type); + + /** + * @brief Check if kernel type is binary + * + * @param type Kernel type string + * @return Whether kernel type is binary + */ + static bool isBinary(std::string type); + + /** + * @brief Check if kernel type is text-based + * + * @param type Kernel type string + * @return Whether kernel type is text-based + */ + static bool isText(std::string type); + + /** + * @brief Check if kernel type is CK + * + * @param type Kernel type string + * @return Whether kernel type is CK + */ + static bool isCk(std::string type); + + /** + * @brief Check if kernel type is SPK + * + * @param type Kernel type string + * @return Whether kernel type is SPK + */ + static bool isSpk(std::string type); /** * @brief Switch between Quality enum to string diff --git a/SpiceQL/src/api.cpp b/SpiceQL/src/api.cpp index 0a607adb..38bd780b 100644 --- a/SpiceQL/src/api.cpp +++ b/SpiceQL/src/api.cpp @@ -63,6 +63,7 @@ namespace SpiceQL { {"MARS", "mro"}, {"MSGR_MDIS_WAC", "mdis"}, {"MSGR_MDIS_NAC", "mdis"}, + {"MEX_HRSC_HEAD", "hrsc"}, {"MEX_HRSC_SRC", "src"}, {"MEX_HRSC_IR", "hrsc"}, {"MGS_MOC_NA", "mgs"}, diff --git a/SpiceQL/src/spice_types.cpp b/SpiceQL/src/spice_types.cpp index 71580d4b..a0063af1 100644 --- a/SpiceQL/src/spice_types.cpp +++ b/SpiceQL/src/spice_types.cpp @@ -54,6 +54,14 @@ namespace SpiceQL { "reconstructed", "smithed"}; + const std::unordered_map KERNEL_EXTS = { {Kernel::Type::CK, ".bc"}, + {Kernel::Type::SPK, ".bsp"}, + {Kernel::Type::FK, ".tf"}, + {Kernel::Type::IK, ".ti"}, + {Kernel::Type::LSK, ".tls"}, + {Kernel::Type::MK, ".tm"}, + {Kernel::Type::PCK, ".tpc"}, + {Kernel::Type::SCLK, ".tsc"}}; string Kernel::translateType(Kernel::Type type) { return KERNEL_TYPES[static_cast(type)]; @@ -61,14 +69,38 @@ namespace SpiceQL { Kernel::Type Kernel::translateType(string type) { - auto res = findInVector(KERNEL_TYPES, type); + auto res = findInVector(KERNEL_TYPES, toLower(type)); if (res.first) { return static_cast(res.second); } throw invalid_argument(fmt::format("{} is not a valid kernel type", type)); - }; + } + + std::string Kernel::getExt(std::string type) { + Kernel::Type ktype = translateType(type); + auto it = KERNEL_EXTS.find(ktype); + if (it != KERNEL_EXTS.end()) { + return it->second; + } + throw invalid_argument(fmt::format("{} is not a valid kernel type", type)); + } + + bool Kernel::isBinary(std::string type) { + return (isCk(type) || isSpk(type)); + } + + bool Kernel::isText(std::string type) { + return !isBinary(translateType(type)); + } + bool Kernel::isCk(std::string type) { + return translateType(type) == Kernel::Type::CK; + } + + bool Kernel::isSpk(std::string type) { + return translateType(type) == Kernel::Type::SPK; + } string Kernel::translateQuality(Kernel::Quality qa) { return KERNEL_QUALITIES[static_cast(qa)]; From 8ea9ecbc7ef8692d76f0eec10fe62f5b7f837f33 Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Fri, 3 Apr 2026 12:19:28 -0400 Subject: [PATCH 7/9] fix --- SpiceQL/src/spice_types.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpiceQL/src/spice_types.cpp b/SpiceQL/src/spice_types.cpp index a0063af1..8bf3a1c4 100644 --- a/SpiceQL/src/spice_types.cpp +++ b/SpiceQL/src/spice_types.cpp @@ -91,7 +91,7 @@ namespace SpiceQL { } bool Kernel::isText(std::string type) { - return !isBinary(translateType(type)); + return !isBinary(type); } bool Kernel::isCk(std::string type) { From cb337ff81c48b979541b981fd638300c31608165 Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Fri, 3 Apr 2026 14:41:29 -0400 Subject: [PATCH 8/9] updated aliasMap --- SpiceQL/src/api.cpp | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/SpiceQL/src/api.cpp b/SpiceQL/src/api.cpp index 38bd780b..8fb4bdc4 100644 --- a/SpiceQL/src/api.cpp +++ b/SpiceQL/src/api.cpp @@ -39,6 +39,8 @@ namespace SpiceQL { {"CHANDRAYAAN-1_MRFFR", "mrffr"}, {"CASSINI_ISS_NAC", "cassini"}, {"CASSINI_ISS_WAC", "cassini"}, + {"CASSINI_VIMS_V", "cassini"}, + {"Cassini-Huygens", "cassini"}, {"DAWN_FC2_FILTER_1", "fc2"}, {"DAWN_FC2_FILTER_2", "fc2"}, {"DAWN_FC2_FILTER_3", "fc2"}, @@ -47,6 +49,7 @@ namespace SpiceQL { {"DAWN_FC2_FILTER_6", "fc2"}, {"DAWN_FC2_FILTER_7", "fc2"}, {"DAWN_FC2_FILTER_8", "fc2"}, + {"DAWN_VIR_IR", "vir"}, {"GLL_SSI_PLATFORM", "galileo"}, {"HAYABUSA_AMICA", "amica"}, {"HAYABUSA_NIRS", "nirs"}, @@ -60,9 +63,11 @@ namespace SpiceQL { {"LRO_MINIRF", "minirf"}, {"M10_VIDICON_A", "m10_vidicon_a"}, {"M10_VIDICON_B", "m10_vidicon_b"}, + {"VIDICON_A", "m10_vidicon_a"}, {"MARS", "mro"}, {"MSGR_MDIS_WAC", "mdis"}, {"MSGR_MDIS_NAC", "mdis"}, + {"MERCURY DUAL IMAGING SYSTEM NARROW ANGLE CAMERA", "mdis"}, {"MEX_HRSC_HEAD", "hrsc"}, {"MEX_HRSC_SRC", "src"}, {"MEX_HRSC_IR", "hrsc"}, @@ -71,15 +76,22 @@ namespace SpiceQL { {"MGS_MOC_WA_BLUE", "mgs"}, {"MRO_MARCI_VIS", "marci"}, {"MRO_MARCI_UV", "marci"}, + {"MRO_MARCI_BASE", "marci"}, {"MRO_CTX", "ctx"}, {"MRO_HIRISE", "hirise"}, {"MRO_HIRISE_LOOK_DIRECTION", "hirise"}, {"MRO_CRISM_VNIR", "crism"}, - {"NEAR EARTH ASTEROID RENDEZVOUS", ""}, + {"NEAR EARTH ASTEROID RENDEZVOUS", "near"}, + {"NEAR_MSI", "msi"}, {"NH_LORRI", "lorri"}, + {"NH_LORRI_1X1", "lorri"}, {"NH_RALPH_LEISA", "leisa"}, {"NH_MVIC", "mvic_tdi"}, + {"ISIS_NH_RALPH_LEISA", "leisa"}, {"ISIS_NH_RALPH_MVIC_METHANE", "mvic_framing"}, + {"ISIS_NH_RALPH_MVIC_FT", "mvic_framing"}, + {"M01_THEMIS_IR", "odyssey"}, + {"M01_THEMIS_VIS", "odyssey"}, {"THEMIS_IR", "odyssey"}, {"THEMIS_VIS", "odyssey"}, {"LISM_MI-VIS1", "kaguya"}, @@ -104,16 +116,27 @@ namespace SpiceQL { {"LISM_TC1_SDH", "kaguya"}, {"LISM_TC1_STH", "kaguya"}, {"LISM_TC1_SSH", "kaguya"}, + {"SELENE", "kaguya"}, + {"KAGUYA", "kaguya"}, + {"SELENE MAIN ORBITER", "kaguya"}, {"LO1_HIGH_RESOLUTION_CAMERA", "lo"}, {"LO2_HIGH_RESOLUTION_CAMERA", "lo"}, {"LO3_HIGH_RESOLUTION_CAMERA", "lo"}, {"LO4_HIGH_RESOLUTION_CAMERA", "lo"}, {"LO5_HIGH_RESOLUTION_CAMERA", "lo"}, + {"LO3_MED_RESOLUTION_CAMERA", "lo"}, {"NEPTUNE", "voyager1"}, {"SATURN", "voyager1"}, {"TGO_CASSIS", "cassis"}, + {"TGO_CASSIS_CRU", "cassis"}, {"VIKING ORBITER 1", "viking1"}, + {"VIKING_ORBITER_1", "viking1"}, {"VIKING ORBITER 2", "viking2"}, + {"VIKING_ORBITER_2", "viking2"}, + {"VO2_VISB", "viking2"}, + {"VO2_VISA", "viking2"}, + {"VO1_VISA", "viking1"}, + {"VO1_VISB", "viking1"}, {"VG1_ISSNA", "voyager1"}, {"VG1_ISSWA", "voyager1"}, {"VG2_ISSNA", "voyager2"}, @@ -123,8 +146,14 @@ namespace SpiceQL { {"High Resolution Camera", "clementine1"}, {"Long Wave Infrared Camera", "clementine1"}, {"Visual and Infrared Spectrometer", "vir"}, + {"CLEM_HIRES", "clementine1"}, + {"CLEM_UVVIS_A", "uvvis"}, + {"CLEM_LWIR", "clementine1"}, + {"CLEM_NIR", "nir"}, {"CH2", "chandrayaan2"}, - {"CH-2", "chandrayaan2"} + {"CH-2", "chandrayaan2"}, + {"ROS_VIRTIS-M_IR", "virtis"}, + {"MSL_MASTCAM_LEFT", "msl"} }; /** From d2a1373f422ae84866e09b6ef451b1c29e6579ec Mon Sep 17 00:00:00 2001 From: Christine Kim Date: Fri, 3 Apr 2026 16:09:02 -0400 Subject: [PATCH 9/9] remove ck_writer --- CHANGELOG.md | 2 +- SpiceQL/include/io.h | 25 ------ SpiceQL/include/utils.h | 10 +++ SpiceQL/src/io.cpp | 142 +++++++++------------------------ SpiceQL/src/utils.cpp | 14 ++++ bindings/python/CMakeLists.txt | 3 - bindings/python/ck_writer.py | 136 ------------------------------- bindings/python/io.i | 1 - 8 files changed, 64 insertions(+), 269 deletions(-) delete mode 100644 bindings/python/ck_writer.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ae87a79c..646f7c9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ release. ### Added - Added getTargetStatesRanged so a request can be made with start, end, and range of ETs instead of a list [#115](https://github.com/DOI-USGS/SpiceQL/pull/115) -- Added ck_writer.py and additional support for ISD to kernel generation [#116](https://github.com/DOI-USGS/SpiceQL/pull/116) +- Added additional support for ISD to kernel generation [#116](https://github.com/DOI-USGS/SpiceQL/pull/116) ### 1.2.7 diff --git a/SpiceQL/include/io.h b/SpiceQL/include/io.h index 9b461b9e..d07f3b61 100644 --- a/SpiceQL/include/io.h +++ b/SpiceQL/include/io.h @@ -208,28 +208,3 @@ namespace SpiceQL { void writeTextKernel(std::string fileName, std::string type, nlohmann::json &keywords, std::string comment = ""); } - -#ifdef __cplusplus -extern "C" { -#endif -/** Returns 0 on success, -1 on error. On error, call writeCkFromBuffersLastError() for the message. */ -int writeCkFromBuffers( - const char* path, - const double* quats, - size_t n_quats, - const double* times, - size_t n_times, - int bodyCode, - const char* referenceFrame, - const char* segmentId, - const char* sclk, - const char* lsk, - const double* av, - size_t n_av, - const char* comment -); -/** Last error message from writeCkFromBuffers (valid until next writeCk call). */ -const char* writeCkFromBuffersLastError(void); -#ifdef __cplusplus -} -#endif diff --git a/SpiceQL/include/utils.h b/SpiceQL/include/utils.h index 976e3564..f5572ea1 100644 --- a/SpiceQL/include/utils.h +++ b/SpiceQL/include/utils.h @@ -61,6 +61,16 @@ namespace SpiceQL { std::string replaceAll(std::string str, const std::string &from, const std::string &to); + /** + * @brief turn a string into a vector with a deliminator + * + * @param s input string + * @param delim char deliminator + * @return std::vector + */ + std::vector split(const std::string& s, char delim); + + /** * @brief glob, but with json * diff --git a/SpiceQL/src/io.cpp b/SpiceQL/src/io.cpp index 4300b9b0..921812a5 100644 --- a/SpiceQL/src/io.cpp +++ b/SpiceQL/src/io.cpp @@ -78,7 +78,7 @@ namespace SpiceQL { this->angularVelocities = angularVelocities; this->comment = comment; } - + void writeCk(string path, vector> quats, @@ -94,12 +94,19 @@ namespace SpiceQL { SpiceInt handle; // convert times, but first, we need SCLK+LSK kernels - Kernel sclkKernel(sclk); - Kernel lskKernel(lsk); + // allow and furnish multiple sclks + std::vector> sclkKernels; + if (!sclk.empty()) { + for (const std::string& sclkPath : split(sclk, ',')) + sclkKernels.push_back(std::make_unique(sclkPath)); + } + Kernel lskKernel(lsk); + for(auto &et : times) { double sclkdp; checkNaifErrors(); + sce2c_c(bodyCode/1000, et, &sclkdp); checkNaifErrors(); et = sclkdp; } @@ -112,6 +119,33 @@ namespace SpiceQL { ckopn_c(path.c_str(), "CK", comment.size(), &handle); checkNaifErrors(); + // Flatten the nested quaternions into a single contiguous array. + // CSPICE functions (like ckw03_c) are C-based and expect a pointer to a + // contiguous block of memory (SpiceDouble quats[][4]). + // A std::vector> stores inner vectors in fragmented + // locations on the heap; we must "flatten" them into a single ribbon + // of doubles so that pointer arithmetic works correctly inside SPICE. + vector flatQuats; + flatQuats.reserve(quats.size() * 4); + for (const auto& q : quats) { + for (double d : q) { + flatQuats.push_back(d); + } + } + + // Flatten angular velocities if they exist. + // Similar to quaternions, AV data must be contiguous (SpiceDouble av[][3]). + // We check if the input nested vector is non-empty before proceeding. + vector flatAv; + if (!angularVelocities.empty()) { + flatAv.reserve(angularVelocities.size() * 3); + for (const auto& a : angularVelocities) { + for (double d : a) { + flatAv.push_back(d); + } + } + } + ckw03_c (handle, times.at(0), times.at(times.size()-1), @@ -121,8 +155,8 @@ namespace SpiceQL { segmentId.c_str(), times.size(), times.data(), - quats.data(), - (!angularVelocities.empty()) ? angularVelocities.data() : nullptr, + flatQuats.data(), + (!angularVelocities.empty()) ? flatAv.data() : nullptr, times.size(), times.data()); checkNaifErrors(); @@ -380,102 +414,4 @@ namespace SpiceQL { SPDLOG_TRACE("Text kernel written to {}", fileName); } - namespace { - thread_local std::string g_writeCkFromBuffersLastError; - - std::vector split(const std::string& s, char delim) { - std::vector out; - std::istringstream ss(s); - std::string part; - while (std::getline(ss, part, delim)) { - auto start = part.find_first_not_of(" \t"); - if (start == std::string::npos) continue; - auto end = part.find_last_not_of(" \t"); - out.push_back(part.substr(start, end == std::string::npos ? part.size() : end - start + 1)); - } - return out; - } - } - - extern "C" int writeCkFromBuffers( - const char* path, - const double* quats, - size_t n_quats, - const double* times, - size_t n_times, - int bodyCode, - const char* referenceFrame, - const char* segmentId, - const char* sclk, - const char* lsk, - const double* av, - size_t n_av, - const char* comment - ) { - g_writeCkFromBuffersLastError.clear(); - if (path == nullptr || quats == nullptr || times == nullptr || n_quats == 0 || n_times == 0) { - g_writeCkFromBuffersLastError = "writeCkFromBuffers: path/quats/times must be non-null and non-empty."; - return -1; - } - try { - std::string commentStr(comment ? comment : ""); - if (commentStr.empty()) commentStr = "CK Kernel"; - - // sclk: single path or comma-separated list; must keep Kernel objects alive or destructors unload them - std::vector> sclkKernels; - if (sclk && *sclk) { - for (const std::string& sclkPath : split(sclk, ',')) - sclkKernels.push_back(std::make_unique(sclkPath)); - } - Kernel lskKernel(lsk ? lsk : ""); - - int clockId = bodyCode / 1000; - std::vector sclkTimes(n_times); - for (size_t i = 0; i < n_times; i++) { - double sclkdp; - checkNaifErrors(); - sce2c_c(clockId, times[i], &sclkdp); - checkNaifErrors(); - sclkTimes[i] = sclkdp; - } - checkNaifErrors(); - - SpiceInt handle; - ckopn_c(path, "CK", (SpiceInt)commentStr.size(), &handle); - checkNaifErrors(); - ckw03_c( - handle, - sclkTimes[0], - sclkTimes[n_times - 1], - bodyCode, - referenceFrame ? referenceFrame : "", - (av != nullptr && n_av > 0) ? SPICETRUE : SPICEFALSE, - segmentId ? segmentId : "", - (SpiceInt)n_times, sclkTimes.data(), - quats, - (av != nullptr && n_av > 0) ? av : nullptr, - (SpiceInt)n_times, - sclkTimes.data() - ); - checkNaifErrors(); - - ckcls_c(handle); - checkNaifErrors(); - writeComment(path, commentStr); - return 0; - } catch (const std::exception& e) { - g_writeCkFromBuffersLastError = e.what(); - reset_c(); - return -1; - } catch (...) { - g_writeCkFromBuffersLastError = "writeCkFromBuffers: unknown exception"; - reset_c(); - return -1; - } - } - - extern "C" const char* writeCkFromBuffersLastError(void) { - return g_writeCkFromBuffersLastError.c_str(); - } - } diff --git a/SpiceQL/src/utils.cpp b/SpiceQL/src/utils.cpp index e113880d..60a96021 100644 --- a/SpiceQL/src/utils.cpp +++ b/SpiceQL/src/utils.cpp @@ -111,6 +111,20 @@ namespace SpiceQL { } + vector split(const string& s, char delim) { + vector out; + istringstream ss(s); + string part; + while (std::getline(ss, part, delim)) { + auto start = part.find_first_not_of(" \t"); + if (start == string::npos) continue; + auto end = part.find_last_not_of(" \t"); + out.push_back(part.substr(start, end == string::npos ? part.size() : end - start + 1)); + } + return out; + } + + vector> getPathsFromRegex(string root, vector regexes) { vector files_to_search = Memo::ls(root, true); diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index 5415744f..a0a915f3 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -44,9 +44,6 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in configure_file(${CMAKE_CURRENT_SOURCE_DIR}/__init__.py ${PYSPICEQL_OUTPUT_DIR}/__init__.py COPYONLY) -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ck_writer.py - ${PYSPICEQL_OUTPUT_DIR}/ck_writer.py - COPYONLY) # Setup to run setup tools on install install(CODE "execute_process(COMMAND pip install . WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})") diff --git a/bindings/python/ck_writer.py b/bindings/python/ck_writer.py deleted file mode 100644 index 641f448d..00000000 --- a/bindings/python/ck_writer.py +++ /dev/null @@ -1,136 +0,0 @@ -import ctypes -import numpy as np -import os -import sys - -def _find_lib(): - # Load library - try: - from . import _pyspiceql as mod - - lib = ctypes.CDLL(mod.__file__) - if hasattr(lib, "writeCkFromBuffers"): - return lib, mod.__file__, [] - except (ImportError, AttributeError, OSError) as e: - print(f"DEBUG: Internal import failed: {e}") - pass - - # Manual search and load library - try: - current_dir = os.path.dirname(os.path.abspath(__file__)) - # Look for the .so or .dylib file in the same folder as ck_writer.py - for f in os.listdir(current_dir): - if f.startswith("_pyspiceql") and (f.endswith(".so") or f.endswith(".dylib")): - path = os.path.join(current_dir, f) - lib = ctypes.CDLL(path) - if hasattr(lib, "writeCkFromBuffers"): - return lib, path, [] - except Exception as e: - print(f"DEBUG: Manual directory search failed: {e}") - - return None, None, [] - - -_ck_lib, _ck_lib_path, _ck_lib_search_dirs = _find_lib() - -if _ck_lib is not None: - try: - _ck_lib.writeCkFromBuffers.argtypes = [ - ctypes.c_char_p, - ctypes.POINTER(ctypes.c_double), - ctypes.c_size_t, - ctypes.POINTER(ctypes.c_double), - ctypes.c_size_t, - ctypes.c_int, - ctypes.c_char_p, - ctypes.c_char_p, - ctypes.c_char_p, - ctypes.c_char_p, - ctypes.POINTER(ctypes.c_double), - ctypes.c_size_t, - ctypes.c_char_p, - ] - _ck_lib.writeCkFromBuffers.restype = ctypes.c_int - if hasattr(_ck_lib, "writeCkFromBuffersLastError"): - _ck_lib.writeCkFromBuffersLastError.restype = ctypes.c_char_p - _ck_lib.writeCkFromBuffersLastError.argtypes = [] - _ck_lib_ok = True - except AttributeError: - _ck_lib_ok = False -else: - _ck_lib_ok = False - - -def write_ck( - path, - quats, - times, - body_code, - reference_frame, - segment_id, - sclk, - lsk, - angular_velocities=None, - comment="", -): - if not _ck_lib_ok: - dirs = _ck_lib_search_dirs - hint = "" - if dirs: - hint = " Searched: " + ", ".join(dirs) + ". " - raise RuntimeError( - "writeCkFromBuffers not found. Rebuild SpiceQL and ensure libSpiceQL is on the library path." - + hint - ) - - quats = np.ascontiguousarray(np.asarray(quats, dtype=np.float64)) - times = np.ascontiguousarray(np.asarray(times, dtype=np.float64)) - n = quats.shape[0] - if quats.ndim != 2 or quats.shape[1] != 4: - raise ValueError("quats must have shape (n, 4)") - if times.shape[0] != n: - raise ValueError("times length must match quats rows") - - if angular_velocities is not None and len(angular_velocities) != 0: - angular_velocities = np.ascontiguousarray(np.asarray(angular_velocities, dtype=np.float64)) - if angular_velocities.shape[0] != n or angular_velocities.shape[1] != 3: - raise ValueError("angular_velocities must have shape (n, 3)") - av_ptr = angular_velocities.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) - n_av = n - else: - av_ptr = None - n_av = 0 - - def _b(s): - """Convert value to bytes""" - if s is None: - return b"" - if isinstance(s, str): - return s.encode("utf-8") - return str(s).encode("utf-8") - - # Accept str or list of str (multiple SCLK kernels) for sclk, comma deliminated - sclk_arg = ",".join(sclk) if isinstance(sclk, (list, tuple)) else sclk - - rc = _ck_lib.writeCkFromBuffers( - _b(path), - quats.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - n, - times.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - n, - int(body_code), - _b(reference_frame), - _b(segment_id), - _b(sclk_arg), - _b(lsk), - av_ptr, - n_av, - _b(comment), - ) - if rc != 0: - err = "unknown error" - if hasattr(_ck_lib, "writeCkFromBuffersLastError"): - msg = _ck_lib.writeCkFromBuffersLastError() - if msg: - err = msg.decode("utf-8") if isinstance(msg, bytes) else msg - raise RuntimeError(f"writeCkFromBuffers failed: {err}") diff --git a/bindings/python/io.i b/bindings/python/io.i index da5b773d..decff1ee 100644 --- a/bindings/python/io.i +++ b/bindings/python/io.i @@ -4,5 +4,4 @@ #include "io.h" %} -%ignore writeCkFromBuffers; %include "io.h" \ No newline at end of file