resolve-core 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
1
+ cmake_minimum_required(VERSION 3.18)
2
+
3
+ # Check if we're being included as a subdirectory or built standalone
4
+ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
5
+ # Standalone build - define project and build resolve_core.
6
+ # No VERSION here: the wheel's version comes from pyproject.toml, the
7
+ # engine's from resolve::VERSION, and both are kept in sync with the
8
+ # repo-root VERSION file by tools/version.py. Nothing consumes
9
+ # PROJECT_VERSION, and this directory is also the sdist root, which does
10
+ # not carry the repo-root file.
11
+ project(resolve_python LANGUAGES CXX)
12
+
13
+ # CPU by default: the PyPI wheels link the CPU libtorch the user's `torch`
14
+ # ships and do not bundle CUDA. Pass -DUSE_CUDA=ON for a local CUDA build.
15
+ option(USE_CUDA "Enable CUDA support" OFF)
16
+
17
+ # libtorch 2.13+ requires C++20 (bit-field default initializers in
18
+ # c10/core/AutogradState.h). Match the parent src/core/CMakeLists.txt.
19
+ set(CMAKE_CXX_STANDARD 20)
20
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
21
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
22
+
23
+ # Skip Caffe2's CUDA detection entirely when building CPU-only
24
+ if(NOT USE_CUDA)
25
+ set(CAFFE2_USE_CUDA OFF CACHE BOOL "" FORCE)
26
+ set(USE_CUDA OFF CACHE BOOL "" FORCE)
27
+ set(CAFFE2_USE_CUDNN OFF CACHE BOOL "" FORCE)
28
+ set(USE_CUDNN OFF CACHE BOOL "" FORCE)
29
+ # This is the key: tell Torch not to look for CUDA
30
+ set(TORCH_CUDA_ARCH_LIST "" CACHE STRING "" FORCE)
31
+ set(CMAKE_CUDA_COMPILER "" CACHE FILEPATH "" FORCE)
32
+ else()
33
+ # Local CUDA wheel build: link PyTorch's prebuilt CUDA libs (we do not
34
+ # compile CUDA ourselves except the custom kernels below). cuDNN off to
35
+ # match the engine build.
36
+ set(CAFFE2_USE_CUDA ON)
37
+ set(CAFFE2_USE_CUDNN OFF)
38
+ set(USE_CUDNN OFF)
39
+ endif()
40
+
41
+ # Locate libtorch via the installed `torch` package's CMake prefix when no
42
+ # explicit Torch_DIR / CMAKE_PREFIX_PATH was given. This is how the PyPI
43
+ # wheel and sdist builds find libtorch: `torch` is a build dependency
44
+ # (pyproject.toml), so it is present in the build environment, and the
45
+ # extension links the same libtorch the user's torch ships -- no separate
46
+ # libtorch is downloaded or bundled.
47
+ if(NOT Torch_DIR AND NOT CMAKE_PREFIX_PATH)
48
+ find_package(Python COMPONENTS Interpreter REQUIRED)
49
+ execute_process(
50
+ COMMAND "${Python_EXECUTABLE}" -c "import torch.utils; print(torch.utils.cmake_prefix_path)"
51
+ OUTPUT_VARIABLE TORCH_CMAKE_PREFIX
52
+ OUTPUT_STRIP_TRAILING_WHITESPACE
53
+ RESULT_VARIABLE TORCH_PROBE_RC
54
+ )
55
+ if(TORCH_PROBE_RC EQUAL 0 AND TORCH_CMAKE_PREFIX)
56
+ list(APPEND CMAKE_PREFIX_PATH "${TORCH_CMAKE_PREFIX}")
57
+ message(STATUS "Using libtorch from the torch package: ${TORCH_CMAKE_PREFIX}")
58
+ endif()
59
+ endif()
60
+
61
+ # Find libtorch (will skip CUDA if the CPU-only vars above are set)
62
+ find_package(Torch REQUIRED)
63
+
64
+ # Core library sources: single source of truth shared with the main engine
65
+ # build via resolve_core_sources.cmake (no drift -- this is why the wheel
66
+ # previously failed to link set_vram_fraction / install_crash_handler).
67
+ set(RESOLVE_CORE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..)
68
+ include(${RESOLVE_CORE_DIR}/resolve_warnings.cmake)
69
+ include(${RESOLVE_CORE_DIR}/resolve_core_sources.cmake)
70
+
71
+ add_library(resolve_core STATIC ${RESOLVE_CORE_SOURCES})
72
+ target_include_directories(resolve_core PUBLIC
73
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
74
+ )
75
+ target_link_libraries(resolve_core PUBLIC ${TORCH_LIBRARIES})
76
+ target_link_libraries(resolve_core PRIVATE resolve_warnings)
77
+
78
+ # Custom CUDA kernels + RESOLVE_HAS_CUDA, shared with the engine build. Under
79
+ # -DUSE_CUDA=ON this defines RESOLVE_HAS_CUDA so set_vram_fraction and the
80
+ # hash kernels are actually compiled in; a no-op on the default CPU wheel.
81
+ include(${RESOLVE_CORE_DIR}/resolve_cuda_kernels.cmake)
82
+
83
+ # OpenMP for fuzzy::query_batch (optional).
84
+ find_package(OpenMP QUIET)
85
+ if(OpenMP_CXX_FOUND)
86
+ target_link_libraries(resolve_core PUBLIC OpenMP::OpenMP_CXX)
87
+ message(STATUS "OpenMP enabled for resolve_core (standalone Python build)")
88
+ else()
89
+ message(STATUS "OpenMP not found; fuzzy::query_batch will run serially")
90
+ endif()
91
+
92
+ set_property(TARGET resolve_core PROPERTY CXX_STANDARD 20)
93
+ endif()
94
+
95
+ # Warning flags for RESOLVE's own targets. Already included by the branch above
96
+ # when this directory configures standalone (the sdist root) and by
97
+ # src/core/CMakeLists.txt when it is a subdirectory; the file guards itself.
98
+ include(${CMAKE_CURRENT_SOURCE_DIR}/../resolve_warnings.cmake)
99
+
100
+ # Python bindings using nanobind
101
+ find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
102
+
103
+ # Fetch nanobind
104
+ include(FetchContent)
105
+ FetchContent_Declare(
106
+ nanobind
107
+ GIT_REPOSITORY https://github.com/wjakob/nanobind.git
108
+ GIT_TAG v2.4.0
109
+ )
110
+ FetchContent_MakeAvailable(nanobind)
111
+
112
+ # Build the extension module
113
+ set(BINDING_SOURCES
114
+ src/bindings.cpp
115
+ src/bindings_enums.cpp
116
+ src/bindings_types.cpp
117
+ src/bindings_dataset.cpp
118
+ src/bindings_model.cpp
119
+ src/bindings_trainer.cpp
120
+ src/bindings_metrics.cpp
121
+ src/bindings_pretraining.cpp
122
+ src/bindings_fuzzy.cpp
123
+ )
124
+ nanobind_add_module(_resolve_core ${BINDING_SOURCES})
125
+
126
+ # Find torch_python library for Python tensor interop (THPVariable_Wrap, etc.)
127
+ get_filename_component(TORCH_LIB_DIR "${TORCH_INSTALL_PREFIX}/lib" ABSOLUTE)
128
+ find_library(TORCH_PYTHON_LIB torch_python PATHS "${TORCH_LIB_DIR}" NO_DEFAULT_PATH)
129
+ if(TORCH_PYTHON_LIB)
130
+ message(STATUS "Found torch_python: ${TORCH_PYTHON_LIB}")
131
+ target_link_libraries(_resolve_core PRIVATE resolve_core ${TORCH_LIBRARIES} ${TORCH_PYTHON_LIB})
132
+ else()
133
+ message(WARNING "torch_python library not found, some features may not work")
134
+ target_link_libraries(_resolve_core PRIVATE resolve_core ${TORCH_LIBRARIES})
135
+ endif()
136
+
137
+ target_link_libraries(_resolve_core PRIVATE resolve_warnings)
138
+
139
+ # nanobind reaches the binding sources through a FetchContent target, which is
140
+ # not IMPORTED, so CMake does not mark its headers SYSTEM the way it does
141
+ # libtorch's. Promote them, otherwise /W4 and -Wall report nanobind's internals
142
+ # instead of our bindings. Which target carries them depends on the
143
+ # nanobind_add_module mode (static / shared / abi3 variants), so read it off
144
+ # the link line rather than hardcoding a name.
145
+ get_target_property(RESOLVE_NB_LINKED _resolve_core LINK_LIBRARIES)
146
+ foreach(RESOLVE_NB_LIB IN LISTS RESOLVE_NB_LINKED)
147
+ if(RESOLVE_NB_LIB MATCHES "^nanobind" AND TARGET ${RESOLVE_NB_LIB})
148
+ get_target_property(RESOLVE_NB_INCS ${RESOLVE_NB_LIB} INTERFACE_INCLUDE_DIRECTORIES)
149
+ if(RESOLVE_NB_INCS)
150
+ set_property(TARGET ${RESOLVE_NB_LIB} APPEND PROPERTY
151
+ INTERFACE_SYSTEM_INCLUDE_DIRECTORIES ${RESOLVE_NB_INCS})
152
+ endif()
153
+ endif()
154
+ endforeach()
155
+
156
+ target_include_directories(_resolve_core PRIVATE
157
+ ${CMAKE_CURRENT_SOURCE_DIR}/../include
158
+ )
159
+
160
+ # Set output directory for the built module
161
+ set_target_properties(_resolve_core PROPERTIES
162
+ LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/resolve_core
163
+ )
164
+
165
+ # Runtime libtorch resolution. The wheel deliberately does NOT bundle libtorch
166
+ # -- it links the same libs the user's `torch` ships (a separate copy would be
167
+ # huge and risk an ABI clash). resolve_core/__init__.py imports torch before
168
+ # loading this extension, so torch's libs are already in the process; the RPATH
169
+ # below is a belt-and-braces pointer to the installed torch lib dir
170
+ # (site-packages/torch/lib, one level up from resolve_core/). On Windows, torch
171
+ # adds its lib dir to the DLL search at import, so no RPATH/copy is needed.
172
+ if(UNIX AND NOT APPLE)
173
+ set_target_properties(_resolve_core PROPERTIES
174
+ INSTALL_RPATH "$ORIGIN/../torch/lib"
175
+ BUILD_WITH_INSTALL_RPATH TRUE)
176
+ elseif(APPLE)
177
+ set_target_properties(_resolve_core PROPERTIES
178
+ INSTALL_RPATH "@loader_path/../torch/lib"
179
+ BUILD_WITH_INSTALL_RPATH TRUE)
180
+ # Linking against libtorch_python pulls libtorch's bundled pybind11 into the
181
+ # module (via torch/csrc/autograd/python_variable.h in bindings_model.cpp),
182
+ # which references Python C-API symbols (PyInstanceMethod_*, _PyThreadState_
183
+ # UncheckedGet) not in nanobind's restricted macOS symbol list. A Python
184
+ # extension on macOS does not link libpython; those symbols resolve at load
185
+ # time from the host interpreter, so allow undefined-symbol dynamic lookup
186
+ # (the canonical macOS Python-extension link flag). Without it the arm64 link
187
+ # fails with "Undefined symbols for architecture arm64".
188
+ target_link_options(_resolve_core PRIVATE -Wl,-undefined,dynamic_lookup)
189
+ endif()
190
+
191
+ # Install target for scikit-build-core
192
+ install(TARGETS _resolve_core LIBRARY DESTINATION resolve_core)
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.2
2
+ Name: resolve-core
3
+ Version: 0.8.1
4
+ Summary: RESOLVE C++ engine: predict sample outcomes from compositional data via learned representations
5
+ Author-Email: Gilles Colling <gilles.colling051@gmail.com>
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: C++
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Project-URL: Homepage, https://github.com/gcol33/resolve
18
+ Project-URL: Repository, https://github.com/gcol33/resolve
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: numpy>=1.20.0
21
+ Requires-Dist: pandas>=1.3.0
22
+ Requires-Dist: torch>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
26
+ Description-Content-Type: text/markdown
27
+
28
+ # resolve-core
29
+
30
+ C++ bindings for the RESOLVE compositional-data prediction engine.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install resolve-core
36
+ ```
37
+
38
+ ## Requirements
39
+
40
+ - Python >= 3.9
41
+ - PyTorch/libtorch
42
+ - CMake >= 3.18
43
+
44
+ ## Usage
45
+
46
+ ```python
47
+ from resolve_core import Trainer, TrainConfig, ModelConfig
48
+
49
+ # See full package documentation for API details
50
+ ```
@@ -0,0 +1,23 @@
1
+ # resolve-core
2
+
3
+ C++ bindings for the RESOLVE compositional-data prediction engine.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install resolve-core
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - Python >= 3.9
14
+ - PyTorch/libtorch
15
+ - CMake >= 3.18
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from resolve_core import Trainer, TrainConfig, ModelConfig
21
+
22
+ # See full package documentation for API details
23
+ ```
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ # torch is a build requirement: the extension links libtorch, located via the
3
+ # installed torch package's CMake prefix (src/core/python/CMakeLists.txt).
4
+ requires = ["scikit-build-core>=0.4.3", "nanobind>=2.0.0", "torch>=2.0"]
5
+ build-backend = "scikit_build_core.build"
6
+
7
+ [project]
8
+ name = "resolve-core"
9
+ version = "0.8.1"
10
+ description = "RESOLVE C++ engine: predict sample outcomes from compositional data via learned representations"
11
+ readme = "README.md"
12
+ license = {text = "MIT"}
13
+ requires-python = ">=3.9"
14
+ authors = [
15
+ {name = "Gilles Colling", email = "gilles.colling051@gmail.com"}
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: C++",
27
+ "Topic :: Scientific/Engineering",
28
+ ]
29
+ dependencies = [
30
+ "numpy>=1.20.0",
31
+ "pandas>=1.3.0",
32
+ # The extension links libtorch and imports torch before loading; the wheels
33
+ # do not bundle libtorch, so torch must be installed at runtime.
34
+ "torch>=2.0",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=7.0.0",
40
+ "pytest-cov>=4.0.0",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/gcol33/resolve"
45
+ Repository = "https://github.com/gcol33/resolve"
46
+
47
+ [tool.scikit-build]
48
+ # cmake.version, not cmake.minimum-version: scikit-build-core >= 0.8 rejects the
49
+ # old key outright, which fails metadata generation before any build starts.
50
+ cmake.version = ">=3.18"
51
+ cmake.build-type = "Release"
52
+ wheel.packages = ["src/resolve_core"]
53
+
54
+ [tool.scikit-build.cmake.define]
55
+ BUILD_PYTHON = "ON"
56
+ BUILD_TESTS = "OFF"
@@ -0,0 +1,118 @@
1
+ // Main binding entry point - delegates to split modules for maintainability
2
+ //
3
+ // The actual binding code is split into:
4
+ // - bindings_enums.cpp: All enum bindings
5
+ // - bindings_types.cpp: Configuration structs and result types
6
+ // - bindings_dataset.cpp: ResolveDataset and species encoding
7
+ // - bindings_model.cpp: ResolveModel
8
+ // - bindings_trainer.cpp: Trainer and Predictor
9
+ // - bindings_metrics.cpp: Metrics classes
10
+
11
+ #include "bindings_common.hpp"
12
+
13
+ #include <string>
14
+
15
+ NB_MODULE(_resolve_core, m) {
16
+ m.doc() = "RESOLVE C++ core library for species-composition based prediction";
17
+
18
+ // libtorch holds global state (type registrations, dispatch keys) via shared_ptr
19
+ // that outlives nanobind's module cleanup at interpreter shutdown. This causes
20
+ // false-positive "leaked N types / N functions" warnings. Instance leaks are
21
+ // fixed by returning config objects by value (not reference) in property bindings.
22
+ nb::set_leak_warnings(false);
23
+
24
+ // Register all bindings from split modules
25
+ register_enums(m);
26
+ register_types(m);
27
+ register_dataset(m);
28
+ register_model(m);
29
+ register_trainer(m);
30
+ register_metrics(m);
31
+ register_pretraining(m);
32
+ register_fuzzy(m);
33
+
34
+ // Platform-aware PYTORCH_CUDA_ALLOC_CONF setter. The primary surface is
35
+ // resolve_core.configure_cuda_allocator() in the Python __init__ which
36
+ // runs at module import (pre-torch). This C++ entry point is the
37
+ // late-bound fallback for users who imported torch first; in that case
38
+ // the allocator has already initialized and changing the env var no
39
+ // longer affects allocator behavior. Returns the resulting config
40
+ // string for logging.
41
+ m.def(
42
+ "_configure_cuda_allocator_native",
43
+ &resolve::configure_cuda_allocator,
44
+ nb::arg("force") = false,
45
+ "Set PYTORCH_CUDA_ALLOC_CONF if unset (or force-set). Linux/macOS\n"
46
+ "prepend expandable_segments:True; Windows omits it. Returns the\n"
47
+ "active value. Use resolve_core.configure_cuda_allocator() instead;\n"
48
+ "this native shim is late-bound and cannot rescue allocators that\n"
49
+ "have already initialized."
50
+ );
51
+
52
+ // Top-level helper: cap the PyTorch CUDA caching allocator at a fraction
53
+ // of device VRAM. Standalone counterpart to TrainConfig.vram_fraction for
54
+ // users running Predictor-only workflows or wanting to apply the cap
55
+ // before constructing any RESOLVE object.
56
+ m.def(
57
+ "set_vram_fraction",
58
+ [](double fraction, int device_index) {
59
+ resolve::set_vram_fraction(fraction, device_index);
60
+ },
61
+ nb::arg("fraction"),
62
+ nb::arg("device_index") = -1,
63
+ "Cap the PyTorch CUDA caching allocator at `fraction` of device VRAM.\n"
64
+ "fraction must be in (0, 1]; 1.0 disables the cap. device_index = -1\n"
65
+ "uses the current CUDA device. No-op on CPU-only builds or when no\n"
66
+ "CUDA device is present."
67
+ );
68
+
69
+ // Pin libtorch's host thread pools. Standalone counterpart used at startup
70
+ // to avoid worker-thread teardown races on Windows (issue #18); harmless
71
+ // elsewhere. <=0 keeps libtorch's default for the corresponding pool.
72
+ m.def(
73
+ "set_thread_pools",
74
+ [](int intraop_threads, int interop_threads) {
75
+ resolve::set_thread_pools(intraop_threads, interop_threads);
76
+ },
77
+ nb::arg("intraop_threads"),
78
+ nb::arg("interop_threads") = -1,
79
+ "Pin libtorch's intra-op / inter-op thread pools (<=0 keeps the\n"
80
+ "default). Best-effort; call at startup before the first op."
81
+ );
82
+
83
+ // Windows crash hardening (issue #19): convert an unhandled native fault in
84
+ // a headless training worker into an immediate TerminateProcess with the
85
+ // fault's NTSTATUS, instead of an indefinite Windows-Error-Reporting /
86
+ // JIT-debugger (vsjitdebugger) hang that holds the GPU and stalls the
87
+ // batch. No-op off Windows. Auto-installed at import below; also exposed so
88
+ // callers can re-arm or adjust the shutdown exit code explicitly.
89
+ m.def(
90
+ "install_crash_handler",
91
+ [](int shutdown_exit_code) {
92
+ resolve::install_crash_handler(shutdown_exit_code);
93
+ },
94
+ nb::arg("shutdown_exit_code") = 0,
95
+ "Install the Windows unhandled-exception filter that fails fast via\n"
96
+ "TerminateProcess instead of hanging on the JIT debugger. No-op off\n"
97
+ "Windows. Idempotent."
98
+ );
99
+
100
+ // Internal: flip the crash handler to treat a subsequent native fault as a
101
+ // benign teardown artifact (exit with the shutdown code, not a failure
102
+ // code). Registered with atexit() in resolve_core/__init__.py so a clean
103
+ // interpreter shutdown after a successful run is not misreported as a crash.
104
+ m.def(
105
+ "_signal_work_complete",
106
+ []() { resolve::signal_work_complete(); },
107
+ "Mark all engine work complete (atexit hook; see __init__.py)."
108
+ );
109
+
110
+ // Arm the crash handler as soon as resolve_core is imported, so a training
111
+ // worker is hardened before Trainer::fit() ever runs (issue #19). Default
112
+ // shutdown code 0: a clean interpreter shutdown (after _signal_work_complete
113
+ // via atexit) exits 0; a mid-run native fault exits with its NTSTATUS.
114
+ resolve::install_crash_handler(0);
115
+
116
+ // Version
117
+ m.attr("__version__") = resolve::VERSION;
118
+ }
@@ -0,0 +1,79 @@
1
+ #pragma once
2
+
3
+ #include <nanobind/nanobind.h>
4
+ #include <nanobind/stl/string.h>
5
+ #include <nanobind/stl/vector.h>
6
+ #include <nanobind/stl/pair.h>
7
+ #include <nanobind/stl/unordered_map.h>
8
+ #include <nanobind/stl/optional.h>
9
+ #include <torch/torch.h>
10
+ #include <torch/csrc/autograd/python_variable.h> // For THPVariable_Wrap/Unpack
11
+
12
+ #include "resolve/resolve.hpp"
13
+ #include "resolve/role_mapping.hpp"
14
+ #include "resolve/dataset.hpp"
15
+
16
+ namespace nb = nanobind;
17
+
18
+ // Unpack an optional tensor argument. Returns an undefined tensor for None;
19
+ // THPVariable_Unpack on Py_None reinterprets the None singleton as a
20
+ // THPVariable and reads out of bounds (UB), so callers passing None for an
21
+ // unused input (e.g. genus_ids in hash mode) must be guarded here.
22
+ inline at::Tensor unpack_optional_tensor(const nb::object& obj) {
23
+ if (!obj.is_valid() || obj.is_none()) return at::Tensor();
24
+ return THPVariable_Unpack(obj.ptr());
25
+ }
26
+
27
+ // Unpack a required tensor argument. Raises a clear Python-visible error for
28
+ // None or a non-tensor instead of the UB THPVariable_Unpack would hit.
29
+ inline at::Tensor unpack_required_tensor(const nb::object& obj, const char* name) {
30
+ PyObject* p = obj.ptr();
31
+ if (obj.is_none() || !THPVariable_Check(p)) {
32
+ throw std::invalid_argument(std::string(name) + " must be a tensor");
33
+ }
34
+ return THPVariable_Unpack(p);
35
+ }
36
+
37
+ // Helper to convert Python dict to unordered_map of tensors
38
+ inline std::unordered_map<std::string, torch::Tensor> dict_to_tensor_map(const nb::dict& d) {
39
+ std::unordered_map<std::string, torch::Tensor> result;
40
+ for (auto item : d) {
41
+ // Use THPVariable_Unpack to convert Python tensor to C++ tensor.
42
+ // Reject a non-tensor value loudly instead of silently dropping it,
43
+ // which would make a mistyped targets/inputs entry vanish.
44
+ PyObject* py_tensor = item.second.ptr();
45
+ auto key = nb::cast<std::string>(item.first);
46
+ if (!THPVariable_Check(py_tensor)) {
47
+ throw std::runtime_error(
48
+ "dict_to_tensor_map: value for key '" + key +
49
+ "' is not a torch.Tensor");
50
+ }
51
+ result[key] = THPVariable_Unpack(py_tensor);
52
+ }
53
+ return result;
54
+ }
55
+
56
+ // Helper to convert unordered_map of tensors to Python dict
57
+ inline nb::object tensor_map_to_dict(const std::unordered_map<std::string, torch::Tensor>& m) {
58
+ PyObject* py_dict = PyDict_New();
59
+ for (const auto& [key, value] : m) {
60
+ if (value.defined()) {
61
+ // Move tensor to CPU and make contiguous for Python interop
62
+ auto cpu_tensor = value.detach().cpu().contiguous();
63
+ PyObject* py_tensor = THPVariable_Wrap(cpu_tensor);
64
+ PyDict_SetItemString(py_dict, key.c_str(), py_tensor);
65
+ Py_DECREF(py_tensor); // PyDict_SetItemString increments refcount
66
+ }
67
+ }
68
+ return nb::steal(py_dict);
69
+ }
70
+
71
+ // Forward declarations for binding registration functions
72
+ void register_enums(nb::module_& m);
73
+ void register_types(nb::module_& m);
74
+ void register_dataset(nb::module_& m);
75
+ void register_model(nb::module_& m);
76
+ void register_trainer(nb::module_& m);
77
+ void register_metrics(nb::module_& m);
78
+ void register_pretraining(nb::module_& m);
79
+ void register_fuzzy(nb::module_& m);