diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md index 6757466..e78f244 100644 --- a/MIGRATION_PLAN.md +++ b/MIGRATION_PLAN.md @@ -326,9 +326,10 @@ cpu_features_t cfd_get_cpu_features(void); - Automatic CUDA library detection - [x] **2.5.3 Update CI test infrastructure** - - Install CUDA runtime (12.0.0) for CUDA wheel tests + - Install CUDA runtime (12.4.0) for CUDA wheel tests - Use standard `pip` instead of `uv` for stable ABI wheel installation - Test matrix: Python 3.9 and 3.13 on all platforms + - Use apt-based CUDA installation on Linux (more reliable than runfile) - [x] **2.5.4 Ensure PEP 427 compliance** - Standard wheel filenames (no variant suffixes) @@ -383,30 +384,33 @@ cpu_features_t cfd_get_cpu_features(void); **Estimated effort:** 1 day -### Phase 5: Add Backend Availability API (v0.1.6 Feature) +### Phase 5: Add Backend Availability API (v0.1.6 Feature) ✅ COMPLETED **Priority:** P1 - Important for v0.1.6 compatibility +**Status:** Completed on 2025-12-31 + **Tasks:** -- [ ] **5.1 Expose backend enum** - - `BACKEND_SCALAR`, `BACKEND_SIMD`, `BACKEND_OMP`, `BACKEND_CUDA` constants - - Map to `ns_solver_backend_t` enum +- [x] **5.1 Expose backend enum** + - Added `BACKEND_SCALAR`, `BACKEND_SIMD`, `BACKEND_OMP`, `BACKEND_CUDA` constants + - Map to `ns_solver_backend_t` enum (values 0-3) -- [ ] **5.2 Implement backend availability functions** +- [x] **5.2 Implement backend availability functions** - `backend_is_available(backend)` → bool - `backend_get_name(backend)` → string - `list_solvers_by_backend(backend)` → list of solver names -- [ ] **5.3 Add solver creation with validation** - - `create_solver_checked(name)` → raises exception if backend unavailable - - Better error messages for unsupported backends - -- [ ] **5.4 Add backend query helpers** +- [x] **5.3 Add backend query helpers** - `get_available_backends()` → list of available backend names - - `get_solver_backend(solver_name)` → backend enum -**Estimated effort:** 1 day +- [x] **5.4 Add tests** + - Created `tests/test_backend_availability.py` with comprehensive tests + - Tests for constants, availability checking, name queries, solver listing + +**Note:** `create_solver_checked()` and `get_solver_backend()` deferred to Phase 4 (error handling integration). + +**Actual effort:** 0.5 days ### Phase 6: Add CPU Features & Misc (Enhancement) @@ -539,11 +543,11 @@ find_library(CFD_LIBRARY cfd_library) # Unified library name | Phase 2.5: CI/Build System (v0.1.6) | ✅ 1 day | 3 days | | Phase 3: Derived Fields | 1-2 days | 4-5 days | | Phase 4: Error Handling | 1 day | 5-6 days | -| Phase 5: Backend Availability (v0.1.6) | 1 day | 6-7 days | -| Phase 6: CPU Features | 1 day | 7-8 days | -| Phase 7: Docs & Tests | 2 days | 9-10 days | +| Phase 5: Backend Availability (v0.1.6) | ✅ 0.5 days | 3.5 days | +| Phase 6: CPU Features | 1 day | 4.5 days | +| Phase 7: Docs & Tests | 2 days | 6.5 days | -**Total estimated effort:** 9-10 days (3 days completed) +**Total estimated effort:** ~~9-10 days~~ 6.5 days (3.5 days completed) --- diff --git a/cfd_python/__init__.py b/cfd_python/__init__.py index b719c98..e863e39 100644 --- a/cfd_python/__init__.py +++ b/cfd_python/__init__.py @@ -1,4 +1,4 @@ -"""CFD Python - Python bindings for CFD simulation library v0.1.5+. +"""CFD Python - Python bindings for CFD simulation library v0.1.6+. This package provides Python bindings for the C-based CFD simulation library, enabling high-performance computational fluid dynamics simulations from Python. @@ -49,6 +49,10 @@ - BC_BACKEND_CUDA: GPU acceleration Functions: + - bc_get_backend(): Get current BC backend + - bc_get_backend_name(): Get current BC backend name + - bc_set_backend(backend): Set BC backend + - bc_backend_available(backend): Check if BC backend is available - bc_apply_scalar(field, nx, ny, bc_type): Apply BC to scalar field - bc_apply_velocity(u, v, nx, ny, bc_type): Apply BC to velocity - bc_apply_dirichlet(field, nx, ny, left, right, bottom, top): Fixed values @@ -57,6 +61,19 @@ - bc_apply_inlet_parabolic(u, v, nx, ny, max_velocity, edge): Parabolic inlet - bc_apply_outlet_scalar(field, nx, ny, edge): Zero-gradient outlet - bc_apply_outlet_velocity(u, v, nx, ny, edge): Zero-gradient outlet + +Solver backend availability (v0.1.6): + Backends: + - BACKEND_SCALAR: Basic scalar CPU implementation + - BACKEND_SIMD: SIMD-optimized (AVX2/SSE) + - BACKEND_OMP: OpenMP parallelized + - BACKEND_CUDA: CUDA GPU acceleration + + Functions: + - backend_is_available(backend): Check if backend is available + - backend_get_name(backend): Get backend name string + - list_solvers_by_backend(backend): Get solvers for a backend + - get_available_backends(): Get list of all available backends """ from ._version import get_version @@ -130,6 +147,16 @@ "bc_apply_inlet_parabolic", "bc_apply_outlet_scalar", "bc_apply_outlet_velocity", + # Solver backend constants (v0.1.6) + "BACKEND_SCALAR", + "BACKEND_SIMD", + "BACKEND_OMP", + "BACKEND_CUDA", + # Solver backend availability functions (v0.1.6) + "backend_is_available", + "backend_get_name", + "list_solvers_by_backend", + "get_available_backends", ] # Load C extension and populate module namespace diff --git a/cfd_python/_loader.py b/cfd_python/_loader.py index 8d800da..078d045 100644 --- a/cfd_python/_loader.py +++ b/cfd_python/_loader.py @@ -33,6 +33,11 @@ def load_extension(): try: from . import cfd_python as _cfd_module from .cfd_python import ( + BACKEND_CUDA, + BACKEND_OMP, + # Solver backend constants (v0.1.6) + BACKEND_SCALAR, + BACKEND_SIMD, # Boundary condition backends BC_BACKEND_AUTO, BC_BACKEND_CUDA, @@ -66,6 +71,9 @@ def load_extension(): OUTPUT_FULL_FIELD, OUTPUT_VELOCITY, OUTPUT_VELOCITY_MAGNITUDE, + backend_get_name, + # Solver backend availability functions (v0.1.6) + backend_is_available, bc_apply_dirichlet, bc_apply_inlet_parabolic, bc_apply_inlet_uniform, @@ -82,6 +90,7 @@ def load_extension(): clear_error, # Core functions create_grid, + get_available_backends, get_default_solver_params, get_error_string, get_last_error, @@ -89,6 +98,7 @@ def load_extension(): get_solver_info, has_solver, list_solvers, + list_solvers_by_backend, run_simulation, run_simulation_with_params, set_output_dir, @@ -164,6 +174,16 @@ def load_extension(): "bc_apply_inlet_parabolic": bc_apply_inlet_parabolic, "bc_apply_outlet_scalar": bc_apply_outlet_scalar, "bc_apply_outlet_velocity": bc_apply_outlet_velocity, + # Solver backend constants (v0.1.6) + "BACKEND_SCALAR": BACKEND_SCALAR, + "BACKEND_SIMD": BACKEND_SIMD, + "BACKEND_OMP": BACKEND_OMP, + "BACKEND_CUDA": BACKEND_CUDA, + # Solver backend availability functions (v0.1.6) + "backend_is_available": backend_is_available, + "backend_get_name": backend_get_name, + "list_solvers_by_backend": list_solvers_by_backend, + "get_available_backends": get_available_backends, } # Collect dynamic SOLVER_* constants diff --git a/src/cfd_python.c b/src/cfd_python.c index 6c229fd..b27ca8f 100644 --- a/src/cfd_python.c +++ b/src/cfd_python.c @@ -874,6 +874,141 @@ static PyObject* bc_backend_available_py(PyObject* self, PyObject* args) { return PyBool_FromLong(available); } +/* + * ======================================== + * Solver Backend Availability API (v0.1.6) + * ======================================== + */ + +/* + * Check if a solver backend is available at runtime + */ +static PyObject* backend_is_available_py(PyObject* self, PyObject* args) { + (void)self; + int backend; + + if (!PyArg_ParseTuple(args, "i", &backend)) { + return NULL; + } + + int available = cfd_backend_is_available((ns_solver_backend_t)backend); + return PyBool_FromLong(available); +} + +/* + * Get human-readable name for a solver backend + */ +static PyObject* backend_get_name_py(PyObject* self, PyObject* args) { + (void)self; + int backend; + + if (!PyArg_ParseTuple(args, "i", &backend)) { + return NULL; + } + + const char* name = cfd_backend_get_name((ns_solver_backend_t)backend); + if (name == NULL) { + Py_RETURN_NONE; + } + return PyUnicode_FromString(name); +} + +/* + * Get list of solvers for a specific backend + */ +static PyObject* list_solvers_by_backend_py(PyObject* self, PyObject* args) { + (void)self; + int backend; + + if (!PyArg_ParseTuple(args, "i", &backend)) { + return NULL; + } + + if (g_registry == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Solver registry not initialized"); + return NULL; + } + + // First, get the count + int count = cfd_registry_list_by_backend(g_registry, (ns_solver_backend_t)backend, NULL, 0); + if (count <= 0) { + return PyList_New(0); // Return empty list + } + + // Allocate array for names + const char** names = (const char**)malloc(count * sizeof(const char*)); + if (names == NULL) { + PyErr_SetString(PyExc_MemoryError, "Failed to allocate names array"); + return NULL; + } + + // Get the actual names + int actual_count = cfd_registry_list_by_backend(g_registry, (ns_solver_backend_t)backend, names, count); + + // Build Python list + PyObject* result = PyList_New(actual_count); + if (result == NULL) { + free(names); + return NULL; + } + + for (int i = 0; i < actual_count; i++) { + PyObject* name = PyUnicode_FromString(names[i]); + if (name == NULL) { + Py_DECREF(result); + free(names); + return NULL; + } + PyList_SetItem(result, i, name); // Steals reference + } + + free(names); + return result; +} + +/* + * Get list of all available backends + */ +static PyObject* get_available_backends_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + PyObject* result = PyList_New(0); + if (result == NULL) { + return NULL; + } + + // Check each backend + ns_solver_backend_t backends[] = { + NS_SOLVER_BACKEND_SCALAR, + NS_SOLVER_BACKEND_SIMD, + NS_SOLVER_BACKEND_OMP, + NS_SOLVER_BACKEND_CUDA + }; + int num_backends = sizeof(backends) / sizeof(backends[0]); + + for (int i = 0; i < num_backends; i++) { + if (cfd_backend_is_available(backends[i])) { + const char* name = cfd_backend_get_name(backends[i]); + if (name != NULL) { + PyObject* name_obj = PyUnicode_FromString(name); + if (name_obj == NULL) { + Py_DECREF(result); + return NULL; + } + if (PyList_Append(result, name_obj) < 0) { + Py_DECREF(name_obj); + Py_DECREF(result); + return NULL; + } + Py_DECREF(name_obj); + } + } + } + + return result; +} + /* * Apply boundary conditions to scalar field */ @@ -1659,6 +1794,29 @@ static PyMethodDef cfd_python_methods[] = { " nx (int): Grid points in x direction\n" " ny (int): Grid points in y direction\n" " edge (int, optional): Boundary edge (default: BC_EDGE_RIGHT)"}, + // Solver Backend Availability API (v0.1.6) + {"backend_is_available", backend_is_available_py, METH_VARARGS, + "Check if a solver backend is available at runtime.\n\n" + "Args:\n" + " backend (int): Backend type (BACKEND_SCALAR, BACKEND_SIMD, etc.)\n\n" + "Returns:\n" + " bool: True if backend is available"}, + {"backend_get_name", backend_get_name_py, METH_VARARGS, + "Get human-readable name for a solver backend.\n\n" + "Args:\n" + " backend (int): Backend type constant\n\n" + "Returns:\n" + " str or None: Backend name (e.g., 'scalar', 'simd', 'omp', 'cuda')"}, + {"list_solvers_by_backend", list_solvers_by_backend_py, METH_VARARGS, + "Get list of solver types for a specific backend.\n\n" + "Args:\n" + " backend (int): Backend type constant\n\n" + "Returns:\n" + " list: Solver type names for the specified backend"}, + {"get_available_backends", get_available_backends_py, METH_NOARGS, + "Get list of all available backends.\n\n" + "Returns:\n" + " list: Names of available backends (e.g., ['scalar', 'simd', 'omp'])"}, {NULL, NULL, 0, NULL} }; @@ -1808,5 +1966,14 @@ PyMODINIT_FUNC PyInit_cfd_python(void) { return NULL; } + // Add solver backend constants (v0.1.6 API) + if (PyModule_AddIntConstant(m, "BACKEND_SCALAR", NS_SOLVER_BACKEND_SCALAR) < 0 || + PyModule_AddIntConstant(m, "BACKEND_SIMD", NS_SOLVER_BACKEND_SIMD) < 0 || + PyModule_AddIntConstant(m, "BACKEND_OMP", NS_SOLVER_BACKEND_OMP) < 0 || + PyModule_AddIntConstant(m, "BACKEND_CUDA", NS_SOLVER_BACKEND_CUDA) < 0) { + Py_DECREF(m); + return NULL; + } + return m; } diff --git a/tests/test_backend_availability.py b/tests/test_backend_availability.py new file mode 100644 index 0000000..070496a --- /dev/null +++ b/tests/test_backend_availability.py @@ -0,0 +1,197 @@ +""" +Tests for solver backend availability API in cfd_python (v0.1.6). +""" + +import cfd_python + + +class TestBackendConstants: + """Test BACKEND_* constants are defined and valid""" + + def test_backend_constants_exist(self): + """Test all BACKEND_* constants are defined""" + backends = [ + "BACKEND_SCALAR", + "BACKEND_SIMD", + "BACKEND_OMP", + "BACKEND_CUDA", + ] + for const_name in backends: + assert hasattr(cfd_python, const_name), f"Missing constant: {const_name}" + + def test_backend_constants_are_integers(self): + """Test BACKEND_* constants are integers""" + backends = [ + "BACKEND_SCALAR", + "BACKEND_SIMD", + "BACKEND_OMP", + "BACKEND_CUDA", + ] + for const_name in backends: + value = getattr(cfd_python, const_name) + assert isinstance(value, int), f"{const_name} should be an integer" + + def test_backend_constants_unique(self): + """Test BACKEND_* constants have unique values""" + backends = [ + "BACKEND_SCALAR", + "BACKEND_SIMD", + "BACKEND_OMP", + "BACKEND_CUDA", + ] + values = [getattr(cfd_python, name) for name in backends] + assert len(values) == len(set(values)), "BACKEND_* constants should have unique values" + + def test_backend_constants_values(self): + """Test BACKEND_* constants have expected values (matching C enum)""" + assert cfd_python.BACKEND_SCALAR == 0 + assert cfd_python.BACKEND_SIMD == 1 + assert cfd_python.BACKEND_OMP == 2 + assert cfd_python.BACKEND_CUDA == 3 + + +class TestBackendIsAvailable: + """Test backend_is_available function""" + + def test_backend_is_available_returns_bool(self): + """Test backend_is_available returns boolean for all backends""" + for backend in [ + cfd_python.BACKEND_SCALAR, + cfd_python.BACKEND_SIMD, + cfd_python.BACKEND_OMP, + cfd_python.BACKEND_CUDA, + ]: + result = cfd_python.backend_is_available(backend) + assert isinstance( + result, bool + ), f"backend_is_available should return bool for {backend}" + + def test_scalar_backend_always_available(self): + """Test SCALAR backend is always available""" + available = cfd_python.backend_is_available(cfd_python.BACKEND_SCALAR) + assert available is True, "SCALAR backend should always be available" + + def test_backend_is_available_invalid_backend(self): + """Test backend_is_available with invalid backend returns False""" + # Invalid backend ID should return False (not raise exception) + result = cfd_python.backend_is_available(999) + assert result is False + + +class TestBackendGetName: + """Test backend_get_name function""" + + def test_backend_get_name_returns_string(self): + """Test backend_get_name returns string for valid backends""" + for backend in [ + cfd_python.BACKEND_SCALAR, + cfd_python.BACKEND_SIMD, + cfd_python.BACKEND_OMP, + cfd_python.BACKEND_CUDA, + ]: + name = cfd_python.backend_get_name(backend) + assert isinstance(name, str), f"backend_get_name should return string for {backend}" + assert len(name) > 0, f"Backend name should not be empty for {backend}" + + def test_backend_get_name_expected_names(self): + """Test backend_get_name returns expected name strings""" + assert cfd_python.backend_get_name(cfd_python.BACKEND_SCALAR) == "scalar" + assert cfd_python.backend_get_name(cfd_python.BACKEND_SIMD) == "simd" + assert cfd_python.backend_get_name(cfd_python.BACKEND_OMP) == "openmp" + assert cfd_python.backend_get_name(cfd_python.BACKEND_CUDA) == "cuda" + + def test_backend_get_name_invalid_backend(self): + """Test backend_get_name with invalid backend returns 'unknown'""" + result = cfd_python.backend_get_name(999) + assert result == "unknown" + + +class TestListSolversByBackend: + """Test list_solvers_by_backend function""" + + def test_list_solvers_by_backend_returns_list(self): + """Test list_solvers_by_backend returns a list""" + result = cfd_python.list_solvers_by_backend(cfd_python.BACKEND_SCALAR) + assert isinstance(result, list), "list_solvers_by_backend should return a list" + + def test_list_solvers_by_backend_scalar_has_solvers(self): + """Test SCALAR backend has at least one solver registered""" + solvers = cfd_python.list_solvers_by_backend(cfd_python.BACKEND_SCALAR) + assert len(solvers) > 0, "SCALAR backend should have at least one solver" + + def test_list_solvers_by_backend_returns_strings(self): + """Test list_solvers_by_backend returns list of strings""" + solvers = cfd_python.list_solvers_by_backend(cfd_python.BACKEND_SCALAR) + for solver in solvers: + assert isinstance(solver, str), f"Solver name should be string: {solver}" + + def test_list_solvers_by_backend_invalid_backend(self): + """Test list_solvers_by_backend with invalid backend returns empty list""" + result = cfd_python.list_solvers_by_backend(999) + assert isinstance(result, list) + assert len(result) == 0 + + +class TestGetAvailableBackends: + """Test get_available_backends function""" + + def test_get_available_backends_returns_list(self): + """Test get_available_backends returns a list""" + result = cfd_python.get_available_backends() + assert isinstance(result, list), "get_available_backends should return a list" + + def test_get_available_backends_includes_scalar(self): + """Test get_available_backends includes scalar (always available)""" + backends = cfd_python.get_available_backends() + assert "scalar" in backends, "SCALAR backend should always be in available list" + + def test_get_available_backends_returns_strings(self): + """Test get_available_backends returns list of strings""" + backends = cfd_python.get_available_backends() + for backend in backends: + assert isinstance(backend, str), f"Backend name should be string: {backend}" + + def test_get_available_backends_consistency(self): + """Test get_available_backends is consistent with backend_is_available""" + available_backends = cfd_python.get_available_backends() + + # Check each backend constant + for backend_id, backend_name in [ + (cfd_python.BACKEND_SCALAR, "scalar"), + (cfd_python.BACKEND_SIMD, "simd"), + (cfd_python.BACKEND_OMP, "openmp"), + (cfd_python.BACKEND_CUDA, "cuda"), + ]: + is_available = cfd_python.backend_is_available(backend_id) + in_list = backend_name in available_backends + assert is_available == in_list, ( + f"Inconsistency for {backend_name}: " + f"backend_is_available={is_available}, in list={in_list}" + ) + + +class TestBackendFunctionsExported: + """Test that all backend functions are properly exported""" + + def test_backend_functions_in_all(self): + """Test all backend functions are in __all__""" + backend_functions = [ + "backend_is_available", + "backend_get_name", + "list_solvers_by_backend", + "get_available_backends", + ] + for func_name in backend_functions: + assert func_name in cfd_python.__all__, f"{func_name} should be in __all__" + assert callable(getattr(cfd_python, func_name)), f"{func_name} should be callable" + + def test_backend_constants_in_all(self): + """Test all BACKEND_* constants are in __all__""" + backend_constants = [ + "BACKEND_SCALAR", + "BACKEND_SIMD", + "BACKEND_OMP", + "BACKEND_CUDA", + ] + for const_name in backend_constants: + assert const_name in cfd_python.__all__, f"{const_name} should be in __all__"