add documention workflow

This commit is contained in:
AndreaRigoni
2026-03-06 10:45:33 +00:00
parent f3ebba4931
commit b64afe8773
15 changed files with 1513 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
# Developer Guide Python Bindings
This guide is aimed at contributors who want to extend or modify the Python bindings for `uLib`.
---
## Repository Layout
```
ulib/
├── src/
│ └── Python/
│ ├── module.cpp # pybind11 module entry point
│ ├── core_bindings.cpp # uLib::Core bindings
│ ├── math_bindings.cpp # uLib::Math bindings
│ ├── math_filters_bindings.cpp# VoxImageFilter bindings
│ ├── CMakeLists.txt # builds uLib_python shared lib
│ ├── testing/ # Python unit tests
│ │ ├── pybind_test.py
│ │ ├── core_pybind_test.py
│ │ ├── math_pybind_test.py
│ │ └── math_filters_test.py
│ └── uLib/ # Python package (uLib_python.so lands here)
│ └── __init__.py
├── build_python.py # poetry build hook (calls CMake)
├── pyproject.toml # poetry metadata
└── condaenv.yml # conda/micromamba environment
```
---
## Adding a New Binding
All bindings live in the four source files listed above. The module entry point `module.cpp` calls `init_core()`, `init_math()`, and `init_math_filters()` in order.
### 1. Pick (or create) the right binding file
| C++ header location | Binding file |
|---|---|
| `src/Core/` | `core_bindings.cpp` |
| `src/Math/` (geometry, grids, VoxImage) | `math_bindings.cpp` |
| `src/Math/VoxImageFilter*.hpp` | `math_filters_bindings.cpp` |
### 2. Add the `#include` directive
```cpp
// math_bindings.cpp
#include "Math/MyNewClass.h"
```
### 3. Write the pybind11 binding inside the appropriate `init_*` function
```cpp
void init_math(py::module_ &m) {
// ... existing bindings ...
py::class_<MyNewClass>(m, "MyNewClass")
.def(py::init<>())
.def("MyMethod", &MyNewClass::MyMethod)
.def("AnotherMethod", &MyNewClass::AnotherMethod,
py::arg("x"), py::arg("y") = 0.0f);
}
```
### 4. Rebuild only the Python target
```bash
cmake --build build --target uLib_python -j$(nproc)
```
### 5. Write a Python test
Add a new test class to the relevant test file (or create a new one under `src/Python/testing/`):
```python
# src/Python/testing/math_pybind_test.py
class TestMyNewClass(unittest.TestCase):
def test_basic(self):
obj = uLib.Math.MyNewClass()
result = obj.MyMethod()
self.assertAlmostEqual(result, expected_value)
```
Register the test in `src/Python/CMakeLists.txt` if you add a new file:
```cmake
add_test(NAME pybind_my_new
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/my_new_test.py)
set_tests_properties(pybind_my_new PROPERTIES
ENVIRONMENT "PYTHONPATH=$<TARGET_FILE_DIR:uLib_python>:${PROJECT_SOURCE_DIR}/src/Python")
```
---
## Build System Details
### CMakeLists.txt (`src/Python/`)
`pybind11_add_module` compiles the shared library `uLib_python` and links it against the C++ static/shared libraries `uLibCore` and `uLibMath`. The install target copies the `.so` into the standard library directory.
```cmake
pybind11_add_module(uLib_python
module.cpp core_bindings.cpp math_bindings.cpp math_filters_bindings.cpp)
target_link_libraries(uLib_python PRIVATE uLibCore uLibMath)
```
### poetry / build_python.py
`pyproject.toml` declares `build_python.py` as the custom build hook. When `poetry install` or `poetry build` is invoked it:
1. Calls `cmake <root> -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=<pkg_dir> ...` in `build_python/`.
2. Builds only the `uLib_python` target.
3. The resulting `.so` is placed inside `src/Python/uLib/` so it is picked up by Poetry as a package data file.
The `USE_CUDA` environment variable gates CUDA support at build time:
```bash
USE_CUDA=ON poetry install # with CUDA
USE_CUDA=OFF poetry install # CPU only (default)
```
---
## Running All Tests
```bash
# From the repository root, with PYTHONPATH set:
export PYTHONPATH="$(pwd)/build/src/Python:$(pwd)/src/Python"
python -m pytest src/Python/testing/ -v
```
Or through CMake's test runner (after building the full project):
```bash
cd build
ctest --output-on-failure -R pybind
```
Expected output (all passing):
```
Start 1: pybind_general
1/4 Test #1: pybind_general ............. Passed
Start 2: pybind_core
2/4 Test #2: pybind_core ................ Passed
Start 3: pybind_math
3/4 Test #3: pybind_math ................ Passed
Start 4: pybind_math_filters
4/4 Test #4: pybind_math_filters ........ Passed
```
---
## Memory Management Notes
`uLib::Vector<T>` has explicit GPU memory management. When wrapping methods that return references to internal data, use `py::return_value_policy::reference_internal` to avoid dangling references:
```cpp
.def("Data", &VoxImage<Voxel>::Data,
py::return_value_policy::reference_internal)
```
For objects held by `std::unique_ptr` without Python-side deletion, use `py::nodelete`:
```cpp
py::class_<Abstract::VoxImageFilter,
std::unique_ptr<Abstract::VoxImageFilter, py::nodelete>>(m, "AbstractVoxImageFilter")
```
---
## Useful References
- [pybind11 documentation](https://pybind11.readthedocs.io)
- [pybind11 STL containers](https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html)
- [pybind11 Eigen integration](https://pybind11.readthedocs.io/en/stable/advanced/cast/eigen.html)
- [CMake pybind11 integration](https://pybind11.readthedocs.io/en/stable/compiling.html)

146
docs/python/installation.md Normal file
View File

@@ -0,0 +1,146 @@
# Python Installation
The `uLib` Python package exposes the Core and Math C++ libraries via [pybind11](https://pybind11.readthedocs.io) bindings. There are two ways to install it: as an **end user** (pre-built wheel / pip) or as a **developer** (editable build from source).
---
## Prerequisites
`uLib` depends on native C++ libraries that must be compiled. Ensure the following are available in your environment before installing:
| Dependency | Minimum version | Notes |
|---|---|---|
| Python | 3.9 | |
| CMake | 3.12 | |
| pybind11 | 2.6.0 | |
| Conan | 2.x | for Eigen3 / Boost |
| micromamba / conda | any | recommended provides ROOT, VTK |
### Creating the `mutom` Conda/Micromamba Environment
A ready-to-use environment definition is provided as `condaenv.yml` at the repository root.
=== "Micromamba"
```bash
micromamba env create -f condaenv.yml
micromamba activate mutom
```
=== "Conda"
```bash
conda env create -f condaenv.yml
conda activate mutom
```
The environment installs CMake, Conan, ROOT, VTK, and the compiler toolchain.
> **CUDA (optional)**
> If you want GPU-accelerated voxel filtering, you also need NVCC inside the environment:
> ```bash
> micromamba install cuda-nvcc -c conda-forge
> ```
---
## User Installation (wheel / pip)
Once the native dependencies are present in your environment, install the package with Poetry or pip:
```bash
# Activate your environment first
micromamba activate mutom
# Build and install (CUDA disabled by default)
poetry install
# Build and install with CUDA support
USE_CUDA=ON poetry install
```
After installation the module is importable from anywhere in the environment:
```python
import uLib
print(dir(uLib.Core))
print(dir(uLib.Math))
```
---
## Developer Installation (editable / in-tree build)
For development you typically want to skip the packaging layer and work directly against the CMake build tree.
### Step 1 Install Conan dependencies
```bash
conan profile detect # first time only
conan install . --output-folder=build --build=missing
```
### Step 2 Configure and build
```bash
# Standard release build
cmake --preset conan-release
# …or manually
cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DUSE_CUDA=OFF # set to ON when a GPU is available
cmake --build build --target uLib_python -j$(nproc)
```
The shared library (`uLib_python*.so`) is written to `build/src/Python/`.
### Step 3 Make the module importable
Point `PYTHONPATH` at the build output **and** the Python source directory (the latter carries the `uLib/__init__.py` that stitches sub-modules together):
```bash
export PYTHONPATH="$(pwd)/build/src/Python:$(pwd)/src/Python:$PYTHONPATH"
python -c "import uLib; print(uLib.__version__)"
```
Or, for a one-shot check:
```bash
PYTHONPATH="build/src/Python:src/Python" python src/Python/testing/pybind_test.py
```
### Step 4 Run the tests
CMake registers the Python tests alongside the C++ ones; use `ctest` from the build directory:
```bash
cd build
ctest --output-on-failure -R pybind
```
Individual test scripts can also be run directly once `PYTHONPATH` is set:
```bash
python src/Python/testing/core_pybind_test.py
python src/Python/testing/math_pybind_test.py
python src/Python/testing/math_filters_test.py
```
---
## Verifying the Installation
```python
import uLib
# Core module
obj = uLib.Core.Object()
timer = uLib.Core.Timer()
timer.Start()
elapsed = timer.StopWatch() # float, seconds
# Math module
v3 = uLib.Math.Vector3f([1.0, 0.0, 0.0])
print(v3[0]) # 1.0
```

373
docs/python/usage.md Normal file
View File

@@ -0,0 +1,373 @@
# Python API Usage
The `uLib` Python package is split into two sub-modules mirroring the C++ library:
| Sub-module | Contents |
|---|---|
| `uLib.Core` | Low-level utilities: `Object`, `Timer`, `Options`, `TypeRegister` |
| `uLib.Math` | Geometry, grids, voxel images, ray-tracing, image filters |
```python
import uLib
# Sub-modules are accessible as attributes
uLib.Core # core utilities
uLib.Math # mathematical structures
```
---
## uLib.Core
### Object
Base class for uLib objects; exposed to Python for type-hierarchy purposes.
```python
obj = uLib.Core.Object()
copy = obj.DeepCopy()
```
### Timer
Precision wall-clock timer.
```python
import time
timer = uLib.Core.Timer()
timer.Start()
time.sleep(0.5)
elapsed = timer.StopWatch() # returns elapsed seconds as float
print(f"Elapsed: {elapsed:.3f} s")
```
### Options
Wraps Boost.ProgramOptions for INI-style configuration files.
```python
opt = uLib.Core.Options("My Program")
opt.parse_config_file("config.ini") # load settings
n = opt.count("my_key") # check if key exists
opt.save_config_file("out.ini")
```
---
## uLib.Math Linear Algebra
The math module exposes Eigen3 vectors and matrices as well-typed Python objects with NumPy interoperability.
### Fixed-size Vectors
```python
import numpy as np
import uLib
# Construct from list
v3 = uLib.Math.Vector3f([1.0, 2.0, 3.0])
print(v3[0], v3[1], v3[2]) # 1.0 2.0 3.0
# Construct from NumPy array
arr = np.array([4.0, 5.0, 6.0], dtype=np.float32)
v3b = uLib.Math.Vector3f(arr)
# Zero-initialise
v4d = uLib.Math.Vector4d() # all zeros
# Available types
# Vector1f / 2f / 3f / 4f (float32)
# Vector1d / 2d / 3d / 4d (float64)
# Vector1i / 2i / 3i / 4i (int32)
```
### Fixed-size Matrices
```python
# 2-by-2 float matrix
m2f = uLib.Math.Matrix2f()
m2f[0, 0] = 1; m2f[0, 1] = 2
m2f[1, 0] = 3; m2f[1, 1] = 4
# From list (row-major)
m4f = uLib.Math.Matrix4f([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])
# From NumPy (2-D array)
mat = np.eye(3, dtype=np.float32)
m3f = uLib.Math.Matrix3f(mat)
# Dynamic matrices
mXf = uLib.Math.MatrixXf(4, 4) # 4×4 float, zeros
```
### Homogeneous Types
```python
# HPoint3f a 3-D point in homogeneous coordinates (w = 1)
p = uLib.Math.HPoint3f(1.0, 2.0, 3.0)
# HVector3f a free vector (w = 0)
v = uLib.Math.HVector3f(0.0, 1.0, 0.0)
# HLine3f a parametric ray
line = uLib.Math.HLine3f()
line.origin = uLib.Math.HPoint3f(0, 0, 0)
line.direction = uLib.Math.HVector3f(0, 0, 1)
```
---
## uLib.Math Transforms and Geometry
### AffineTransform
A rigid-body / affine transform stored as a 4×4 matrix.
```python
tf = uLib.Math.AffineTransform()
tf.SetPosition([1.0, 0.0, 0.0]) # translate
tf.Translate([0.0, 1.0, 0.0]) # cumulative translate
tf.Scale([2.0, 2.0, 2.0]) # uniform scale
tf.Rotate(uLib.Math.Vector3f([0, 0, 3.14159])) # Euler angles (rad)
mat4 = tf.GetWorldMatrix() # 4×4 matrix
pos = tf.GetPosition() # Vector3f
```
### Geometry
Inherits `AffineTransform`; converts points between world and local frames.
```python
geo = uLib.Math.Geometry()
geo.SetPosition([1.0, 1.0, 1.0])
world_pt = uLib.Math.Vector4f([2.0, 3.0, 2.0, 1.0])
local_pt = geo.GetLocalPoint(world_pt)
back = geo.GetWorldPoint(local_pt)
# back ≈ [2, 3, 2, 1]
```
### ContainerBox
An axis-aligned bounding box with an associated transform.
```python
box = uLib.Math.ContainerBox()
box.SetOrigin([-1.0, -1.0, -1.0])
box.SetSize([2.0, 2.0, 2.0])
print(box.GetSize()) # [2, 2, 2]
```
---
## uLib.Math Structured Grids
### StructuredGrid (3-D)
A 3-D voxel grid (origin, spacing, and integer dimensions).
```python
dims = uLib.Math.Vector3i([10, 10, 10])
grid = uLib.Math.StructuredGrid(dims)
grid.SetSpacing([1.0, 1.0, 1.0])
grid.SetOrigin([0.0, 0.0, 0.0])
print(grid.GetSpacing()) # [1, 1, 1]
print(grid.IsInsideBounds([5, 5, 5, 1])) # True
idx = grid.Find([2.5, 2.5, 2.5]) # returns grid cell index
```
### Structured2DGrid / Structured4DGrid
```python
grid2d = uLib.Math.Structured2DGrid()
grid2d.SetDims([100, 100])
grid2d.SetPhysicalSpace([0, 0], [1, 1])
print(grid2d.GetSpacing())
```
---
## uLib.Math VoxImage
`VoxImage` is a 3-D voxel volume where each cell stores a `Voxel` ( `.Value` + `.Count`).
```python
dims = uLib.Math.Vector3i([20, 20, 20])
img = uLib.Math.VoxImage(dims)
img.SetSpacing([0.5, 0.5, 0.5])
# Access by linear index
img.SetValue(0, 42.0)
print(img.GetValue(0)) # 42.0
# Access by 3-D index
img.SetValue(uLib.Math.Vector3i([1, 1, 1]), 7.5)
print(img.GetValue(uLib.Math.Vector3i([1, 1, 1]))) # 7.5
# Clipping / masking helpers
cropped = img.clipImage(uLib.Math.Vector3i([2, 2, 2]),
uLib.Math.Vector3i([18, 18, 18]))
masked = img.maskImage(0.0, 100.0, 0.0) # mask outside [0, 100]
# I/O
img.ExportToVti("output.vti")
img.ImportFromVti("output.vti")
```
### Voxel (element type)
```python
vox = uLib.Math.Voxel()
vox.Value = 1.5
vox.Count = 3
data = img.Data() # returns the underlying Vector_Voxel
vox0 = data[0]
print(vox0.Value, vox0.Count)
```
---
## uLib.Math VoxRaytracer
Performs ray-tracing through a `StructuredGrid` and returns per-voxel chord lengths.
```python
import numpy as np
import uLib
grid = uLib.Math.StructuredGrid([10, 10, 10])
grid.SetSpacing([1.0, 1.0, 1.0])
grid.SetOrigin([0.0, 0.0, 0.0])
rt = uLib.Math.VoxRaytracer(grid)
# Trace a ray between two homogeneous points (x, y, z, w=1)
p1 = np.array([0.5, 0.5, -1.0, 1.0], dtype=np.float32)
p2 = np.array([0.5, 0.5, 11.0, 1.0], dtype=np.float32)
result = rt.TraceBetweenPoints(p1, p2)
print("Voxels crossed:", result.Count())
print("Total length :", result.TotalLength())
elements = result.Data()
for i in range(result.Count()):
print(f" vox_id={elements[i].vox_id} L={elements[i].L:.4f}")
```
---
## uLib.Math Image Filters
All filters share the same interface: construct with a kernel size, attach a `VoxImage`, optionally set parameters, then call `.Run()`.
```python
import uLib
dims = uLib.Math.Vector3i([10, 10, 10])
img = uLib.Math.VoxImage(dims)
for i in range(10**3):
img.SetValue(i, float(i))
kernel_dims = uLib.Math.Vector3i([3, 3, 3])
```
### Linear (Gaussian / Box) Filter
```python
filt = uLib.Math.VoxFilterAlgorithmLinear(kernel_dims)
filt.SetImage(img)
filt.SetKernelNumericXZY([1.0] * 27) # uniform box kernel, length = product of dims
filt.Run()
```
### ABTrim Filter
Applies alpha-beta trimming to remove outliers before averaging.
```python
filt = uLib.Math.VoxFilterAlgorithmAbtrim(kernel_dims)
filt.SetImage(img)
filt.SetKernelNumericXZY([1.0] * 27)
filt.SetABTrim(2, 2) # trim 2 low and 2 high values
filt.Run()
```
### Bilateral Filter
Edge-preserving smoothing controlled by a spatial sigma (from the kernel shape) and an intensity sigma.
```python
filt = uLib.Math.VoxFilterAlgorithmBilateral(kernel_dims)
filt.SetImage(img)
filt.SetKernelNumericXZY([1.0] * 27)
filt.SetIntensitySigma(0.3)
filt.Run()
```
### Threshold Filter
Zeros voxels below a threshold.
```python
filt = uLib.Math.VoxFilterAlgorithmThreshold(kernel_dims)
filt.SetImage(img)
filt.SetKernelNumericXZY([1.0] * 27)
filt.SetThreshold(0.5)
filt.Run()
```
### Median Filter
```python
filt = uLib.Math.VoxFilterAlgorithmMedian(kernel_dims)
filt.SetImage(img)
filt.SetKernelNumericXZY([1.0] * 27)
filt.Run()
```
---
## uLib.Math Accumulators
Accumulators collect scalar samples and return a summary statistic.
```python
# Arithmetic mean
acc = uLib.Math.Accumulator_Mean_f()
acc(10.0)
acc(20.0)
mean = acc() # 15.0
# Alpha-beta trimmed mean
acc2 = uLib.Math.Accumulator_ABTrim_f()
acc2.SetABTrim(0.1, 0.1) # trim bottom 10 % and top 10 %
acc2 += 1.0
acc2 += 9999.0 # outlier
acc2 += 5.0
result = acc2() # trimmed mean ≈ 3.0
```
---
## Dynamic Vectors (`uLib.Math.Vector_*`)
Typed dynamic arrays backed by `uLib::Vector<T>` with optional CUDA memory management.
```python
# Integer vector
vi = uLib.Math.Vector_i()
vi.append(1); vi.append(2); vi.append(3)
print(len(vi), vi[0])
# Float vector with CUDA management
vf = uLib.Math.Vector_f()
vf.append(1.5)
vf.MoveToVRAM() # copy to GPU (no-op when CUDA is absent)
vf.MoveToRAM() # copy back to CPU
# Other types: Vector_ui, Vector_l, Vector_ul, Vector_d
# Compound element types: Vector_Vector3f, Vector_Vector4f, Vector_Voxel …
```