From e69b29a259110089cc1e7058524643e9a4057367 Mon Sep 17 00:00:00 2001 From: AndreaRigoni Date: Thu, 5 Mar 2026 09:16:15 +0000 Subject: [PATCH] add first python bindings --- CMakeLists.txt | 3 + conanfile.txt | 1 + src/Python/CMakeLists.txt | 44 +++++++ src/Python/core_bindings.cpp | 30 +++++ src/Python/math_bindings.cpp | 152 +++++++++++++++++++++++++ src/Python/module.cpp | 18 +++ src/Python/testing/core_pybind_test.py | 33 ++++++ src/Python/testing/math_pybind_test.py | 62 ++++++++++ src/Python/testing/pybind_test.py | 46 ++++++++ 9 files changed, 389 insertions(+) create mode 100644 src/Python/CMakeLists.txt create mode 100644 src/Python/core_bindings.cpp create mode 100644 src/Python/math_bindings.cpp create mode 100644 src/Python/module.cpp create mode 100644 src/Python/testing/core_pybind_test.py create mode 100644 src/Python/testing/math_pybind_test.py create mode 100644 src/Python/testing/pybind_test.py diff --git a/CMakeLists.txt b/CMakeLists.txt index c090c3e..c2eda1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,7 @@ include(${ROOT_USE_FILE}) find_package(VTK REQUIRED) # include(${VTK_USE_FILE}) +find_package(pybind11 REQUIRED) option(CENTOS_SUPPORT "VTK definitions for CentOS" OFF) @@ -212,6 +213,8 @@ add_subdirectory(${SRC_DIR}/Root) include_directories(${SRC_DIR}/Vtk) add_subdirectory(${SRC_DIR}/Vtk) +add_subdirectory(${SRC_DIR}/Python) + #add_subdirectory("${SRC_DIR}/utils/make_recipe") ## Documentation and packages diff --git a/conanfile.txt b/conanfile.txt index f8fb77a..8af9f6f 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,6 +1,7 @@ [requires] eigen/3.4.0 boost/1.83.0 +pybind11/3.0.2 [generators] CMakeDeps diff --git a/src/Python/CMakeLists.txt b/src/Python/CMakeLists.txt new file mode 100644 index 0000000..0ef8874 --- /dev/null +++ b/src/Python/CMakeLists.txt @@ -0,0 +1,44 @@ +set(HEADERS "") + +set(SOURCES + module.cpp + core_bindings.cpp + math_bindings.cpp +) + +# Use pybind11 to add the python module +pybind11_add_module(uLib_python module.cpp core_bindings.cpp math_bindings.cpp) + +# Link against our C++ libraries +target_link_libraries(uLib_python PRIVATE + ${PACKAGE_LIBPREFIX}Core + ${PACKAGE_LIBPREFIX}Math +) + +# Include directories from Core and Math are automatically handled if target_include_directories were set appropriately, +# but we might need to manually include them if they aren't INTERFACE includes. +target_include_directories(uLib_python PRIVATE + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_BINARY_DIR} +) + +# --- Python Tests ---------------------------------------------------------- # + +if(BUILD_TESTING) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + add_test(NAME pybind_general + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/pybind_test.py) + set_tests_properties(pybind_general PROPERTIES + ENVIRONMENT "PYTHONPATH=$") + + add_test(NAME pybind_core + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/core_pybind_test.py) + set_tests_properties(pybind_core PROPERTIES + ENVIRONMENT "PYTHONPATH=$") + + add_test(NAME pybind_math + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/math_pybind_test.py) + set_tests_properties(pybind_math PROPERTIES + ENVIRONMENT "PYTHONPATH=$") +endif() diff --git a/src/Python/core_bindings.cpp b/src/Python/core_bindings.cpp new file mode 100644 index 0000000..9e8e19c --- /dev/null +++ b/src/Python/core_bindings.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "Core/Object.h" +#include "Core/Timer.h" +#include "Core/Options.h" +#include "Core/Uuid.h" + +namespace py = pybind11; +using namespace uLib; + +void init_core(py::module_ &m) { + py::class_>(m, "Object") + .def(py::init<>()) + .def("DeepCopy", &Object::DeepCopy); + + py::class_(m, "Timer") + .def(py::init<>()) + .def("Start", &Timer::Start) + .def("StopWatch", &Timer::StopWatch); + + py::class_(m, "Options") + .def(py::init(), py::arg("str") = "Program options") + .def("parse_config_file", py::overload_cast(&Options::parse_config_file)) + .def("save_config_file", &Options::save_config_file) + .def("count", &Options::count); + + py::class_(m, "TypeRegister") + .def_static("Controller", &TypeRegister::Controller, py::return_value_policy::reference_internal); +} diff --git a/src/Python/math_bindings.cpp b/src/Python/math_bindings.cpp new file mode 100644 index 0000000..0f4434f --- /dev/null +++ b/src/Python/math_bindings.cpp @@ -0,0 +1,152 @@ +#include +#include +#include + +#include "Math/Dense.h" +#include "Math/Transform.h" +#include "Math/Geometry.h" +#include "Math/ContainerBox.h" +#include "Math/StructuredData.h" +#include "Math/StructuredGrid.h" +#include "Math/Structured2DGrid.h" +#include "Math/Structured4DGrid.h" +#include "Math/TriangleMesh.h" +#include "Math/VoxRaytracer.h" +#include "Math/Accumulator.h" + +namespace py = pybind11; +using namespace uLib; + +void init_math(py::module_ &m) { + + // Math/Transform.h + py::class_(m, "AffineTransform") + .def(py::init<>()) + .def("GetWorldMatrix", &AffineTransform::GetWorldMatrix) + .def("SetPosition", &AffineTransform::SetPosition) + .def("GetPosition", &AffineTransform::GetPosition) + .def("Translate", &AffineTransform::Translate) + .def("Scale", &AffineTransform::Scale) + .def("SetRotation", &AffineTransform::SetRotation) + .def("GetRotation", &AffineTransform::GetRotation) + .def("Rotate", py::overload_cast(&AffineTransform::Rotate)) + .def("Rotate", py::overload_cast(&AffineTransform::Rotate)) + .def("EulerYZYRotate", &AffineTransform::EulerYZYRotate) + .def("FlipAxes", &AffineTransform::FlipAxes); + + // Math/Geometry.h + py::class_(m, "Geometry") + .def(py::init<>()) + .def("GetWorldPoint", py::overload_cast(&Geometry::GetWorldPoint, py::const_)) + .def("GetLocalPoint", py::overload_cast(&Geometry::GetLocalPoint, py::const_)); + + // Math/ContainerBox.h + py::class_(m, "ContainerBox") + .def(py::init<>()) + .def("SetOrigin", &ContainerBox::SetOrigin) + .def("GetOrigin", &ContainerBox::GetOrigin) + .def("SetSize", &ContainerBox::SetSize) + .def("GetSize", &ContainerBox::GetSize) + .def("GetWorldMatrix", &ContainerBox::GetWorldMatrix) + .def("GetWorldPoint", py::overload_cast(&ContainerBox::GetWorldPoint, py::const_)) + .def("GetLocalPoint", py::overload_cast(&ContainerBox::GetLocalPoint, py::const_)); + + // Math/StructuredData.h + py::enum_(m, "StructuredDataOrder") + .value("CustomOrder", StructuredData::CustomOrder) + .value("XYZ", StructuredData::XYZ) + .value("XZY", StructuredData::XZY) + .value("YXZ", StructuredData::YXZ) + .value("YZX", StructuredData::YZX) + .value("ZXY", StructuredData::ZXY) + .value("ZYX", StructuredData::ZYX) + .export_values(); + + py::class_(m, "StructuredData") + .def(py::init()) + .def("GetDims", &StructuredData::GetDims) + .def("SetDims", &StructuredData::SetDims) + .def("GetIncrements", &StructuredData::GetIncrements) + .def("SetIncrements", &StructuredData::SetIncrements) + .def("SetDataOrder", &StructuredData::SetDataOrder) + .def("GetDataOrder", &StructuredData::GetDataOrder) + .def("IsInsideGrid", &StructuredData::IsInsideGrid) + .def("Map", &StructuredData::Map) + .def("UnMap", &StructuredData::UnMap); + + // Math/StructuredGrid.h + py::class_(m, "StructuredGrid") + .def(py::init()) + .def("SetSpacing", &StructuredGrid::SetSpacing) + .def("GetSpacing", &StructuredGrid::GetSpacing) + .def("IsInsideBounds", &StructuredGrid::IsInsideBounds) + .def("Find", [](StructuredGrid &self, Vector3f pt) { + return self.Find(HPoint3f(pt)); + }); + + // Math/Structured2DGrid.h + py::class_(m, "Structured2DGrid") + .def(py::init<>()) + .def("SetDims", &Structured2DGrid::SetDims) + .def("GetDims", &Structured2DGrid::GetDims) + .def("IsInsideGrid", &Structured2DGrid::IsInsideGrid) + .def("Map", &Structured2DGrid::Map) + .def("UnMap", &Structured2DGrid::UnMap) + .def("SetPhysicalSpace", &Structured2DGrid::SetPhysicalSpace) + .def("GetSpacing", &Structured2DGrid::GetSpacing) + .def("GetOrigin", &Structured2DGrid::GetOrigin) + .def("IsInsideBounds", &Structured2DGrid::IsInsideBounds) + .def("PhysicsToUnitSpace", &Structured2DGrid::PhysicsToUnitSpace) + .def("UnitToPhysicsSpace", &Structured2DGrid::UnitToPhysicsSpace) + .def("SetDebug", &Structured2DGrid::SetDebug); + + // Math/Structured4DGrid.h + py::class_(m, "Structured4DGrid") + .def(py::init<>()) + .def("SetDims", &Structured4DGrid::SetDims) + .def("GetDims", &Structured4DGrid::GetDims) + .def("IsInsideGrid", &Structured4DGrid::IsInsideGrid) + .def("Map", &Structured4DGrid::Map) + .def("UnMap", &Structured4DGrid::UnMap) + .def("SetPhysicalSpace", &Structured4DGrid::SetPhysicalSpace) + .def("GetSpacing", &Structured4DGrid::GetSpacing) + .def("GetOrigin", &Structured4DGrid::GetOrigin) + .def("IsInsideBounds", &Structured4DGrid::IsInsideBounds) + .def("PhysicsToUnitSpace", &Structured4DGrid::PhysicsToUnitSpace) + .def("UnitToPhysicsSpace", &Structured4DGrid::UnitToPhysicsSpace) + .def("SetDebug", &Structured4DGrid::SetDebug); + + // Math/TriangleMesh.h + py::class_(m, "TriangleMesh") + .def(py::init<>()) + .def("AddPoint", &TriangleMesh::AddPoint) + .def("AddTriangle", py::overload_cast(&TriangleMesh::AddTriangle)) + .def("Points", &TriangleMesh::Points, py::return_value_policy::reference_internal) + .def("Triangles", &TriangleMesh::Triangles, py::return_value_policy::reference_internal); + + // Math/VoxRaytracer.h + py::class_(m, "VoxRaytracerRayDataElement") + .def(py::init<>()) + .def_readwrite("vox_id", &VoxRaytracer::RayData::Element::vox_id) + .def_readwrite("L", &VoxRaytracer::RayData::Element::L); + + py::class_(m, "VoxRaytracerRayData") + .def(py::init<>()) + .def("AppendRay", &VoxRaytracer::RayData::AppendRay) + .def("Count", &VoxRaytracer::RayData::Count) + .def("TotalLength", &VoxRaytracer::RayData::TotalLength) + .def("SetCount", &VoxRaytracer::RayData::SetCount) + .def("SetTotalLength", &VoxRaytracer::RayData::SetTotalLength); + + py::class_(m, "VoxRaytracer") + .def(py::init(), py::keep_alive<1, 2>()) + .def("GetImage", &VoxRaytracer::GetImage, py::return_value_policy::reference_internal); + + // Math/Accumulator.h + py::class_>(m, "Accumulator_Mean_f") + .def(py::init<>()) + .def("AddPass", &Accumulator_Mean::AddPass) + .def("__call__", py::overload_cast(&Accumulator_Mean::operator())) + .def("__call__", py::overload_cast<>(&Accumulator_Mean::operator(), py::const_)); + +} diff --git a/src/Python/module.cpp b/src/Python/module.cpp new file mode 100644 index 0000000..0cef30d --- /dev/null +++ b/src/Python/module.cpp @@ -0,0 +1,18 @@ +#include + +namespace py = pybind11; + +void init_core(py::module_ &m); +void init_math(py::module_ &m); + +PYBIND11_MODULE(uLib_python, m) { + m.doc() = "Python bindings for uLib Core and Math libraries"; + + // Core submodule + py::module_ core = m.def_submodule("Core", "Core library bindings"); + init_core(core); + + // Math submodule + py::module_ math = m.def_submodule("Math", "Math library bindings"); + init_math(math); +} diff --git a/src/Python/testing/core_pybind_test.py b/src/Python/testing/core_pybind_test.py new file mode 100644 index 0000000..216a7b4 --- /dev/null +++ b/src/Python/testing/core_pybind_test.py @@ -0,0 +1,33 @@ +import sys +import os +import unittest +import time + +import uLib_python + +class TestCoreOptions(unittest.TestCase): + def test_options(self): + opt = uLib_python.Core.Options("Test Options") + + # Test basic config file parsing + with open("test_configuration.ini", "w") as f: + f.write("[Section]\n") + + opt.parse_config_file("test_configuration.ini") + os.remove("test_configuration.ini") + +class TestCoreObject(unittest.TestCase): + def test_object(self): + obj = uLib_python.Core.Object() + self.assertIsNotNone(obj) + +class TestCoreTimer(unittest.TestCase): + def test_timer(self): + timer = uLib_python.Core.Timer() + timer.Start() + time.sleep(0.1) + val = timer.StopWatch() + self.assertGreater(val, 0.09) + +if __name__ == '__main__': + unittest.main() diff --git a/src/Python/testing/math_pybind_test.py b/src/Python/testing/math_pybind_test.py new file mode 100644 index 0000000..795252c --- /dev/null +++ b/src/Python/testing/math_pybind_test.py @@ -0,0 +1,62 @@ +import sys +import os +import unittest +import numpy as np + +import uLib_python + +def vector4f0(v, target): + diff = np.array(v) - np.array(target) + diff[3] = 0 # ignoring w + return np.all(np.abs(diff) < 0.001) + +class TestMathGeometry(unittest.TestCase): + def test_geometry(self): + Geo = uLib_python.Math.Geometry() + + Geo.SetPosition([1, 1, 1]) + + pt = Geo.GetLocalPoint([2, 3, 2, 1]) + wp = Geo.GetWorldPoint(pt) + + self.assertTrue(vector4f0(wp, [2, 3, 2, 1])) + + Geo.Scale([2, 2, 2]) + wp = Geo.GetWorldPoint([1, 1, 1, 1]) + self.assertTrue(vector4f0(wp, [3, 3, 3, 1])) + +class TestMathContainerBox(unittest.TestCase): + def test_container_box_local(self): + Cnt = uLib_python.Math.ContainerBox() + Cnt.SetOrigin([-1, -1, -1]) + Cnt.SetSize([2, 2, 2]) + + size = Cnt.GetSize() + self.assertTrue(np.allclose(size, [2, 2, 2])) + + def test_container_box_global(self): + Box = uLib_python.Math.ContainerBox() + Box.SetPosition([1, 1, 1]) + Box.SetSize([2, 2, 2]) + + pt = Box.GetLocalPoint([2, 3, 2, 1]) + wp = Box.GetWorldPoint(pt) + self.assertTrue(vector4f0(wp, [2, 3, 2, 1])) + +class TestMathStructuredGrid(unittest.TestCase): + def test_structured_grid(self): + grid = uLib_python.Math.StructuredGrid([10, 10, 10]) + grid.SetSpacing([1, 1, 1]) + + spacing = grid.GetSpacing() + self.assertTrue(np.allclose(spacing, [1, 1, 1])) + +class TestMathAccumulator(unittest.TestCase): + def test_accumulator_mean(self): + acc = uLib_python.Math.Accumulator_Mean_f() + acc(10.0) + acc(20.0) + self.assertAlmostEqual(acc(), 15.0) + +if __name__ == '__main__': + unittest.main() diff --git a/src/Python/testing/pybind_test.py b/src/Python/testing/pybind_test.py new file mode 100644 index 0000000..ced2d27 --- /dev/null +++ b/src/Python/testing/pybind_test.py @@ -0,0 +1,46 @@ +import sys +import os + +import uLib_python + +def test_core(): + print("Testing Core module...") + obj = uLib_python.Core.Object() + print("Core Object created:", obj) + + timer = uLib_python.Core.Timer() + timer.Start() + print("Core Timer started") + + options = uLib_python.Core.Options("Test Options") + print("Core Options created:", options) + +def test_math(): + print("Testing Math module...") + + # Test AffineTransform + transform = uLib_python.Math.AffineTransform() + print("AffineTransform created") + + # Test Geometry + geom = uLib_python.Math.Geometry() + print("Geometry created") + + # Test StructuredData + data = uLib_python.Math.StructuredData([10, 10, 10]) + print("StructuredData created with dims:", data.GetDims()) + + # Test Structured2DGrid + grid2d = uLib_python.Math.Structured2DGrid() + grid2d.SetDims([100, 100]) + print("Structured2DGrid created with dims:", grid2d.GetDims()) + + # Test TriangleMesh + mesh = uLib_python.Math.TriangleMesh() + print("TriangleMesh created") + + print("All tests passed successfully!") + +if __name__ == "__main__": + test_core() + test_math()