From 0164f50182a608dc421216ccdc26ad245cc92d78 Mon Sep 17 00:00:00 2001 From: Hiroshi Shinaoka Date: Fri, 7 Nov 2025 08:20:36 +0900 Subject: [PATCH] feat: enforce automatic ctypes generation in CMake build - Make uv and clang REQUIRED for Python package build - Auto-generate ctypes_autogen.py during CMake build process - Improve gen_ctypes.py to correctly recognize int64_t and c_complex types - Fix c_double_complex type consistency between core.py and ctypes_autogen.py - Update test files to use c_double_complex for complex C-API functions - Add AGENTS.md documentation for Python interface build process The ctypes bindings are now automatically generated before building the library, ensuring they are always up-to-date with the C-API header. --- AGENTS.md | 128 ++++++++++ python/CMakeLists.txt | 60 +++++ python/pylibsparseir/core.py | 304 ++++++------------------ python/pylibsparseir/ctypes_autogen.py | 112 +++++++++ python/tests/c_api/core_tests.py | 30 +-- python/tests/c_api/dlr_tests.py | 6 +- python/tests/c_api/integration_tests.py | 18 +- python/tests/c_api/sampling_tests.py | 12 +- python/tools/gen_ctypes.py | 301 +++++++++++++++++++++++ 9 files changed, 701 insertions(+), 270 deletions(-) create mode 100644 AGENTS.md create mode 100644 python/pylibsparseir/ctypes_autogen.py create mode 100755 python/tools/gen_ctypes.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..29dc2c3e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,128 @@ +# AGENTS.md - AI Assistant Workflow for libsparseir + +This file provides guidance for AI assistants working on the libsparseir project, particularly for the Python interface. + +## Python Interface Build Process + +The Python interface (`libsparseir/python/`) uses `scikit-build-core` with CMake to build the C++ backend and generate Python bindings. + +### Prerequisites + +1. **uv** - Python package manager (REQUIRED) + ```bash + # Install uv + curl -LsSf https://astral.sh/uv/install.sh | sh + # Or on macOS: + brew install uv + ``` + +2. **clang (libclang)** - Required for auto-generating ctypes bindings + ```bash + # Install clang Python package via uv + cd libsparseir/python + uv sync --group dev + + # On macOS, also install llvm for libclang: + brew install llvm + # Set DYLD_LIBRARY_PATH if needed (see README.md) + ``` + +3. **CMake** (>= 3.25) - Build system +4. **C++ compiler** with C++11 support + +### Build Process + +The build process is automated through shell scripts: + +#### Option 1: Using `run_tests.sh` (Recommended for testing) + +This script performs a clean build and runs tests: + +```bash +cd libsparseir/python +bash run_tests.sh +``` + +What it does: +1. Cleans up previous build artifacts (`.venv`, `_skbuild`, `dist`, etc.) +2. Runs `setup_build.py` to copy source files +3. Runs `uv sync --refresh` to build the package (CMake will auto-generate `ctypes_autogen.py`) +4. Runs `uv run pytest tests/ -v` to execute tests + +#### Option 2: Manual build steps + +```bash +cd libsparseir/python + +# 1. Prepare build environment +python3 setup_build.py + +# 2. Build and install +uv sync --refresh + +# 3. Run tests +uv run pytest tests/ -v +``` + +### CMake Build Integration + +The CMake build process (`CMakeLists.txt`) automatically: + +1. **Requires `uv`** - The build will fail if `uv` is not found +2. **Requires `clang`** - The build will fail if `clang` Python package is not available +3. **Auto-generates `ctypes_autogen.py`** - The `generate_ctypes` target runs `tools/gen_ctypes.py` before building the library +4. **Installs generated file** - `pylibsparseir/ctypes_autogen.py` is installed as part of the package + +The generation happens via: +- Custom target: `generate_ctypes` +- Dependency: `sparseir` library depends on `generate_ctypes` +- Command: `uv run python tools/gen_ctypes.py` + +### Updating ctypes Bindings + +If you modify the C-API header (`sparseir.h`), you need to regenerate the bindings: + +```bash +cd libsparseir/python +bash update_wrapper.sh +``` + +This script: +1. Checks for header files (runs `setup_build.py` if needed) +2. Sets up `libclang` library path for macOS +3. Runs `uv run python tools/gen_ctypes.py` +4. Verifies the generated file + +**Note:** The bindings are automatically regenerated during CMake builds, so manual regeneration is only needed if you want to update them outside of a build. + +### Key Files + +- **`tools/gen_ctypes.py`** - Script to parse `sparseir.h` and generate `ctypes_autogen.py` +- **`pylibsparseir/ctypes_autogen.py`** - Auto-generated ctypes bindings (DO NOT EDIT MANUALLY) +- **`pylibsparseir/core.py`** - Python wrapper that uses the generated bindings +- **`CMakeLists.txt`** - CMake configuration that enforces generation during build +- **`run_tests.sh`** - Complete build and test script +- **`update_wrapper.sh`** - Script to manually regenerate bindings + +### Troubleshooting + +1. **`uv` not found**: Install `uv` using the commands above +2. **`clang` not found**: Run `uv sync --group dev` to install development dependencies +3. **`libclang.dylib` not found** (macOS): Install `llvm` via Homebrew and set `DYLD_LIBRARY_PATH` +4. **`ctypes_autogen.py` missing**: The file should be auto-generated during build. If missing, run `bash update_wrapper.sh` + +### Development Workflow + +1. Make changes to C-API (`backend/cxx/include/sparseir/sparseir.h`) +2. Run `bash update_wrapper.sh` to regenerate bindings (or let CMake do it) +3. Update Python wrapper code if needed (`pylibsparseir/core.py`) +4. Run `bash run_tests.sh` to build and test +5. Commit both source changes and `ctypes_autogen.py` (the generated file should be committed) + +### Important Notes + +- **Never edit `ctypes_autogen.py` manually** - It is auto-generated and will be overwritten +- **Always commit `ctypes_autogen.py`** - This ensures the package can be built even without `clang` +- **Use `uv` for all Python operations** - This ensures consistent environment +- **CMake build requires both `uv` and `clang`** - The build will fail if either is missing + diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 1173e72c..7080ec03 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -135,6 +135,59 @@ target_link_libraries(sparseir PRIVATE Eigen3::Eigen Threads::Threads) add_library(SparseIR::sparseir ALIAS sparseir) # ------------------------------------- +# Generate ctypes bindings (REQUIRED) +find_package(Python3 COMPONENTS Interpreter REQUIRED) + +# uv is REQUIRED for building this package +# It ensures we use the correct Python environment with clang installed +find_program(UV_EXECUTABLE uv REQUIRED + PATHS + ${CMAKE_CURRENT_SOURCE_DIR}/.venv/bin + $ENV{HOME}/.cargo/bin + /opt/homebrew/bin + /usr/local/bin + DOC "uv executable (REQUIRED for ctypes generation)" +) + +# Use uv run python to ensure we're using the correct environment +# This is REQUIRED - we don't fall back to system Python +set(PYTHON_GEN_CMD ${UV_EXECUTABLE} run python) +execute_process( + COMMAND ${UV_EXECUTABLE} run python -c "import clang; import clang.cindex" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE CLANG_CHECK_RESULT + OUTPUT_QUIET + ERROR_QUIET +) + +if(CLANG_CHECK_RESULT EQUAL 0) + # Generate ctypes bindings - this is REQUIRED + # Use add_custom_command with OUTPUT to ensure the file is generated + set(CTYPES_AUTOGEN_FILE ${CMAKE_CURRENT_SOURCE_DIR}/pylibsparseir/ctypes_autogen.py) + + add_custom_command( + OUTPUT ${CTYPES_AUTOGEN_FILE} + COMMAND ${PYTHON_GEN_CMD} ${CMAKE_CURRENT_SOURCE_DIR}/tools/gen_ctypes.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating ctypes bindings from C-API header (REQUIRED)" + VERBATIM + ) + + # Create a target that depends on the generated file + add_custom_target(generate_ctypes DEPENDS ${CTYPES_AUTOGEN_FILE}) + + # Make sure bindings are generated before building the library + add_dependencies(sparseir generate_ctypes) +else() + message(FATAL_ERROR + "clang (libclang) package is REQUIRED for generating ctypes bindings.\n" + "Install with: pip install clang\n" + "Or on macOS: brew install llvm && pip install clang\n" + "Then set DYLD_LIBRARY_PATH if needed (see README.md)\n" + "The ctypes bindings MUST be generated during the build process." + ) +endif() + # Installation # Install the shared library to the Python package directory @@ -155,3 +208,10 @@ install(FILES DESTINATION pylibsparseir COMPONENT sparseir ) + +# Install generated ctypes bindings (REQUIRED - generated by generate_ctypes target) +install(FILES + pylibsparseir/ctypes_autogen.py + DESTINATION pylibsparseir + COMPONENT sparseir +) diff --git a/python/pylibsparseir/core.py b/python/pylibsparseir/core.py index 7b70457e..0afee144 100644 --- a/python/pylibsparseir/core.py +++ b/python/pylibsparseir/core.py @@ -95,236 +95,62 @@ class c_double_complex(ctypes.Structure): def value(self): return self.real+1j*self.imag # fields declared above -# Set up function prototypes +# Set up function prototypes using auto-generated bindings +try: + from .ctypes_autogen import FUNCTIONS, c_double_complex as _autogen_c_double_complex + # Use the generated c_double_complex if available, otherwise use the one defined below + # (They should be identical, but we keep the local definition for backward compatibility) +except ImportError: + # Fallback: if autogen file doesn't exist, use manual setup + FUNCTIONS = {} + print("WARNING: ctypes_autogen.py not found. Run tools/gen_ctypes.py to generate it.") def _setup_prototypes(): - # Kernel functions - _lib.spir_logistic_kernel_new.argtypes = [c_double, POINTER(c_int)] - _lib.spir_logistic_kernel_new.restype = spir_kernel - - _lib.spir_reg_bose_kernel_new.argtypes = [c_double, POINTER(c_int)] - _lib.spir_reg_bose_kernel_new.restype = spir_kernel - - _lib.spir_kernel_domain.argtypes = [ - spir_kernel, POINTER(c_double), POINTER(c_double), - POINTER(c_double), POINTER(c_double) - ] - _lib.spir_kernel_domain.restype = c_int - - # SVE result functions - _lib.spir_sve_result_new.argtypes = [ - spir_kernel, c_double, c_double, c_int, c_int, c_int, POINTER(c_int)] - _lib.spir_sve_result_new.restype = spir_sve_result - - _lib.spir_sve_result_get_size.argtypes = [spir_sve_result, POINTER(c_int)] - _lib.spir_sve_result_get_size.restype = c_int - - _lib.spir_sve_result_get_svals.argtypes = [ - spir_sve_result, POINTER(c_double)] - _lib.spir_sve_result_get_svals.restype = c_int - - # Basis functions - _lib.spir_basis_new.argtypes = [ - c_int, c_double, c_double, c_double, spir_kernel, spir_sve_result, c_int, POINTER(c_int) - ] - _lib.spir_basis_new.restype = spir_basis - - _lib.spir_basis_get_size.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_basis_get_size.restype = c_int - - _lib.spir_basis_get_svals.argtypes = [spir_basis, POINTER(c_double)] - _lib.spir_basis_get_svals.restype = c_int - - _lib.spir_basis_get_stats.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_basis_get_stats.restype = c_int - - # Basis function objects - _lib.spir_basis_get_u.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_basis_get_u.restype = spir_funcs - - _lib.spir_basis_get_v.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_basis_get_v.restype = spir_funcs - - _lib.spir_basis_get_uhat.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_basis_get_uhat.restype = spir_funcs - - _lib.spir_funcs_get_slice.argtypes = [ - spir_funcs, c_int, POINTER(c_int), POINTER(c_int)] - _lib.spir_funcs_get_slice.restype = spir_funcs - - # Function evaluation - _lib.spir_funcs_get_size.argtypes = [spir_funcs, POINTER(c_int)] - _lib.spir_funcs_get_size.restype = c_int - - _lib.spir_funcs_eval.argtypes = [spir_funcs, c_double, POINTER(c_double)] - _lib.spir_funcs_eval.restype = c_int - - _lib.spir_funcs_eval_matsu.argtypes = [ - spir_funcs, c_int64, POINTER(c_double_complex)] - _lib.spir_funcs_eval_matsu.restype = c_int - - _lib.spir_funcs_batch_eval.argtypes = [ - spir_funcs, c_int, c_size_t, POINTER(c_double), POINTER(c_double) - ] - _lib.spir_funcs_batch_eval.restype = c_int - - _lib.spir_funcs_batch_eval_matsu.argtypes = [ - spir_funcs, c_int, c_int, POINTER(c_int64), POINTER(c_double) - ] - _lib.spir_funcs_batch_eval_matsu.restype = c_int - - _lib.spir_funcs_get_n_knots.argtypes = [spir_funcs, POINTER(c_int)] - _lib.spir_funcs_get_n_knots.restype = c_int - - _lib.spir_funcs_get_knots.argtypes = [spir_funcs, POINTER(c_double)] - _lib.spir_funcs_get_knots.restype = c_int - - # Default sampling points - _lib.spir_basis_get_n_default_taus.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_basis_get_n_default_taus.restype = c_int - - _lib.spir_basis_get_default_taus.argtypes = [spir_basis, POINTER(c_double)] - _lib.spir_basis_get_default_taus.restype = c_int - - _lib.spir_basis_get_default_taus_ext.argtypes = [ - spir_basis, c_int, POINTER(c_double), POINTER(c_int)] - _lib.spir_basis_get_default_taus_ext.restype = c_int - - _lib.spir_basis_get_n_default_ws.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_basis_get_n_default_ws.restype = c_int - - _lib.spir_basis_get_default_ws.argtypes = [spir_basis, POINTER(c_double)] - _lib.spir_basis_get_default_ws.restype = c_int - - _lib.spir_basis_get_n_default_matsus.argtypes = [ - spir_basis, c_bool, POINTER(c_int)] - _lib.spir_basis_get_n_default_matsus.restype = c_int - - _lib.spir_basis_get_n_default_matsus_ext.argtypes = [ - spir_basis, c_bool, c_int, POINTER(c_int)] - _lib.spir_basis_get_n_default_matsus_ext.restype = c_int - - _lib.spir_basis_get_default_matsus.argtypes = [ - spir_basis, c_bool, POINTER(c_int64)] - _lib.spir_basis_get_default_matsus.restype = c_int - - _lib.spir_basis_get_default_matsus_ext.argtypes = [ - spir_basis, c_bool, c_int, POINTER(c_int64), POINTER(c_int)] - _lib.spir_basis_get_default_matsus_ext.restype = c_int - - # Sampling objects - _lib.spir_tau_sampling_new.argtypes = [ - spir_basis, c_int, POINTER(c_double), POINTER(c_int)] - _lib.spir_tau_sampling_new.restype = spir_sampling - - _lib.spir_tau_sampling_new_with_matrix.argtypes = [ - c_int, c_int, c_int, c_int, POINTER(c_double), POINTER(c_double), POINTER(c_int)] - _lib.spir_tau_sampling_new_with_matrix.restype = spir_sampling - - _lib.spir_matsu_sampling_new.argtypes = [ - spir_basis, c_bool, c_int, POINTER(c_int64), POINTER(c_int)] - _lib.spir_matsu_sampling_new.restype = spir_sampling - - _lib.spir_matsu_sampling_new_with_matrix.argtypes = [ - c_int, # order - c_int, # statistics - c_int, # basis_size - c_bool, # positive_only - c_int, # num_points - POINTER(c_int64), # points - POINTER(c_double_complex), # matrix - POINTER(c_int) # status - ] - _lib.spir_matsu_sampling_new_with_matrix.restype = spir_sampling - - # Sampling operations - _lib.spir_sampling_eval_dd.argtypes = [ - spir_sampling, c_int, c_int, POINTER(c_int), c_int, - POINTER(c_double), POINTER(c_double) - ] - _lib.spir_sampling_eval_dd.restype = c_int - - _lib.spir_sampling_fit_dd.argtypes = [ - spir_sampling, c_int, c_int, POINTER(c_int), c_int, - POINTER(c_double), POINTER(c_double) - ] - _lib.spir_sampling_fit_dd.restype = c_int - - # Additional sampling functions - _lib.spir_sampling_get_npoints.argtypes = [spir_sampling, POINTER(c_int)] - _lib.spir_sampling_get_npoints.restype = c_int - - _lib.spir_sampling_get_taus.argtypes = [spir_sampling, POINTER(c_double)] - _lib.spir_sampling_get_taus.restype = c_int - - _lib.spir_sampling_get_matsus.argtypes = [spir_sampling, POINTER(c_int64)] - _lib.spir_sampling_get_matsus.restype = c_int - - _lib.spir_sampling_get_cond_num.argtypes = [ - spir_sampling, POINTER(c_double)] - _lib.spir_sampling_get_cond_num.restype = c_int - - # Multi-dimensional sampling evaluation functions - _lib.spir_sampling_eval_dz.argtypes = [ - spir_sampling, c_int, c_int, POINTER(c_int), c_int, - POINTER(c_double), POINTER(c_double_complex) - ] - _lib.spir_sampling_eval_dz.restype = c_int - - _lib.spir_sampling_eval_zz.argtypes = [ - spir_sampling, c_int, c_int, POINTER(c_int), c_int, - POINTER(c_double_complex), POINTER(c_double_complex) - ] - _lib.spir_sampling_eval_zz.restype = c_int - - _lib.spir_sampling_fit_zz.argtypes = [ - spir_sampling, c_int, c_int, POINTER(c_int), c_int, - POINTER(c_double_complex), POINTER(c_double_complex) - ] - _lib.spir_sampling_fit_zz.restype = c_int - - # DLR functions - _lib.spir_dlr_new.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_dlr_new.restype = spir_basis - - _lib.spir_dlr_new_with_poles.argtypes = [ - spir_basis, c_int, POINTER(c_double), POINTER(c_int)] - _lib.spir_dlr_new_with_poles.restype = spir_basis - - _lib.spir_dlr_get_npoles.argtypes = [spir_basis, POINTER(c_int)] - _lib.spir_dlr_get_npoles.restype = c_int - - _lib.spir_dlr_get_poles.argtypes = [spir_basis, POINTER(c_double)] - _lib.spir_dlr_get_poles.restype = c_int - - _lib.spir_dlr2ir_dd.argtypes = [ - spir_basis, c_int, c_int, POINTER(c_int), c_int, - POINTER(c_double), POINTER(c_double) - ] - _lib.spir_dlr2ir_dd.restype = c_int - - _lib.spir_dlr2ir_zz.argtypes = [ - spir_basis, c_int, c_int, POINTER(c_int), c_int, - POINTER(c_double), POINTER(c_double) - ] - _lib.spir_dlr2ir_zz.restype = c_int - - # Release functions - _lib.spir_kernel_release.argtypes = [spir_kernel] - _lib.spir_kernel_release.restype = None - - _lib.spir_sve_result_release.argtypes = [spir_sve_result] - _lib.spir_sve_result_release.restype = None - - _lib.spir_basis_release.argtypes = [spir_basis] - _lib.spir_basis_release.restype = None - - _lib.spir_funcs_release.argtypes = [spir_funcs] - _lib.spir_funcs_release.restype = None - - _lib.spir_sampling_release.argtypes = [spir_sampling] - _lib.spir_sampling_release.restype = None + """Set up function prototypes from auto-generated bindings.""" + if not FUNCTIONS: + # Fallback to manual setup if generation failed + return + + # Import necessary types into local namespace for eval + from ctypes import c_int, c_double, c_int64, c_size_t, c_bool, POINTER, c_char_p + from .ctypes_wrapper import spir_kernel, spir_funcs, spir_basis, spir_sampling, spir_sve_result + # Use the c_double_complex from this module (core.py), not from ctypes_autogen + # This ensures type consistency + + # Type mapping for eval + type_map = { + 'c_int': c_int, 'c_double': c_double, 'c_int64': c_int64, + 'c_size_t': c_size_t, 'c_bool': c_bool, + 'POINTER': POINTER, 'c_char_p': c_char_p, + 'spir_kernel': spir_kernel, 'spir_funcs': spir_funcs, + 'spir_basis': spir_basis, 'spir_sampling': spir_sampling, + 'spir_sve_result': spir_sve_result, + 'c_double_complex': c_double_complex, # Use the one defined in this module + } + + # Apply generated prototypes to the library + for name, (restype_str, argtypes_list) in FUNCTIONS.items(): + if not hasattr(_lib, name): + continue + + func = getattr(_lib, name) + try: + # Evaluate restype + if restype_str == 'None': + func.restype = None + else: + func.restype = eval(restype_str, globals(), type_map) + + # Evaluate argtypes + evaluated_argtypes = [] + for argtype_str in argtypes_list: + evaluated_argtypes.append(eval(argtype_str, globals(), type_map)) + func.argtypes = evaluated_argtypes + except (NameError, AttributeError, SyntaxError) as e: + # Skip functions that can't be evaluated (might be missing types) + if os.environ.get("SPARSEIR_DEBUG", "").lower() in ("1", "true", "yes", "on"): + print(f"WARNING: Could not set prototype for {name}: {e}") _setup_prototypes() @@ -352,14 +178,17 @@ def reg_bose_kernel_new(lambda_val): def sve_result_new(kernel, epsilon, cutoff=None, lmax=None, n_gauss=None, Twork=None): - """Create a new SVE result.""" + """Create a new SVE result. + + Note: cutoff parameter is deprecated and ignored (C-API doesn't have it). + It's kept for backward compatibility but not passed to C-API. + """ # Validate epsilon if epsilon <= 0: raise RuntimeError( f"Failed to create SVE result: epsilon must be positive, got {epsilon}") - if cutoff is None: - cutoff = -1.0 + # Note: cutoff parameter was removed from C-API, kept for backward compatibility if lmax is None: lmax = -1 if n_gauss is None: @@ -368,8 +197,9 @@ def sve_result_new(kernel, epsilon, cutoff=None, lmax=None, n_gauss=None, Twork= Twork = SPIR_TWORK_FLOAT64X2 status = c_int() + # C-API signature: spir_sve_result_new(kernel, epsilon, lmax, n_gauss, Twork, status) sve = _lib.spir_sve_result_new( - kernel, epsilon, cutoff, lmax, n_gauss, Twork, byref(status)) + kernel, c_double(epsilon), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) if status.value != COMPUTATION_SUCCESS: raise RuntimeError(f"Failed to create SVE result: {status.value}") return sve @@ -600,15 +430,15 @@ def basis_get_default_matsubara_sampling_points(basis, positive_only=False): # Get number of points n_points = c_int() status = _lib.spir_basis_get_n_default_matsus( - basis, c_bool(positive_only), byref(n_points)) + basis, c_int(1 if positive_only else 0), byref(n_points)) if status != COMPUTATION_SUCCESS: raise RuntimeError( f"Failed to get number of default Matsubara points: {status}") # Get the points points = np.zeros(n_points.value, dtype=np.int64) - status = _lib.spir_basis_get_default_matsus(basis, c_bool( - positive_only), points.ctypes.data_as(POINTER(c_int64))) + status = _lib.spir_basis_get_default_matsus(basis, c_int( + 1 if positive_only else 0), points.ctypes.data_as(POINTER(c_int64))) if status != COMPUTATION_SUCCESS: raise RuntimeError(f"Failed to get default Matsubara points: {status}") @@ -619,7 +449,7 @@ def basis_get_n_default_matsus_ext(basis, n_points, positive_only): """Get the number of default Matsubara sampling points for a basis.""" n_points_returned = c_int() status = _lib.spir_basis_get_n_default_matsus_ext( - basis, c_bool(positive_only), n_points, byref(n_points_returned)) + basis, c_int(1 if positive_only else 0), n_points, byref(n_points_returned)) if status != COMPUTATION_SUCCESS: raise RuntimeError( f"Failed to get number of default Matsubara points: {status}") @@ -629,8 +459,8 @@ def basis_get_n_default_matsus_ext(basis, n_points, positive_only): def basis_get_default_matsus_ext(basis, positive_only, points): n_points = len(points) n_points_returned = c_int() - status = _lib.spir_basis_get_default_matsus_ext(basis, c_bool( - positive_only), n_points, points.ctypes.data_as(POINTER(c_int64)), byref(n_points_returned)) + status = _lib.spir_basis_get_default_matsus_ext(basis, c_int( + 1 if positive_only else 0), c_int(0), n_points, points.ctypes.data_as(POINTER(c_int64)), byref(n_points_returned)) if status != COMPUTATION_SUCCESS: raise RuntimeError(f"Failed to get default Matsubara points: {status}") return points diff --git a/python/pylibsparseir/ctypes_autogen.py b/python/pylibsparseir/ctypes_autogen.py new file mode 100644 index 00000000..4e776b47 --- /dev/null +++ b/python/pylibsparseir/ctypes_autogen.py @@ -0,0 +1,112 @@ +""" +Auto-generated ctypes bindings from C-API header. +DO NOT EDIT THIS FILE MANUALLY. +Generated by tools/gen_ctypes.py +""" + +import ctypes +from ctypes import ( + c_int, c_double, c_int64, c_size_t, c_bool, + POINTER, c_char_p, Structure, +) + +from .ctypes_wrapper import ( + spir_kernel, spir_funcs, spir_basis, + spir_sampling, spir_sve_result, +) + +# Custom complex type (matches core.py definition) +class c_double_complex(Structure): + """Complex number as ctypes Structure.""" + _fields_ = [("real", ctypes.c_double), ("imag", ctypes.c_double)] + + @property + def value(self): + return self.real + 1j * self.imag + + +# Function prototypes: {name: (restype, [argtypes])} +FUNCTIONS = { + 'spir_basis_clone': ('spir_basis', ['spir_basis']), + 'spir_basis_get_default_matsus': ('c_int', ['spir_basis', 'c_int', 'POINTER(c_int64)']), + 'spir_basis_get_default_matsus_ext': ('c_int', ['spir_basis', 'c_int', 'c_int', 'c_int', 'POINTER(c_int64)', 'POINTER(c_int)']), + 'spir_basis_get_default_taus': ('c_int', ['spir_basis', 'POINTER(c_double)']), + 'spir_basis_get_default_taus_ext': ('c_int', ['spir_basis', 'c_int', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_basis_get_default_ws': ('c_int', ['spir_basis', 'POINTER(c_double)']), + 'spir_basis_get_n_default_matsus': ('c_int', ['spir_basis', 'c_int', 'POINTER(c_int)']), + 'spir_basis_get_n_default_matsus_ext': ('c_int', ['spir_basis', 'c_int', 'c_int', 'POINTER(c_int)']), + 'spir_basis_get_n_default_taus': ('c_int', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_get_n_default_ws': ('c_int', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_get_singular_values': ('c_int', ['spir_basis', 'POINTER(c_double)']), + 'spir_basis_get_size': ('c_int', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_get_stats': ('c_int', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_get_svals': ('c_int', ['spir_basis', 'POINTER(c_double)']), + 'spir_basis_get_u': ('spir_funcs', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_get_uhat': ('spir_funcs', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_get_uhat_full': ('spir_funcs', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_get_v': ('spir_funcs', ['spir_basis', 'POINTER(c_int)']), + 'spir_basis_is_assigned': ('c_int', ['spir_basis']), + 'spir_basis_new': ('spir_basis', ['c_int', 'c_double', 'c_double', 'c_double', 'spir_kernel', 'spir_sve_result', 'c_int', 'POINTER(c_int)']), + 'spir_basis_new_from_sve_and_inv_weight': ('spir_basis', ['c_int', 'c_double', 'c_double', 'c_double', 'c_double', 'c_int', 'c_double', 'spir_sve_result', 'spir_funcs', 'c_int', 'POINTER(c_int)']), + 'spir_basis_release': ('None', ['spir_basis']), + 'spir_choose_working_type': ('c_int', ['c_double']), + 'spir_dlr2ir_dd': ('c_int', ['spir_basis', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)']), + 'spir_dlr2ir_zz': ('c_int', ['spir_basis', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double_complex)', 'POINTER(c_double_complex)']), + 'spir_dlr_get_npoles': ('c_int', ['spir_basis', 'POINTER(c_int)']), + 'spir_dlr_get_poles': ('c_int', ['spir_basis', 'POINTER(c_double)']), + 'spir_dlr_new': ('spir_basis', ['spir_basis', 'POINTER(c_int)']), + 'spir_dlr_new_with_poles': ('spir_basis', ['spir_basis', 'c_int', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_funcs_batch_eval': ('c_int', ['spir_funcs', 'c_int', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)']), + 'spir_funcs_batch_eval_matsu': ('c_int', ['spir_funcs', 'c_int', 'c_int', 'POINTER(c_int64)', 'POINTER(c_double_complex)']), + 'spir_funcs_clone': ('spir_funcs', ['spir_funcs']), + 'spir_funcs_eval': ('c_int', ['spir_funcs', 'c_double', 'POINTER(c_double)']), + 'spir_funcs_eval_matsu': ('c_int', ['spir_funcs', 'c_int64', 'POINTER(c_double_complex)']), + 'spir_funcs_from_piecewise_legendre': ('spir_funcs', ['POINTER(c_double)', 'c_int', 'POINTER(c_double)', 'c_int', 'c_int', 'POINTER(c_int)']), + 'spir_funcs_get_knots': ('c_int', ['spir_funcs', 'POINTER(c_double)']), + 'spir_funcs_get_n_knots': ('c_int', ['spir_funcs', 'POINTER(c_int)']), + 'spir_funcs_get_size': ('c_int', ['spir_funcs', 'POINTER(c_int)']), + 'spir_funcs_get_slice': ('spir_funcs', ['spir_funcs', 'c_int', 'POINTER(c_int)', 'POINTER(c_int)']), + 'spir_funcs_is_assigned': ('c_int', ['spir_funcs']), + 'spir_funcs_release': ('None', ['spir_funcs']), + 'spir_gauss_legendre_rule_piecewise_ddouble': ('c_int', ['c_int', 'POINTER(c_double)', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_gauss_legendre_rule_piecewise_double': ('c_int', ['c_int', 'POINTER(c_double)', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_ir2dlr_dd': ('c_int', ['spir_basis', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)']), + 'spir_ir2dlr_zz': ('c_int', ['spir_basis', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double_complex)', 'POINTER(c_double_complex)']), + 'spir_kernel_clone': ('spir_kernel', ['spir_kernel']), + 'spir_kernel_domain': ('c_int', ['spir_kernel', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_double)']), + 'spir_kernel_get_sve_hints_ngauss': ('c_int', ['spir_kernel', 'c_double', 'POINTER(c_int)']), + 'spir_kernel_get_sve_hints_nsvals': ('c_int', ['spir_kernel', 'c_double', 'POINTER(c_int)']), + 'spir_kernel_get_sve_hints_segments_x': ('c_int', ['spir_kernel', 'c_double', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_kernel_get_sve_hints_segments_y': ('c_int', ['spir_kernel', 'c_double', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_kernel_is_assigned': ('c_int', ['spir_kernel']), + 'spir_kernel_release': ('None', ['spir_kernel']), + 'spir_logistic_kernel_new': ('spir_kernel', ['c_double', 'POINTER(c_int)']), + 'spir_matsu_sampling_new': ('spir_sampling', ['spir_basis', 'c_int', 'c_int', 'POINTER(c_int64)', 'POINTER(c_int)']), + 'spir_matsu_sampling_new_with_matrix': ('spir_sampling', ['c_int', 'c_int', 'c_int', 'c_int', 'c_int', 'POINTER(c_int64)', 'POINTER(c_double_complex)', 'POINTER(c_int)']), + 'spir_reg_bose_kernel_new': ('spir_kernel', ['c_double', 'POINTER(c_int)']), + 'spir_sampling_clone': ('spir_sampling', ['spir_sampling']), + 'spir_sampling_eval_dd': ('c_int', ['spir_sampling', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)']), + 'spir_sampling_eval_dz': ('c_int', ['spir_sampling', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double)', 'POINTER(c_double_complex)']), + 'spir_sampling_eval_zz': ('c_int', ['spir_sampling', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double_complex)', 'POINTER(c_double_complex)']), + 'spir_sampling_fit_dd': ('c_int', ['spir_sampling', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)']), + 'spir_sampling_fit_zd': ('c_int', ['spir_sampling', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double_complex)', 'POINTER(c_double)']), + 'spir_sampling_fit_zz': ('c_int', ['spir_sampling', 'c_int', 'c_int', 'POINTER(c_int)', 'c_int', 'POINTER(c_double_complex)', 'POINTER(c_double_complex)']), + 'spir_sampling_get_cond_num': ('c_int', ['spir_sampling', 'POINTER(c_double)']), + 'spir_sampling_get_matsus': ('c_int', ['spir_sampling', 'POINTER(c_int64)']), + 'spir_sampling_get_npoints': ('c_int', ['spir_sampling', 'POINTER(c_int)']), + 'spir_sampling_get_taus': ('c_int', ['spir_sampling', 'POINTER(c_double)']), + 'spir_sampling_is_assigned': ('c_int', ['spir_sampling']), + 'spir_sampling_release': ('None', ['spir_sampling']), + 'spir_sve_result_clone': ('spir_sve_result', ['spir_sve_result']), + 'spir_sve_result_from_matrix': ('spir_sve_result', ['POINTER(c_double)', 'POINTER(c_double)', 'c_int', 'c_int', 'c_int', 'POINTER(c_double)', 'c_int', 'POINTER(c_double)', 'c_int', 'c_int', 'c_double', 'POINTER(c_int)']), + 'spir_sve_result_from_matrix_centrosymmetric': ('spir_sve_result', ['POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_double)', 'c_int', 'c_int', 'c_int', 'POINTER(c_double)', 'c_int', 'POINTER(c_double)', 'c_int', 'c_int', 'c_double', 'POINTER(c_int)']), + 'spir_sve_result_get_size': ('c_int', ['spir_sve_result', 'POINTER(c_int)']), + 'spir_sve_result_get_svals': ('c_int', ['spir_sve_result', 'POINTER(c_double)']), + 'spir_sve_result_is_assigned': ('c_int', ['spir_sve_result']), + 'spir_sve_result_new': ('spir_sve_result', ['spir_kernel', 'c_double', 'c_int', 'c_int', 'c_int', 'POINTER(c_int)']), + 'spir_sve_result_release': ('None', ['spir_sve_result']), + 'spir_sve_result_truncate': ('spir_sve_result', ['spir_sve_result', 'c_double', 'c_int', 'POINTER(c_int)']), + 'spir_tau_sampling_new': ('spir_sampling', ['spir_basis', 'c_int', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_tau_sampling_new_with_matrix': ('spir_sampling', ['c_int', 'c_int', 'c_int', 'c_int', 'POINTER(c_double)', 'POINTER(c_double)', 'POINTER(c_int)']), + 'spir_uhat_get_default_matsus': ('c_int', ['spir_funcs', 'c_int', 'c_int', 'c_int', 'POINTER(c_int64)', 'POINTER(c_int)']), +} diff --git a/python/tests/c_api/core_tests.py b/python/tests/c_api/core_tests.py index 350e02e9..344785c6 100644 --- a/python/tests/c_api/core_tests.py +++ b/python/tests/c_api/core_tests.py @@ -57,11 +57,11 @@ def test_sve_computation(self): assert status.value == COMPUTATION_SUCCESS # Compute SVE - cutoff = -1.0 + # Note: cutoff parameter was removed from C-API lmax = -1 n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 - sve = _lib.spir_sve_result_new(kernel, c_double(1e-6), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) + sve = _lib.spir_sve_result_new(kernel, c_double(1e-6), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) assert status.value == COMPUTATION_SUCCESS assert sve is not None @@ -96,11 +96,11 @@ def test_basis_constructors(self): assert status.value == COMPUTATION_SUCCESS # Compute SVE - cutoff = -1.0 + # Note: cutoff parameter was removed from C-API lmax = -1 n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 - sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) + sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) assert status.value == COMPUTATION_SUCCESS # Create basis @@ -145,7 +145,7 @@ def test_tau_sampling_creation_and_properties(self): n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 max_size = -1 - sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) + sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) basis = _lib.spir_basis_new(c_int(SPIR_STATISTICS_FERMIONIC), c_double(beta), c_double(wmax), c_double(1e-10), kernel, sve, max_size, byref(status)) @@ -190,23 +190,23 @@ def test_matsubara_sampling_creation(self): lmax = -1 n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 - sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) + sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) basis = _lib.spir_basis_new(c_int(SPIR_STATISTICS_FERMIONIC), c_double(beta), c_double(wmax), c_double(1e-10), kernel, sve, max_size, byref(status)) # Get default Matsubara points n_matsu = c_int() - status_val = _lib.spir_basis_get_n_default_matsus(basis, c_bool(False), byref(n_matsu)) + status_val = _lib.spir_basis_get_n_default_matsus(basis, c_int(0), byref(n_matsu)) assert status_val == COMPUTATION_SUCCESS assert n_matsu.value > 0 default_matsus = np.zeros(n_matsu.value, dtype=np.int64) - status_val = _lib.spir_basis_get_default_matsus(basis, c_bool(False), + status_val = _lib.spir_basis_get_default_matsus(basis, c_int(0), default_matsus.ctypes.data_as(POINTER(c_int64))) assert status_val == COMPUTATION_SUCCESS # Create Matsubara sampling - matsu_sampling = _lib.spir_matsu_sampling_new(basis, c_bool(False), n_matsu.value, + matsu_sampling = _lib.spir_matsu_sampling_new(basis, c_int(0), n_matsu.value, default_matsus.ctypes.data_as(POINTER(c_int64)), byref(status)) assert status.value == COMPUTATION_SUCCESS @@ -231,7 +231,7 @@ def test_basis_functions_u(self): n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 max_size = -1 - sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) + sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) basis = _lib.spir_basis_new(c_int(SPIR_STATISTICS_FERMIONIC), c_double(beta), c_double(wmax), c_double(1e-10), kernel, sve, max_size, byref(status)) @@ -262,7 +262,7 @@ def test_memory_management(self): lmax = -1 n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 - sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) + sve = _lib.spir_sve_result_new(kernel, c_double(1e-10), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) basis = _lib.spir_basis_new(c_int(SPIR_STATISTICS_FERMIONIC), c_double(10.0), c_double(1.0), c_double(1e-10), kernel, sve, -1, byref(status)) @@ -300,11 +300,11 @@ def _spir_basis_new(self, statistics, beta, wmax, epsilon): return None, status.value # Create SVE result - cutoff = -1.0 + # Note: cutoff parameter was removed from C-API lmax = -1 n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 - sve = _lib.spir_sve_result_new(kernel, c_double(epsilon), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) + sve = _lib.spir_sve_result_new(kernel, c_double(epsilon), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(status)) if status.value != COMPUTATION_SUCCESS or sve is None: _lib.spir_kernel_release(kernel) return None, status.value @@ -476,12 +476,12 @@ def test_basis_constructor_with_sve_patterns(self): assert kernel is not None # Create SVE result - cutoff = -1.0 + # Note: cutoff parameter was removed from C-API lmax = -1 n_gauss = -1 Twork = SPIR_TWORK_FLOAT64X2 sve_status = c_int() - sve_result = _lib.spir_sve_result_new(kernel, c_double(epsilon), c_double(cutoff), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(sve_status)) + sve_result = _lib.spir_sve_result_new(kernel, c_double(epsilon), c_int(lmax), c_int(n_gauss), c_int(Twork), byref(sve_status)) assert sve_status.value == COMPUTATION_SUCCESS assert sve_result is not None diff --git a/python/tests/c_api/dlr_tests.py b/python/tests/c_api/dlr_tests.py index b29b2171..a1d4ec9c 100644 --- a/python/tests/c_api/dlr_tests.py +++ b/python/tests/c_api/dlr_tests.py @@ -14,7 +14,7 @@ _lib, logistic_kernel_new, reg_bose_kernel_new, sve_result_new, basis_new, - COMPUTATION_SUCCESS + COMPUTATION_SUCCESS, c_double_complex ) from pylibsparseir.ctypes_wrapper import * from pylibsparseir.constants import SPIR_STATISTICS_FERMIONIC, SPIR_STATISTICS_BOSONIC, SPIR_ORDER_ROW_MAJOR @@ -348,8 +348,8 @@ def test_dlr_to_ir_conversion_complex(self, statistics): ndim, dims.ctypes.data_as(POINTER(c_int)), target_dim, - dlr_coeffs_complex.ctypes.data_as(POINTER(c_double)), - ir_coeffs_complex.ctypes.data_as(POINTER(c_double)) + dlr_coeffs_complex.ctypes.data_as(POINTER(c_double_complex)), + ir_coeffs_complex.ctypes.data_as(POINTER(c_double_complex)) ) assert convert_status == COMPUTATION_SUCCESS diff --git a/python/tests/c_api/integration_tests.py b/python/tests/c_api/integration_tests.py index 4f6375b8..5536dad6 100644 --- a/python/tests/c_api/integration_tests.py +++ b/python/tests/c_api/integration_tests.py @@ -16,7 +16,7 @@ logistic_kernel_new, reg_bose_kernel_new, sve_result_new, basis_new, tau_sampling_new, matsubara_sampling_new, - COMPUTATION_SUCCESS + COMPUTATION_SUCCESS, c_double_complex ) from pylibsparseir.ctypes_wrapper import * from pylibsparseir.constants import * @@ -107,19 +107,19 @@ def test_complete_ir_dlr_workflow(self, statistics, positive_only): # Get default Matsubara points n_matsu_points = c_int() - status = _lib.spir_basis_get_n_default_matsus(ir_basis, c_bool(positive_only), byref(n_matsu_points)) + status = _lib.spir_basis_get_n_default_matsus(ir_basis, c_int(1 if positive_only else 0), byref(n_matsu_points)) assert status == COMPUTATION_SUCCESS assert n_matsu_points.value > 0 matsu_points = np.zeros(n_matsu_points.value, dtype=np.int64) - status = _lib.spir_basis_get_default_matsus(ir_basis, c_bool(positive_only), + status = _lib.spir_basis_get_default_matsus(ir_basis, c_int(1 if positive_only else 0), matsu_points.ctypes.data_as(POINTER(c_int64))) assert status == COMPUTATION_SUCCESS # Create Matsubara sampling for IR matsu_sampling_status = c_int() ir_matsu_sampling = _lib.spir_matsu_sampling_new( - ir_basis, c_bool(positive_only), n_matsu_points.value, + ir_basis, c_int(1 if positive_only else 0), n_matsu_points.value, matsu_points.ctypes.data_as(POINTER(c_int64)), byref(matsu_sampling_status) ) @@ -160,7 +160,7 @@ def test_complete_ir_dlr_workflow(self, statistics, positive_only): dlr_matsu_sampling_status = c_int() dlr_matsu_sampling = _lib.spir_matsu_sampling_new( - dlr, c_bool(positive_only), n_matsu_points.value, + dlr, c_int(1 if positive_only else 0), n_matsu_points.value, matsu_points.ctypes.data_as(POINTER(c_int64)), byref(dlr_matsu_sampling_status) ) @@ -473,7 +473,7 @@ def _evaluate_matsubara_basis_functions(self, uhat, matsubara_indices): status = _lib.spir_funcs_batch_eval_matsu( uhat, SPIR_ORDER_ROW_MAJOR, len(matsubara_indices), freq_indices.ctypes.data_as(POINTER(c_int64)), - uhat_eval_mat.ctypes.data_as(POINTER(c_double)) + uhat_eval_mat.ctypes.data_as(POINTER(c_double_complex)) ) assert status == COMPUTATION_SUCCESS @@ -571,18 +571,18 @@ def test_complete_dlr_sampling_workflow(self, statistics, positive_only): # Get default Matsubara points n_matsu_points = c_int() - status = _lib.spir_basis_get_n_default_matsus(ir_basis, c_bool(positive_only), byref(n_matsu_points)) + status = _lib.spir_basis_get_n_default_matsus(ir_basis, c_int(1 if positive_only else 0), byref(n_matsu_points)) assert status == COMPUTATION_SUCCESS matsu_points = np.zeros(n_matsu_points.value, dtype=np.int64) - status = _lib.spir_basis_get_default_matsus(ir_basis, c_bool(positive_only), + status = _lib.spir_basis_get_default_matsus(ir_basis, c_int(1 if positive_only else 0), matsu_points.ctypes.data_as(POINTER(c_int64))) assert status == COMPUTATION_SUCCESS # Create DLR Matsubara sampling dlr_matsu_sampling_status = c_int() dlr_matsu_sampling = _lib.spir_matsu_sampling_new( - dlr, c_bool(positive_only), n_matsu_points.value, + dlr, c_int(1 if positive_only else 0), n_matsu_points.value, matsu_points.ctypes.data_as(POINTER(c_int64)), byref(dlr_matsu_sampling_status) ) diff --git a/python/tests/c_api/sampling_tests.py b/python/tests/c_api/sampling_tests.py index af2182f3..98075aa7 100644 --- a/python/tests/c_api/sampling_tests.py +++ b/python/tests/c_api/sampling_tests.py @@ -115,19 +115,19 @@ def test_matsubara_sampling_creation(self, statistics, positive_only): # Get default Matsubara points n_matsu_points = c_int() - status = _lib.spir_basis_get_n_default_matsus(basis, c_bool(positive_only), byref(n_matsu_points)) + status = _lib.spir_basis_get_n_default_matsus(basis, c_int(1 if positive_only else 0), byref(n_matsu_points)) assert status == COMPUTATION_SUCCESS assert n_matsu_points.value > 0 matsu_points = np.zeros(n_matsu_points.value, dtype=np.int64) - status = _lib.spir_basis_get_default_matsus(basis, c_bool(positive_only), + status = _lib.spir_basis_get_default_matsus(basis, c_int(1 if positive_only else 0), matsu_points.ctypes.data_as(POINTER(c_int64))) assert status == COMPUTATION_SUCCESS # Create Matsubara sampling sampling_status = c_int() sampling = _lib.spir_matsu_sampling_new( - basis, c_bool(positive_only), n_matsu_points.value, + basis, c_int(1 if positive_only else 0), n_matsu_points.value, matsu_points.ctypes.data_as(POINTER(c_int64)), byref(sampling_status) ) @@ -350,17 +350,17 @@ def test_matsubara_sampling_evaluation_complex(self, statistics): # Create Matsubara sampling n_matsu_points = c_int() - status = _lib.spir_basis_get_n_default_matsus(basis, c_bool(positive_only), byref(n_matsu_points)) + status = _lib.spir_basis_get_n_default_matsus(basis, c_int(1 if positive_only else 0), byref(n_matsu_points)) assert status == COMPUTATION_SUCCESS matsu_points = np.zeros(n_matsu_points.value, dtype=np.int64) - status = _lib.spir_basis_get_default_matsus(basis, c_bool(positive_only), + status = _lib.spir_basis_get_default_matsus(basis, c_int(1 if positive_only else 0), matsu_points.ctypes.data_as(POINTER(c_int64))) assert status == COMPUTATION_SUCCESS sampling_status = c_int() sampling = _lib.spir_matsu_sampling_new( - basis, c_bool(positive_only), n_matsu_points.value, + basis, c_int(1 if positive_only else 0), n_matsu_points.value, matsu_points.ctypes.data_as(POINTER(c_int64)), byref(sampling_status) ) diff --git a/python/tools/gen_ctypes.py b/python/tools/gen_ctypes.py new file mode 100755 index 00000000..b5cc0a1e --- /dev/null +++ b/python/tools/gen_ctypes.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +""" +Generate Python ctypes bindings from C-API header using libclang. + +This script parses sparseir.h and generates ctypes_autogen.py with function +prototypes that can be automatically applied to the loaded library. +""" + +import sys +from pathlib import Path + +try: + from clang import cindex + from clang.cindex import Index, CursorKind +except ImportError: + print("ERROR: clang (libclang) is required. Install with: pip install clang") + sys.exit(1) + +# Map C types to ctypes +# Note: On macOS, int64_t is typically long, not long long +CTYPE_MAP = { + 'int': 'c_int', + 'double': 'c_double', + 'bool': 'c_bool', + 'int64_t': 'c_int64', + 'long long': 'c_int64', # int64_t is often typedef'd to long long on Linux + 'long long int': 'c_int64', + 'long': 'c_int64', # On macOS, int64_t is typically long + 'size_t': 'c_size_t', + 'void': 'None', + 'c_complex': 'c_double_complex', +} + +# Map opaque types +OPAQUE_TYPES = { + 'spir_kernel': 'spir_kernel', + 'spir_funcs': 'spir_funcs', + 'spir_basis': 'spir_basis', + 'spir_sampling': 'spir_sampling', + 'spir_sve_result': 'spir_sve_result', +} + + +def get_cursor_type_str(cursor_type): + """Convert clang Type to Python ctypes string.""" + spelling = cursor_type.spelling + + # Handle pointers + if cursor_type.kind == cindex.TypeKind.POINTER: + pointee = cursor_type.get_pointee() + pointee_spelling = pointee.spelling + pointee_canonical = pointee.get_canonical() + pointee_canonical_spelling = pointee_canonical.spelling if pointee_canonical else pointee_spelling + + # Special handling for int64_t: check if the original spelling or canonical contains int64_t + # even if canonical resolves to long/long long + if 'int64_t' in pointee_spelling or 'int64_t' in pointee_canonical_spelling: + pointee_str = 'c_int64' + # Special handling for c_complex: check if it's c_complex type + elif 'c_complex' in pointee_spelling or 'c_complex' in pointee_canonical_spelling: + pointee_str = 'c_double_complex' + # Check canonical type first for other typedefs + elif pointee_canonical_spelling in CTYPE_MAP: + pointee_str = CTYPE_MAP[pointee_canonical_spelling] + elif pointee_spelling in CTYPE_MAP: + pointee_str = CTYPE_MAP[pointee_spelling] + else: + pointee_str = get_cursor_type_str(pointee) + + # Check if it's an opaque type + if pointee_str in OPAQUE_TYPES: + return OPAQUE_TYPES[pointee_str] + + # Handle char* -> c_char_p + if pointee_str == 'char': + return 'c_char_p' + + # Generic pointer -> POINTER(type) + return f'POINTER({pointee_str})' + + # Handle const + if 'const' in spelling: + spelling = spelling.replace('const', '').strip() + + # Handle typedefs - check canonical type for int64_t, size_t, c_complex, etc. + # On some platforms, int64_t might be typedef'd to long long + canonical = cursor_type.get_canonical() + canonical_spelling = canonical.spelling if canonical else spelling + + # Special handling for c_complex + if 'c_complex' in spelling or 'c_complex' in canonical_spelling: + return 'c_double_complex' + + # Check canonical type first (handles typedefs) + if canonical_spelling in CTYPE_MAP: + return CTYPE_MAP[canonical_spelling] + + # Also check if canonical spelling contains known types + for ctype, pytype in CTYPE_MAP.items(): + if canonical_spelling.endswith(ctype) or canonical_spelling == ctype: + return pytype + + # Map basic types (exact match on original spelling) + if spelling in CTYPE_MAP: + return CTYPE_MAP[spelling] + + # Handle opaque types directly + if spelling in OPAQUE_TYPES: + return OPAQUE_TYPES[spelling] + + # Default: return as-is (will need manual mapping) + return spelling + + +def parse_function(cursor): + """Parse a function cursor and return (name, restype, argtypes).""" + if cursor.kind != CursorKind.FUNCTION_DECL: + return None + + name = cursor.spelling + + # Only process spir_* functions + if not name.startswith('spir_'): + return None + + # Get return type + result_type = cursor.result_type + restype = get_cursor_type_str(result_type) + + # Get arguments + argtypes = [] + for arg in cursor.get_arguments(): + arg_type = get_cursor_type_str(arg.type) + # Debug output for c_complex parameters + if 'complex' in arg.type.spelling.lower() or 'complex' in str(arg.type.get_canonical().spelling if arg.type.get_canonical() else '').lower(): + pointee = arg.type.get_pointee() if arg.type.kind == cindex.TypeKind.POINTER else None + if pointee: + print(f"DEBUG {name} arg '{arg.spelling}': spelling='{pointee.spelling}', canonical='{pointee.get_canonical().spelling if pointee.get_canonical() else 'N/A'}' -> {arg_type}") + argtypes.append(arg_type) + + return (name, restype, argtypes) + + +def parse_header(header_path, include_dirs): + """Parse the C header and extract function signatures.""" + index = Index.create() + + # Build include arguments + # Include standard headers for int64_t, size_t, etc. + # Use C99 standard to ensure int64_t is properly defined + args = ['-x', 'c-header', '-std=c99', '-D__STDC_LIMIT_MACROS', '-D__STDC_CONSTANT_MACROS'] + for inc_dir in include_dirs: + args.extend(['-I', inc_dir]) + + # Add system include paths for stdint.h + import platform + if platform.system() == 'Darwin': + # macOS: try common paths + import subprocess + try: + result = subprocess.run(['xcrun', '--show-sdk-path'], capture_output=True, text=True) + if result.returncode == 0: + sdk_path = result.stdout.strip() + args.extend(['-isysroot', sdk_path]) + except: + pass + + # Parse the translation unit + try: + tu = index.parse(header_path, args=args) + except Exception as e: + print(f"ERROR: Failed to parse {header_path}: {e}") + sys.exit(1) + + functions = {} + + def visit_cursor(cursor): + """Recursively visit cursors to find function declarations.""" + if cursor.location.file and cursor.location.file.name == header_path: + func_info = parse_function(cursor) + if func_info: + name, restype, argtypes = func_info + functions[name] = (restype, argtypes) + + # Recurse into children + for child in cursor.get_children(): + visit_cursor(child) + + visit_cursor(tu.cursor) + + return functions + + +def generate_python_file(functions, output_path): + """Generate the Python file with function prototypes.""" + lines = [ + '"""', + 'Auto-generated ctypes bindings from C-API header.', + 'DO NOT EDIT THIS FILE MANUALLY.', + 'Generated by tools/gen_ctypes.py', + '"""', + '', + 'import ctypes', + 'from ctypes import (', + ' c_int, c_double, c_int64, c_size_t, c_bool,', + ' POINTER, c_char_p, Structure,', + ')', + '', + 'from .ctypes_wrapper import (', + ' spir_kernel, spir_funcs, spir_basis,', + ' spir_sampling, spir_sve_result,', + ')', + '', + '# Custom complex type (matches core.py definition)', + 'class c_double_complex(Structure):', + ' """Complex number as ctypes Structure."""', + ' _fields_ = [("real", ctypes.c_double), ("imag", ctypes.c_double)]', + '', + ' @property', + ' def value(self):', + ' return self.real + 1j * self.imag', + '', + '', + '# Function prototypes: {name: (restype, [argtypes])}', + 'FUNCTIONS = {', + ] + + # Sort functions by name for consistent output + for name in sorted(functions.keys()): + restype, argtypes = functions[name] + # Format argtypes as a list of strings (will be evaluated in core.py) + argtypes_list = ', '.join([f"'{at}'" for at in argtypes]) + lines.append(f" '{name}': ('{restype}', [{argtypes_list}]),") + + lines.extend([ + '}', + '', + ]) + + # Write to file + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w') as f: + f.write('\n'.join(lines)) + + print(f"Generated {output_path} with {len(functions)} functions") + + +def main(): + """Main entry point.""" + # Determine paths + script_dir = Path(__file__).parent + python_dir = script_dir.parent + project_root = python_dir.parent + + # Try to find header in copied location first (after setup_build.py) + # Then fall back to original location + header_candidates = [ + python_dir / 'include' / 'sparseir' / 'sparseir.h', # Copied location + project_root / 'backend' / 'cxx' / 'include' / 'sparseir' / 'sparseir.h', # Original + ] + + header_path = None + for candidate in header_candidates: + if candidate.exists(): + header_path = candidate + break + + if not header_path: + print(f"ERROR: Header not found. Tried:") + for candidate in header_candidates: + print(f" {candidate}") + sys.exit(1) + + output_path = python_dir / 'pylibsparseir' / 'ctypes_autogen.py' + + # Include directories + include_dirs = [ + str(python_dir / 'include'), # Copied location + str(python_dir / 'include' / 'sparseir'), + str(project_root / 'backend' / 'cxx' / 'include'), # Original + str(project_root / 'backend' / 'cxx' / 'include' / 'sparseir'), + ] + + if not header_path.exists(): + print(f"ERROR: Header not found: {header_path}") + sys.exit(1) + + print(f"Parsing {header_path}...") + functions = parse_header(str(header_path), include_dirs) + + if not functions: + print("WARNING: No functions found!") + sys.exit(1) + + print(f"Found {len(functions)} functions") + generate_python_file(functions, output_path) + + +if __name__ == '__main__': + main() +