diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e8cfa7..3b27b0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,38 @@ jobs: run: | valgrind --leak-check=full --track-origins=yes --error-exitcode=1 \ ./build/tests/test_bethe_ext00 + + python_tests: + runs-on: ubuntu-latest + needs: build_and_test + strategy: + matrix: + python-version: ["3.9", "3.13"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Configure + run: cmake -S . -B build + + - name: Build + run: cmake --build build --parallel + + - name: Install Python package and dev dependencies + run: python -m pip install -e "python[dev]" + + - name: Run ruff + if: ${{ matrix.python-version == '3.13' }} + working-directory: python + run: python -m ruff check . + + - name: Run Python tests + working-directory: python + env: + LIBDEDX_SO: ${{ github.workspace }}/build/libdedx/libdedx.so + run: python -m pytest tests -q diff --git a/.gitignore b/.gitignore index 6c67661..15c9b23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,20 @@ # Custom ignores +.codex *.bin + build/ build-release/ build-coverage/ + +Testing/ + docs/doxygen/html/ -.codex + +python/.venv/ +python/__pycache__/ +python/.pytest_cache/ +python/*.egg-info/ +*.pyc # Generated by CMake at configure time - do not commit libdedx/dedx_config.h diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4c901c8..015d8a6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { "recommendations": [ - "llvm-vs-code-extensions.vscode-clangd", "ms-vscode.cmake-tools", - "twxs.cmake" + "ms-python.python" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index fcd5de8..2a02297 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,13 @@ "editor.formatOnSave": true }, + // Python — add python/ to the analysis path so Pylance finds the libdedx package (__init__.py / __init__.pyi) + "python.analysis.extraPaths": ["python"], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.cwd": "${workspaceFolder}/python", + "python.testing.pytestArgs": ["tests"], + // Editor "editor.rulers": [120], "files.trimTrailingWhitespace": true, diff --git a/libdedx/CMakeLists.txt b/libdedx/CMakeLists.txt index dcab99f..deeeac8 100644 --- a/libdedx/CMakeLists.txt +++ b/libdedx/CMakeLists.txt @@ -3,35 +3,60 @@ set(dedx_data data/ASTAR.bin data/astarEng.dat data/mstarEng.dat data/pstarEng.b set(dedx_data_bin data/ASTAR.bin data/astarEng.bin data/ICRU73.bin data/icru73Eng.bin data/ICRU73_NEW.bin data/icru73_newEng.bin data/MSTAR.bin data/mstarEng.bin data/PSTAR.bin data/ICRU_PSTAR.bin data/ICRU_ASTAR.bin data/pstarEng.bin data/betheEng.bin data/icru_astarEng.bin data/icru_pstarEng.bin data/compos.txt data/effective_charge.dat data/short_names data/composition data/gas_states.dat data/atima_compos) -add_library(dedx STATIC dedx_tools.c dedx_bethe.c dedx.c dedx_wrappers.c dedx_file_access.c dedx_file.c dedx_mpaul.c dedx_mstar.c dedx_spline.c dedx_split.c dedx_periodic_table.c tools/dedx_math.c dedx_validate.c) - -target_include_directories(dedx PUBLIC +# Object library: compiled once with -fPIC so the objects feed both the +# static and the shared library without a second compilation pass. +add_library(dedx_objects OBJECT + dedx_tools.c dedx_bethe.c dedx.c dedx_wrappers.c dedx_file_access.c + dedx_file.c dedx_mpaul.c dedx_mstar.c dedx_spline.c dedx_split.c + dedx_periodic_table.c tools/dedx_math.c dedx_validate.c +) +set_property(TARGET dedx_objects PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(dedx_objects PUBLIC "${PROJECT_SOURCE_DIR}/libdedx" "${PROJECT_BINARY_DIR}" ) -# removed math_dedx.h -# removed dedx_atima.c - # Version passed as compile definitions, derived from git tag at configure time -target_compile_definitions(dedx PRIVATE +target_compile_definitions(dedx_objects PRIVATE DEDX_VERSION_MAJOR=${DEDX_VERSION_MAJOR} DEDX_VERSION_MINOR=${DEDX_VERSION_MINOR} DEDX_VERSION_PATCH=${DEDX_VERSION_PATCH} ) -target_link_libraries(dedx) - +# Static library — for C/CMake consumers; PUBLIC so include dirs propagate. +add_library(dedx STATIC) +target_link_libraries(dedx PUBLIC dedx_objects) # Visual Studio does not need or want you to explicitly request linking the math library. # You must avoid adding it as a link library when building for Windows. # see https://stackoverflow.com/questions/54935559/linking-math-library-in-cmake-file-on-windows-and-linux -IF (NOT WIN32) - target_link_libraries (dedx m) -ENDIF() +if(NOT WIN32) + target_link_libraries(dedx PUBLIC m) +endif() + +# Shared library — for Python ctypes and other dynamic consumers. +add_library(dedx_shared SHARED) +target_link_libraries(dedx_shared PUBLIC dedx_objects) +if(WIN32) + set_target_properties(dedx_shared PROPERTIES + OUTPUT_NAME dedx + ARCHIVE_OUTPUT_NAME dedx_shared + ) +else() + set_target_properties(dedx_shared PROPERTIES OUTPUT_NAME dedx) +endif() +if(NOT WIN32) + target_link_libraries(dedx_shared PUBLIC m) +endif() + +install(FILES dedx.h dedx_tools.h dedx_wrappers.h DESTINATION include) + +install(TARGETS dedx dedx_shared + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) -install (FILES dedx.h dedx_tools.h dedx_wrappers.h DESTINATION include) -install (TARGETS dedx DESTINATION lib) if(WIN32) - install (FILES ${dedx_data_bin} DESTINATION data) + install(FILES ${dedx_data_bin} DESTINATION data) else() - install (FILES ${dedx_data_bin} DESTINATION share/libdedx) + install(FILES ${dedx_data_bin} DESTINATION share/libdedx) endif() diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt deleted file mode 100644 index 032f928..0000000 --- a/python/CMakeLists.txt +++ /dev/null @@ -1,65 +0,0 @@ -cmake_minimum_required(VERSION 2.6.0) - -find_package(PythonLibs) -if (NOT PYTHONLIBS_FOUND) - message(STATUS "Python libraries NOT found") -endif (NOT PYTHONLIBS_FOUND) - -find_package(PythonInterp) -if (NOT PYTHONINTERP_FOUND) - message(STATUS "Python interpreter NOT found") -endif (NOT PYTHONINTERP_FOUND) - -if(PYTHONLIBS_FOUND AND PYTHONINTERP_FOUND) - set(DO_PYTHON_BINDINGS ON BOOL) - message(STATUS "Python bindings will be compiled") -endif(PYTHONLIBS_FOUND AND PYTHONINTERP_FOUND) - -if(DO_PYTHON_BINDINGS) - add_custom_target( python_bindings ALL ) - - add_custom_command( - TARGET python_bindings - POST_BUILD - - COMMAND echo "\\#!/usr/bin/python" > setup.py - COMMAND echo "from distutils.core import setup, Extension" >> setup.py - COMMAND echo "" >> setup.py - COMMAND echo "module1 = Extension\\(\\'libdedx\\'," >> setup.py - COMMAND echo " define_macros = [\\(\\'MAJOR_VERSION\\', \\'${DEDX_VERSION_MAJOR}\\'\\)," >> setup.py - COMMAND echo " \\(\\'MINOR_VERSION\\', \\'${DEDX_VERSION_MINOR}\\'\\)]," >> setup.py - COMMAND echo " include_dirs = [\\'${PROJECT_SOURCE_DIR}/libdedx\\']," >> setup.py - COMMAND echo " libraries = [\\'dedx\\']," >> setup.py - COMMAND echo " library_dirs = [\\'${PROJECT_BINARY_DIR}/libdedx\\']," >> setup.py - COMMAND echo " sources = [\\'libdedx.c\\']\\)" >> setup.py - COMMAND echo "setup \\(name = \\'libdedx\\'," >> setup.py - COMMAND echo " version = \\'${DEDX_VERSION_MAJOR}.${DEDX_VERSION_MINOR}.${DEDX_VERSION_PATCH}\\'," >> setup.py - COMMAND echo " description = \\'Wrapper for stopping power library libdedx\\'," >> setup.py - COMMAND echo " author = \\'Jakob Toftegaard\\'," >> setup.py - COMMAND echo " author_email = \\'jakob.toftegaard@gmail.com\\'," >> setup.py - COMMAND echo " url = \\'https://github.com/APTG/libdedx\\'," >> setup.py - COMMAND echo " long_description = \\'\\'," >> setup.py - COMMAND echo " ext_modules = [module1]\\)" >> setup.py - COMMAND echo "" >> setup.py - - COMMAND ${PYTHON_EXECUTABLE} setup.py build - COMMENT "Build python bindings..." - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python - DEPENDS dedx - ) -endif() - -#add_custom_target(python_bindings_install -# COMMAND echo Nb test2 -# COMMAND ${PYTHON_EXECUTABLE} setup.py install -# COMMENT "Install python bindings..." -# WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python -# DEPENDS dedx -#) - -# TODO: this simply runs python ./setup.py install after libdEdx was installed. -# This is not the proper way of doing it. -if(DO_PYTHON_BINDINGS) - install(CODE "execute_process(COMMAND echo Install python bindings...)") - install(CODE "execute_process(COMMAND ${PYTHON_EXECUTABLE} setup.py install WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python)") -endif() \ No newline at end of file diff --git a/python/README b/python/README deleted file mode 100644 index 053ac98..0000000 --- a/python/README +++ /dev/null @@ -1,17 +0,0 @@ -#install - -run -python setup.py build -as su -python setup.py install - -#module name -libdedx - -#functions -get_simple_dedx(ion,target,energy) - -# or - -a = libdedx.Libdedx(2,1,276) -a.get_stp(200) diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..5bcb3fb --- /dev/null +++ b/python/README.md @@ -0,0 +1,7 @@ +# libdedx Python binding + +This directory contains the `ctypes`-based Python binding for `libdedx`. + +The Python module loads the native `libdedx` shared library at runtime. +Build and install the C library first, or set `LIBDEDX_SO` to the path of +the built shared library during development and testing. diff --git a/python/libdedx.c b/python/libdedx.c deleted file mode 100644 index ff0e720..0000000 --- a/python/libdedx.c +++ /dev/null @@ -1,141 +0,0 @@ -#include -#include -#include - -#include "structmember.h" - -typedef struct { - PyObject_HEAD dedx_config *config; - dedx_workspace *ws; - int err; - -} Libdedx; - -static void Libdedx_dealloc(Libdedx *self) { - int err = 0; - dedx_free_workspace(self->ws, &err); - dedx_free_config(self->config, &err); - self->ob_type->tp_free((PyObject *) self); -} - -static PyObject *Libdedx_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - - Libdedx *self; - int program, ion, target; - static char *kwlist[] = { - "program", - "ion", - "target", - }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lll", kwlist, &program, &ion, &target)) - return NULL; - self = (Libdedx *) type->tp_alloc(type, 0); - self->ws = dedx_allocate_workspace(1, &(self->err)); - if (self->err != 0) - return NULL; - self->config = (dedx_config *) calloc(1, sizeof(dedx_config)); - dedx_config *config = self->config; - config->program = program; - config->ion = ion; - config->target = target; - dedx_load_config(self->ws, config, &(self->err)); - if (self->err != 0) - return NULL; - return (PyObject *) self; -} - -static int Libdedx_init(Libdedx *self, PyObject *args, PyObject *kwds) { - return 0; -} -static PyObject *Libdedx_get_stp(Libdedx *self, PyObject *args) { - float energy, stp; - PyArg_ParseTuple(args, "f", &energy); - stp = dedx_get_stp(self->ws, self->config, energy, &(self->err)); - if (self->err != 0) - return NULL; - return PyFloat_FromDouble((double) stp); -} -static PyObject *Libdedx_get_error(Libdedx *self) { - char *err = calloc(100, sizeof(char)); - PyObject *out; - if (self->err == 0) - err = "No error"; - else { - dedx_get_error_code(err, self->err); - } - out = PyString_FromString(err); - free(err); - return out; -} -static PyMemberDef Libdedx_members[] = { - {NULL} /* Sentinel */ -}; - -static PyMethodDef Libdedx_methods[] = { - {"get_stp", (PyCFunction) Libdedx_get_stp, METH_VARARGS, "Return stp"}, - {"get_error", (PyCFunction) Libdedx_get_error, METH_NOARGS, "Return error string"}, - {NULL} /* Sentinel */ -}; - -static PyTypeObject LibdedxType = { - PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ - "libdedx.Libdedx", /*tp_name*/ - sizeof(Libdedx), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor) Libdedx_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "Libdedx objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - Libdedx_methods, /* tp_methods */ - Libdedx_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc) Libdedx_init, /* tp_init */ - 0, /* tp_alloc */ - Libdedx_new, /* tp_new */ -}; - -static PyMethodDef module_methods[] = { - {NULL} /* Sentinel */ -}; - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif -PyMODINIT_FUNC initlibdedx(void) { - PyObject *m; - - if (PyType_Ready(&LibdedxType) < 0) - return; - - m = Py_InitModule3("libdedx", module_methods, "Example module that creates an extension type."); - - if (m == NULL) - return; - - Py_INCREF(&LibdedxType); - PyModule_AddObject(m, "Libdedx", (PyObject *) &LibdedxType); -} diff --git a/python/libdedx/__init__.py b/python/libdedx/__init__.py new file mode 100644 index 0000000..2f7f17a --- /dev/null +++ b/python/libdedx/__init__.py @@ -0,0 +1,11 @@ +"""Python package for the libdedx ctypes binding.""" + +from ._api import get_csda_table, get_default_table, get_stp, get_stp_table, get_version + +__all__ = [ + "get_version", + "get_stp", + "get_stp_table", + "get_default_table", + "get_csda_table", +] diff --git a/python/libdedx/__init__.pyi b/python/libdedx/__init__.pyi new file mode 100644 index 0000000..2635f7e --- /dev/null +++ b/python/libdedx/__init__.pyi @@ -0,0 +1,7 @@ +def get_version() -> str: ... +def get_stp(program: int, ion: int, target: int, energy: float) -> float: ... +def get_stp_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... +def get_default_table(program: int, ion: int, target: int) -> tuple[list[float], list[float]]: ... +def get_csda_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... + +__all__: list[str] diff --git a/python/libdedx/_api.py b/python/libdedx/_api.py new file mode 100644 index 0000000..0b6856d --- /dev/null +++ b/python/libdedx/_api.py @@ -0,0 +1,120 @@ +"""Public ctypes-backed API for libdedx.""" + +import ctypes +import ctypes.util +import os + + +def _load_library(): + path = os.environ.get("LIBDEDX_SO") + if path: + return ctypes.CDLL(path) + name = ctypes.util.find_library("dedx") + if name is None: + raise OSError( + "libdedx shared library not found. " + "Install libdedx or set LIBDEDX_SO to the path of the shared library." + ) + return ctypes.CDLL(name) + + +_lib = _load_library() + +_c_int_p = ctypes.POINTER(ctypes.c_int) +_c_float_p = ctypes.POINTER(ctypes.c_float) +_c_double_p = ctypes.POINTER(ctypes.c_double) + +_lib.dedx_get_simple_stp_for_program.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_float, _c_int_p, +] +_lib.dedx_get_simple_stp_for_program.restype = ctypes.c_float + +_lib.dedx_get_stp_table_size.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] +_lib.dedx_get_stp_table_size.restype = ctypes.c_int + +_lib.dedx_get_stp_table.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, + ctypes.c_int, _c_float_p, _c_float_p, +] +_lib.dedx_get_stp_table.restype = ctypes.c_int + +_lib.dedx_fill_default_energy_stp_table.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, _c_float_p, _c_float_p, +] +_lib.dedx_fill_default_energy_stp_table.restype = ctypes.c_int + +_lib.dedx_get_csda_range_table.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, + ctypes.c_int, _c_float_p, _c_double_p, +] +_lib.dedx_get_csda_range_table.restype = ctypes.c_int + +_lib.dedx_get_version.argtypes = [_c_int_p, _c_int_p, _c_int_p] +_lib.dedx_get_version.restype = None + + +def _check(err): + if err.value != 0: + raise RuntimeError(f"libdedx error code {err.value}") + + +def get_version(): + """Return the libdedx version as a ``major.minor.patch`` string.""" + major = ctypes.c_int(0) + minor = ctypes.c_int(0) + patch = ctypes.c_int(0) + _lib.dedx_get_version(ctypes.byref(major), ctypes.byref(minor), ctypes.byref(patch)) + return f"{major.value}.{minor.value}.{patch.value}" + + +def get_stp(program, ion, target, energy): + """Return mass stopping power in MeV cm²/g for a single energy in MeV/u.""" + err = ctypes.c_int(0) + result = _lib.dedx_get_simple_stp_for_program( + program, ion, target, float(energy), ctypes.byref(err) + ) + _check(err) + return float(result) + + +def get_stp_table(program, ion, target, energies): + """Return stopping powers (MeV cm²/g) for a list of energies (MeV/u).""" + n = len(energies) + e_arr = (ctypes.c_float * n)(*energies) + s_arr = (ctypes.c_float * n)() + ret = _lib.dedx_get_stp_table(program, ion, target, n, e_arr, s_arr) + if ret != 0: + raise RuntimeError(f"libdedx error code {ret}") + return list(s_arr) + + +def get_default_table(program, ion, target): + """Return (energies, stps) for the built-in tabulated data points. + + energies -- list of energies in MeV/u + stps -- list of stopping powers in MeV cm^2/g + """ + n = _lib.dedx_get_stp_table_size(program, ion, target) + if n < 0: + raise RuntimeError(f"libdedx error code {n}") + if n == 0: + raise RuntimeError( + f"No tabulated data for program={program}, ion={ion}, target={target}" + ) + e_arr = (ctypes.c_float * n)() + s_arr = (ctypes.c_float * n)() + ret = _lib.dedx_fill_default_energy_stp_table(program, ion, target, e_arr, s_arr) + if ret < 0: + raise RuntimeError(f"libdedx error code {ret}") + return list(e_arr), list(s_arr) + + +def get_csda_table(program, ion, target, energies): + """Return CSDA ranges (g/cm^2) for a list of energies (MeV/u).""" + n = len(energies) + e_arr = (ctypes.c_float * n)(*energies) + r_arr = (ctypes.c_double * n)() + ret = _lib.dedx_get_csda_range_table(program, ion, target, n, e_arr, r_arr) + if ret != 0: + raise RuntimeError(f"libdedx error code {ret}") + return list(r_arr) diff --git a/python/libdedx/py.typed b/python/libdedx/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..f8da0c0 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["setuptools>=68", "wheel", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "libdedx" +description = "Python ctypes binding for the libdedx stopping-power library" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "LGPL-2.0-or-later"} +dynamic = ["version"] + +[project.optional-dependencies] +test = ["pytest>=8"] +dev = ["ruff>=0.5.0", "pytest>=8"] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["."] +include = ["libdedx"] + +[tool.setuptools.package-data] +libdedx = ["__init__.pyi", "py.typed"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +line-length = 120 +target-version = "py39" + +[tool.ruff.lint] +select = ["E", "F", "W"] + +[tool.setuptools_scm] +root = ".." +version_scheme = "post-release" +local_scheme = "node-and-date" diff --git a/python/test.py b/python/test.py deleted file mode 100644 index f5f0e4e..0000000 --- a/python/test.py +++ /dev/null @@ -1,4 +0,0 @@ -from libdedx import Libdedx -a = Libdedx(program=2,ion=1,target=276) -for i in range(10000000): - a.get_stp(10) diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 0000000..c99c009 --- /dev/null +++ b/python/tests/conftest.py @@ -0,0 +1,31 @@ +import os +import sys +from pathlib import Path + + +def _candidate_library_paths(repo_root: Path) -> list[Path]: + if sys.platform.startswith("linux"): + lib_names = ["libdedx.so"] + elif sys.platform == "darwin": + lib_names = ["libdedx.dylib"] + elif os.name == "nt": + lib_names = ["dedx.dll", "libdedx.dll"] + else: + lib_names = ["libdedx.so"] + + candidates = [] + for build_dir in ("build", "build-debug", "build-release", "build-coverage"): + for lib_name in lib_names: + candidates.append(repo_root / build_dir / "libdedx" / lib_name) + return candidates + + +def pytest_configure() -> None: + if os.environ.get("LIBDEDX_SO"): + return + + repo_root = Path(__file__).resolve().parents[2] + for candidate in _candidate_library_paths(repo_root): + if candidate.exists(): + os.environ["LIBDEDX_SO"] = str(candidate) + return diff --git a/python/tests/test_libdedx.py b/python/tests/test_libdedx.py new file mode 100644 index 0000000..bb6399a --- /dev/null +++ b/python/tests/test_libdedx.py @@ -0,0 +1,52 @@ +""" +Basic smoke tests for the libdedx Python binding. + +Uses PSTAR (program=2), Hydrogen (ion=1), Liquid water (target=276) +as a reference combination with well-known stopping power values. +""" + +import libdedx + +PROGRAM = 2 # PSTAR +ION = 1 # Hydrogen (Z=1) +TARGET = 276 # Liquid water + + +def test_get_version(capsys): + version = libdedx.get_version() + print(version) + captured = capsys.readouterr() + assert version.count(".") == 2 + assert captured.out.strip() == version + + +def test_get_stp_returns_positive(): + stp = libdedx.get_stp(PROGRAM, ION, TARGET, 10.0) + assert stp > 0.0 + + +def test_get_stp_table(): + energies = [1.0, 10.0, 100.0] + stps = libdedx.get_stp_table(PROGRAM, ION, TARGET, energies) + assert len(stps) == len(energies) + assert all(s > 0.0 for s in stps) + + +def test_get_default_table_nonempty(): + energies, stps = libdedx.get_default_table(PROGRAM, ION, TARGET) + assert len(energies) > 0 + assert len(energies) == len(stps) + assert all(e > 0.0 for e in energies) + assert all(s > 0.0 for s in stps) + + +def test_get_csda_table(): + energies = [10.0, 100.0] + ranges = libdedx.get_csda_table(PROGRAM, ION, TARGET, energies) + assert len(ranges) == len(energies) + assert all(r > 0.0 for r in ranges) + + +# TODO: passing an invalid program number currently causes the library to +# segfault rather than returning an error code. A proper test can be added +# once input validation is implemented in the C library. diff --git a/tests/install_smoke_test.cmake b/tests/install_smoke_test.cmake index 143bdd8..7f8eaa8 100644 --- a/tests/install_smoke_test.cmake +++ b/tests/install_smoke_test.cmake @@ -49,7 +49,21 @@ if(NOT rc EQUAL 0) message(FATAL_ERROR "install smoke consumer compile failed: ${rc}") endif() -execute_process(COMMAND "${smoke_consumer}" +set(runtime_env) +if(APPLE) + list(APPEND runtime_env + "DYLD_LIBRARY_PATH=${smoke_prefix}/lib:$ENV{DYLD_LIBRARY_PATH}" + ) +else() + list(APPEND runtime_env + "LD_LIBRARY_PATH=${smoke_prefix}/lib:$ENV{LD_LIBRARY_PATH}" + ) +endif() + +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env + ${runtime_env} + "${smoke_consumer}" RESULT_VARIABLE rc) if(NOT rc EQUAL 0) message(FATAL_ERROR "install smoke consumer run failed: ${rc}") diff --git a/tests/test_version.c b/tests/test_version.c new file mode 100644 index 0000000..c867ec3 --- /dev/null +++ b/tests/test_version.c @@ -0,0 +1,12 @@ +#include +#include + +int main(void) { + int major = 0; + int minor = 0; + int patch = 0; + + dedx_get_version(&major, &minor, &patch); + printf("%d.%d.%d\n", major, minor, patch); + return 0; +}