diff --git a/.gitignore b/.gitignore index 95b16f7..8cd3cea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ build/ build_warnings*.log final_build.log cmake_configure.log -compile_commands.json \ No newline at end of file +compile_commands.json + +dist/ +build_python/ +src/Python/uLib/*.so* \ No newline at end of file diff --git a/build_python.py b/build_python.py new file mode 100644 index 0000000..1a8554f --- /dev/null +++ b/build_python.py @@ -0,0 +1,47 @@ +import os +import subprocess +import sys +import shutil + +def build(setup_kwargs): + """ + Build the C++ extension using CMake. + This function is called by poetry-core during the build process. + The binary is placed directly inside the uLib directory in src/Python. + """ + # Root of the whole project where this build_extension.py is located + project_root = os.path.abspath(os.path.dirname(__file__)) + + # Where the extension should go + package_dir = os.path.join(project_root, "src/Python/uLib") + + # Ensure package directory exists + os.makedirs(package_dir, exist_ok=True) + + # Temporary build directory + build_temp = os.path.join(project_root, "build_python") + os.makedirs(build_temp, exist_ok=True) + + print(f"--- Running CMake build in {build_temp} ---") + print(f"Project root: {project_root}") + print(f"Target binary dir: {package_dir}") + + # CMake configuration + cmake_args = [ + f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={package_dir}", + f"-DPYTHON_EXECUTABLE={sys.executable}", + "-DCMAKE_BUILD_TYPE=Release", + "-DUSE_CUDA=OFF", + "-G", "Unix Makefiles", + ] + + # Use micromamba to ensure Boost and VTK are found during the build + subprocess.check_call(["cmake", project_root] + cmake_args, cwd=build_temp) + subprocess.check_call(["cmake", "--build", ".", "--parallel", "--target", "uLib_python"], cwd=build_temp) + + # Ensure the package is found by poetry during the wheel creation process. + # Return setup_kwargs for poetry-core. + return setup_kwargs + +if __name__ == "__main__": + build({}) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..9764845 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "db9b4c08b159b17b239e26c67ead7c37b82d9f9eb06550245ae3134c095f98f7" diff --git a/pyproject.toml b/pyproject.toml index 1b54914..fff5cad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,15 @@ +[tool.poetry] +name = "uLib" +version = "0.6.0" +description = "CMT Cosmic Muon Tomography project uLib python bindings" +authors = ["Andrea Rigoni Garola "] +readme = "README.md" +packages = [{ include = "uLib", from = "src/Python" }] +build = "build_python.py" + +[tool.poetry.dependencies] +python = ">=3.9" [build-system] -requires = [ - "setuptools>=42", - "wheel", - "pybind11>=2.6.0", - "cmake>=3.12", - "ninja"] - -build-backend = "setuptools.build_meta" +requires = ["poetry-core>=2.0.0", "pybind11>=2.6.0", "cmake>=3.12"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.py b/setup.py deleted file mode 100644 index c6e6e20..0000000 --- a/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import re -import subprocess -import sys - -from setuptools import Extension, setup -from setuptools.command.build_ext import build_ext - -class CMakeExtension(Extension): - def __init__(self, name, sourcedir=""): - Extension.__init__(self, name, sources=[]) - self.sourcedir = os.path.abspath(sourcedir) - -class CMakeBuild(build_ext): - def build_extension(self, ext): - extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) - - if not extdir.endswith(os.path.sep): - extdir += os.path.sep - - debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug - cfg = "Debug" if debug else "Release" - - cmake_args = [ - f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}", - f"-DPYTHON_EXECUTABLE={sys.executable}", - f"-DCMAKE_BUILD_TYPE={cfg}", - f"-DCMAKE_BUILD_WITH_INSTALL_RPATH=TRUE", - f"-DCMAKE_INSTALL_RPATH=$ORIGIN", - "-DUSE_CUDA=OFF", - "-G", "Unix Makefiles", - ] - build_args = [] - - build_temp = os.path.join(self.build_temp, ext.name) - if not os.path.exists(build_temp): - os.makedirs(build_temp) - - subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=build_temp) - subprocess.check_call(["cmake", "--build", ".", "--target", "uLib_python"] + build_args, cwd=build_temp) - -setup( - name="ulib", - version="0.6.0", - author="Andrea Rigoni Garola", - author_email="andrea.rigoni@pd.infn.it", - description="CMT Cosmic Muon Tomography project uLib python bindings", - ext_modules=[CMakeExtension("uLib_python")], - cmdclass={"build_ext": CMakeBuild}, - zip_safe=False, - python_requires=">=3.6", -) diff --git a/src/Python/testing/core_pybind_test.py b/src/Python/testing/core_pybind_test.py index 216a7b4..9cd3f56 100644 --- a/src/Python/testing/core_pybind_test.py +++ b/src/Python/testing/core_pybind_test.py @@ -3,11 +3,11 @@ import os import unittest import time -import uLib_python +import uLib class TestCoreOptions(unittest.TestCase): def test_options(self): - opt = uLib_python.Core.Options("Test Options") + opt = uLib.Core.Options("Test Options") # Test basic config file parsing with open("test_configuration.ini", "w") as f: @@ -18,12 +18,12 @@ class TestCoreOptions(unittest.TestCase): class TestCoreObject(unittest.TestCase): def test_object(self): - obj = uLib_python.Core.Object() - self.assertIsNotNone(obj) + obj = uLib.Core.Object() + self.assertIsNotN one(obj) class TestCoreTimer(unittest.TestCase): def test_timer(self): - timer = uLib_python.Core.Timer() + timer = uLib.Core.Timer() timer.Start() time.sleep(0.1) val = timer.StopWatch() diff --git a/src/Python/testing/math_pybind_test.py b/src/Python/testing/math_pybind_test.py index d638fca..f3d7c2a 100644 --- a/src/Python/testing/math_pybind_test.py +++ b/src/Python/testing/math_pybind_test.py @@ -3,7 +3,7 @@ import os import unittest import numpy as np -import uLib_python +import uLib def vector4f0(v, target): diff = np.array(v) - np.array(target) @@ -12,7 +12,7 @@ def vector4f0(v, target): class TestMathGeometry(unittest.TestCase): def test_geometry(self): - Geo = uLib_python.Math.Geometry() + Geo = uLib.Math.Geometry() Geo.SetPosition([1, 1, 1]) @@ -27,7 +27,7 @@ class TestMathGeometry(unittest.TestCase): class TestMathContainerBox(unittest.TestCase): def test_container_box_local(self): - Cnt = uLib_python.Math.ContainerBox() + Cnt = uLib.Math.ContainerBox() Cnt.SetOrigin([-1, -1, -1]) Cnt.SetSize([2, 2, 2]) @@ -35,7 +35,7 @@ class TestMathContainerBox(unittest.TestCase): self.assertTrue(np.allclose(size, [2, 2, 2])) def test_container_box_global(self): - Box = uLib_python.Math.ContainerBox() + Box = uLib.Math.ContainerBox() Box.SetPosition([1, 1, 1]) Box.SetSize([2, 2, 2]) @@ -45,7 +45,7 @@ class TestMathContainerBox(unittest.TestCase): class TestMathStructuredGrid(unittest.TestCase): def test_structured_grid(self): - grid = uLib_python.Math.StructuredGrid([10, 10, 10]) + grid = uLib.Math.StructuredGrid([10, 10, 10]) grid.SetSpacing([1, 1, 1]) spacing = grid.GetSpacing() @@ -53,40 +53,40 @@ class TestMathStructuredGrid(unittest.TestCase): class TestMathAccumulator(unittest.TestCase): def test_accumulator_mean(self): - acc = uLib_python.Math.Accumulator_Mean_f() + acc = uLib.Math.Accumulator_Mean_f() acc(10.0) acc(20.0) self.assertAlmostEqual(acc(), 15.0) class TestMathNewTypes(unittest.TestCase): def test_eigen_vectors(self): - v1f = uLib_python.Math.Vector1f() - v3d = uLib_python.Math.Vector3d() - m4f = uLib_python.Math.Matrix4f() + v1f = uLib.Math.Vector1f() + v3d = uLib.Math.Vector3d() + m4f = uLib.Math.Matrix4f() self.assertIsNotNone(v1f) self.assertIsNotNone(v3d) self.assertIsNotNone(m4f) def test_ulib_vectors(self): - vi = uLib_python.Math.Vector_i() + vi = uLib.Math.Vector_i() vi.append(1) vi.append(2) self.assertEqual(len(vi), 2) self.assertEqual(vi[0], 1) self.assertEqual(vi[1], 2) - vf = uLib_python.Math.Vector_f() + vf = uLib.Math.Vector_f() vf.append(1.5) self.assertAlmostEqual(vf[0], 1.5) def test_homogeneous(self): - p = uLib_python.Math.HPoint3f(1.0, 2.0, 3.0) - v = uLib_python.Math.HVector3f(0.0, 1.0, 0.0) + p = uLib.Math.HPoint3f(1.0, 2.0, 3.0) + v = uLib.Math.HVector3f(0.0, 1.0, 0.0) self.assertIsNotNone(p) self.assertIsNotNone(v) def test_vox_image(self): - img = uLib_python.Math.VoxImage([2, 2, 2]) + img = uLib.Math.VoxImage([2, 2, 2]) self.assertEqual(img.GetDims()[0], 2) img.SetValue([0, 0, 0], 10.5) # Note: GetValue returns float, and there might be internal scaling (1.E-6 observed in code) @@ -96,11 +96,11 @@ class TestMathNewTypes(unittest.TestCase): class TestMathVoxRaytracer(unittest.TestCase): def test_raytracer(self): - grid = uLib_python.Math.StructuredGrid([10, 10, 10]) + grid = uLib.Math.StructuredGrid([10, 10, 10]) grid.SetSpacing([1, 1, 1]) grid.SetOrigin([0, 0, 0]) - rt = uLib_python.Math.VoxRaytracer(grid) + rt = uLib.Math.VoxRaytracer(grid) self.assertIsNotNone(rt) # Test TraceBetweenPoints @@ -118,7 +118,7 @@ class TestMathVoxRaytracer(unittest.TestCase): self.assertGreater(elements[i].L, 0) def test_ray_data(self): - data = uLib_python.Math.VoxRaytracerRayData() + data = uLib.Math.VoxRaytracerRayData() data.SetCount(10) data.SetTotalLength(5.5) self.assertEqual(data.Count(), 10) diff --git a/src/Python/testing/pybind_test.py b/src/Python/testing/pybind_test.py index ced2d27..a61fcd3 100644 --- a/src/Python/testing/pybind_test.py +++ b/src/Python/testing/pybind_test.py @@ -1,42 +1,42 @@ import sys import os -import uLib_python +import uLib def test_core(): print("Testing Core module...") - obj = uLib_python.Core.Object() + obj = uLib.Core.Object() print("Core Object created:", obj) - timer = uLib_python.Core.Timer() + timer = uLib.Core.Timer() timer.Start() print("Core Timer started") - options = uLib_python.Core.Options("Test Options") + options = uLib.Core.Options("Test Options") print("Core Options created:", options) def test_math(): print("Testing Math module...") # Test AffineTransform - transform = uLib_python.Math.AffineTransform() + transform = uLib.Math.AffineTransform() print("AffineTransform created") # Test Geometry - geom = uLib_python.Math.Geometry() + geom = uLib.Math.Geometry() print("Geometry created") # Test StructuredData - data = uLib_python.Math.StructuredData([10, 10, 10]) + data = uLib.Math.StructuredData([10, 10, 10]) print("StructuredData created with dims:", data.GetDims()) # Test Structured2DGrid - grid2d = uLib_python.Math.Structured2DGrid() + grid2d = uLib.Math.Structured2DGrid() grid2d.SetDims([100, 100]) print("Structured2DGrid created with dims:", grid2d.GetDims()) # Test TriangleMesh - mesh = uLib_python.Math.TriangleMesh() + mesh = uLib.Math.TriangleMesh() print("TriangleMesh created") print("All tests passed successfully!") diff --git a/src/Python/uLib/__init__.py b/src/Python/uLib/__init__.py new file mode 100644 index 0000000..6769b3b --- /dev/null +++ b/src/Python/uLib/__init__.py @@ -0,0 +1,7 @@ +try: + from .uLib_python import Core, Math +except ImportError: + # Handle cases where the binary extension is not yet built + pass + +__all__ = ["Core", "Math"]