diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index d60aaf7..814c6a3 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -19,8 +19,8 @@ on: env: # CFD C library version to build against - # v0.1.6 introduces modular backend libraries - CFD_VERSION: "v0.1.6" + # v0.2.0 adds 3D support, Poisson solver, GPU management, logging + CFD_VERSION: "v0.2.0" jobs: build_wheel: diff --git a/cfd_python/__init__.py b/cfd_python/__init__.py index 77e4a72..9d9ef8f 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.6+. +"""CFD Python - Python bindings for CFD simulation library v0.2.0. This package provides Python bindings for the C-based CFD simulation library, enabling high-performance computational fluid dynamics simulations from Python. @@ -24,6 +24,8 @@ - CFD_ERROR_UNSUPPORTED: Operation not supported (-5) - CFD_ERROR_DIVERGED: Solver diverged (-6) - CFD_ERROR_MAX_ITER: Max iterations reached (-7) + - CFD_ERROR_LIMIT_EXCEEDED: Resource limit exceeded (-8) + - CFD_ERROR_NOT_FOUND: Resource not found (-9) - get_last_error(): Get last error message - get_last_status(): Get last status code - get_error_string(code): Get error description @@ -37,9 +39,11 @@ - BC_TYPE_NOSLIP: No-slip wall (zero velocity) - BC_TYPE_INLET: Inlet velocity specification - BC_TYPE_OUTLET: Outlet conditions + - BC_TYPE_SYMMETRY: Symmetry boundary condition Edges: - BC_EDGE_LEFT, BC_EDGE_RIGHT, BC_EDGE_BOTTOM, BC_EDGE_TOP + - BC_EDGE_FRONT, BC_EDGE_BACK (3D boundaries) Backends: - BC_BACKEND_AUTO: Auto-select best available @@ -79,6 +83,29 @@ - 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 + +Library lifecycle (v0.2.0): + - init(): Initialize the CFD library + - finalize(): Clean up the CFD library + - is_initialized(): Check if library is initialized + - get_cfd_version(): Get C library version string + +Poisson solver (v0.2.0): + - get_default_poisson_params(): Default solver parameters + - poisson_get_backend(): Current backend + - poisson_get_backend_name(): Current backend name + - poisson_set_backend(backend): Set backend + - poisson_backend_available(backend): Check availability + - poisson_simd_available(): Check SIMD availability + +GPU device management (v0.2.0): + - gpu_is_available(): Check GPU availability + - gpu_get_device_info(): Get device information + - gpu_select_device(id): Select a device + - gpu_get_default_config(): Get default GPU config + +Logging (v0.2.0): + - set_log_callback(callable): Set log callback """ from ._exceptions import ( @@ -86,8 +113,10 @@ CFDError, CFDInvalidError, CFDIOError, + CFDLimitExceededError, CFDMaxIterError, CFDMemoryError, + CFDNotFoundError, CFDUnsupportedError, raise_for_status, ) @@ -196,6 +225,68 @@ "has_simd", # Grid initialization variants (Phase 6) "create_grid_stretched", + # Library lifecycle (v0.2.0) + "init", + "finalize", + "is_initialized", + "get_cfd_version", + # Version constants (v0.2.0) + "CFD_VERSION_MAJOR", + "CFD_VERSION_MINOR", + "CFD_VERSION_PATCH", + # New status codes (v0.2.0) + "CFD_ERROR_LIMIT_EXCEEDED", + "CFD_ERROR_NOT_FOUND", + # New exception classes (v0.2.0) + "CFDLimitExceededError", + "CFDNotFoundError", + # New BC constants (v0.2.0) + "BC_TYPE_SYMMETRY", + "BC_EDGE_FRONT", + "BC_EDGE_BACK", + # Poisson solver method constants (v0.2.0) + "POISSON_METHOD_JACOBI", + "POISSON_METHOD_GAUSS_SEIDEL", + "POISSON_METHOD_SOR", + "POISSON_METHOD_REDBLACK_SOR", + "POISSON_METHOD_CG", + "POISSON_METHOD_BICGSTAB", + "POISSON_METHOD_MULTIGRID", + # Poisson solver backend constants (v0.2.0) + "POISSON_BACKEND_AUTO", + "POISSON_BACKEND_SCALAR", + "POISSON_BACKEND_OMP", + "POISSON_BACKEND_SIMD", + "POISSON_BACKEND_GPU", + # Poisson solver type presets (v0.2.0) + "POISSON_SOLVER_SOR_SCALAR", + "POISSON_SOLVER_JACOBI_SIMD", + "POISSON_SOLVER_REDBLACK_SIMD", + "POISSON_SOLVER_REDBLACK_OMP", + "POISSON_SOLVER_REDBLACK_SCALAR", + "POISSON_SOLVER_CG_SCALAR", + "POISSON_SOLVER_CG_SIMD", + "POISSON_SOLVER_CG_OMP", + # Poisson preconditioner constants (v0.2.0) + "POISSON_PRECOND_NONE", + "POISSON_PRECOND_JACOBI", + # Poisson solver functions (v0.2.0) + "get_default_poisson_params", + "poisson_get_backend", + "poisson_get_backend_name", + "poisson_set_backend", + "poisson_backend_available", + "poisson_simd_available", + # GPU functions (v0.2.0) + "gpu_is_available", + "gpu_get_device_info", + "gpu_select_device", + "gpu_get_default_config", + # Logging (v0.2.0) + "set_log_callback", + "CFD_LOG_LEVEL_INFO", + "CFD_LOG_LEVEL_WARNING", + "CFD_LOG_LEVEL_ERROR", ] # Load C extension and populate module namespace diff --git a/cfd_python/__init__.pyi b/cfd_python/__init__.pyi index 0ce296d..172a6b4 100644 --- a/cfd_python/__init__.pyi +++ b/cfd_python/__init__.pyi @@ -1,13 +1,25 @@ """Type stubs for cfd_python C extension module.""" -from typing import Any +from typing import Any, Callable __version__: str __all__: list[str] +# Status code constants +CFD_SUCCESS: int +CFD_ERROR: int +CFD_ERROR_NOMEM: int +CFD_ERROR_INVALID: int +CFD_ERROR_IO: int +CFD_ERROR_UNSUPPORTED: int +CFD_ERROR_DIVERGED: int +CFD_ERROR_MAX_ITER: int +CFD_ERROR_LIMIT_EXCEEDED: int +CFD_ERROR_NOT_FOUND: int + # Output type constants -OUTPUT_PRESSURE: int OUTPUT_VELOCITY: int +OUTPUT_VELOCITY_MAGNITUDE: int OUTPUT_FULL_FIELD: int OUTPUT_CSV_TIMESERIES: int OUTPUT_CSV_CENTERLINE: int @@ -22,6 +34,81 @@ SOLVER_PROJECTION_OPTIMIZED: str SOLVER_EXPLICIT_EULER_GPU: str SOLVER_PROJECTION_JACOBI_GPU: str +# Version constants (v0.2.0) +CFD_VERSION_MAJOR: int +CFD_VERSION_MINOR: int +CFD_VERSION_PATCH: int + +# SIMD architecture constants +SIMD_NONE: int +SIMD_AVX2: int +SIMD_NEON: int + +# Solver backend constants +BACKEND_SCALAR: int +BACKEND_SIMD: int +BACKEND_OMP: int +BACKEND_CUDA: int + +# Boundary condition type constants +BC_TYPE_PERIODIC: int +BC_TYPE_NEUMANN: int +BC_TYPE_DIRICHLET: int +BC_TYPE_NOSLIP: int +BC_TYPE_INLET: int +BC_TYPE_OUTLET: int +BC_TYPE_SYMMETRY: int + +# Boundary condition edge constants +BC_EDGE_LEFT: int +BC_EDGE_RIGHT: int +BC_EDGE_BOTTOM: int +BC_EDGE_TOP: int +BC_EDGE_FRONT: int +BC_EDGE_BACK: int + +# Boundary condition backend constants +BC_BACKEND_AUTO: int +BC_BACKEND_SCALAR: int +BC_BACKEND_OMP: int +BC_BACKEND_SIMD: int +BC_BACKEND_CUDA: int + +# Poisson solver method constants (v0.2.0) +POISSON_METHOD_JACOBI: int +POISSON_METHOD_GAUSS_SEIDEL: int +POISSON_METHOD_SOR: int +POISSON_METHOD_REDBLACK_SOR: int +POISSON_METHOD_CG: int +POISSON_METHOD_BICGSTAB: int +POISSON_METHOD_MULTIGRID: int + +# Poisson solver backend constants (v0.2.0) +POISSON_BACKEND_AUTO: int +POISSON_BACKEND_SCALAR: int +POISSON_BACKEND_OMP: int +POISSON_BACKEND_SIMD: int +POISSON_BACKEND_GPU: int + +# Poisson solver type presets (v0.2.0) +POISSON_SOLVER_SOR_SCALAR: int +POISSON_SOLVER_JACOBI_SIMD: int +POISSON_SOLVER_REDBLACK_SIMD: int +POISSON_SOLVER_REDBLACK_OMP: int +POISSON_SOLVER_REDBLACK_SCALAR: int +POISSON_SOLVER_CG_SCALAR: int +POISSON_SOLVER_CG_SIMD: int +POISSON_SOLVER_CG_OMP: int + +# Poisson preconditioner constants (v0.2.0) +POISSON_PRECOND_NONE: int +POISSON_PRECOND_JACOBI: int + +# Logging constants (v0.2.0) +CFD_LOG_LEVEL_INFO: int +CFD_LOG_LEVEL_WARNING: int +CFD_LOG_LEVEL_ERROR: int + # Simulation functions def run_simulation( nx: int, @@ -99,6 +186,9 @@ def create_grid( xmax: float, ymin: float, ymax: float, + nz: int = 1, + zmin: float = 0.0, + zmax: float = 0.0, ) -> dict[str, Any]: """Create a computational grid. @@ -109,6 +199,46 @@ def create_grid( xmax: Maximum x coordinate ymin: Minimum y coordinate ymax: Maximum y coordinate + nz: Grid dimension in z direction (default: 1, 2D grid) + zmin: Minimum z coordinate (default: 0.0) + zmax: Maximum z coordinate (default: 0.0) + + Returns: + Dictionary with keys: + - nx: int + - ny: int + - xmin: float + - xmax: float + - ymin: float + - ymax: float + - x_coords: list[float] + - y_coords: list[float] + - nz: int + - zmin: float + - zmax: float + - z_coords: list[float] (present when nz > 1) + """ + ... + +def create_grid_stretched( + nx: int, + ny: int, + xmin: float, + xmax: float, + ymin: float, + ymax: float, + beta: float, +) -> dict[str, Any]: + """Create a computational grid with stretched (non-uniform) spacing. + + Args: + nx: Grid dimension in x direction + ny: Grid dimension in y direction + xmin: Minimum x coordinate + xmax: Maximum x coordinate + ymin: Minimum y coordinate + ymax: Maximum y coordinate + beta: Stretching parameter (higher = more clustering near boundaries) Returns: Dictionary with keys: @@ -118,6 +248,7 @@ def create_grid( - xmax: float - ymin: float - ymax: float + - beta: float - x_coords: list[float] - y_coords: list[float] """ @@ -133,49 +264,24 @@ def get_default_solver_params() -> dict[str, float]: # Solver discovery functions def list_solvers() -> list[str]: - """Get list of all available solver names. - - Returns: - List of solver name strings - """ + """Get list of all available solver names.""" ... def has_solver(name: str) -> bool: - """Check if a solver is available. - - Args: - name: Solver name to check - - Returns: - True if solver exists, False otherwise - """ + """Check if a solver is available.""" ... def get_solver_info(name: str) -> dict[str, Any]: """Get information about a solver. - Args: - name: Solver name - Returns: - Dictionary with keys: - - name: str - - description: str - - version: str - - capabilities: list[str] - - Raises: - ValueError: If solver name is not found + Dictionary with keys: name, description, version, capabilities """ ... # Output functions def set_output_dir(directory: str) -> None: - """Set the output directory for VTK/CSV files. - - Args: - directory: Path to output directory - """ + """Set the output directory for VTK/CSV files (deprecated).""" ... def write_vtk_scalar( @@ -188,23 +294,11 @@ def write_vtk_scalar( xmax: float, ymin: float, ymax: float, + nz: int = 1, + zmin: float = 0.0, + zmax: float = 0.0, ) -> None: - """Write scalar field to VTK file. - - Args: - filename: Output file path - field_name: Name of the scalar field - data: Scalar field data (size nx*ny) - nx: Grid dimension in x direction - ny: Grid dimension in y direction - xmin: Minimum x coordinate - xmax: Maximum x coordinate - ymin: Minimum y coordinate - ymax: Maximum y coordinate - - Raises: - ValueError: If data size doesn't match nx*ny - """ + """Write scalar field to VTK file.""" ... def write_vtk_vector( @@ -218,24 +312,12 @@ def write_vtk_vector( xmax: float, ymin: float, ymax: float, + w_data: list[float] | None = None, + nz: int = 1, + zmin: float = 0.0, + zmax: float = 0.0, ) -> None: - """Write vector field to VTK file. - - Args: - filename: Output file path - field_name: Name of the vector field - u_data: U component data (size nx*ny) - v_data: V component data (size nx*ny) - nx: Grid dimension in x direction - ny: Grid dimension in y direction - xmin: Minimum x coordinate - xmax: Maximum x coordinate - ymin: Minimum y coordinate - ymax: Maximum y coordinate - - Raises: - ValueError: If data sizes don't match nx*ny - """ + """Write vector field to VTK file.""" ... def write_csv_timeseries( @@ -251,22 +333,249 @@ def write_csv_timeseries( iterations: int, create_new: bool = False, ) -> None: - """Write simulation timeseries data to CSV file. + """Write simulation timeseries data to CSV file.""" + ... - Args: - filename: Output file path - step: Time step number - time: Simulation time - u_data: U velocity component (size nx*ny) - v_data: V velocity component (size nx*ny) - p_data: Pressure field (size nx*ny) - nx: Grid dimension in x direction - ny: Grid dimension in y direction - dt: Time step size - iterations: Number of solver iterations - create_new: If True, create new file; if False, append +# Error handling functions +def get_last_error() -> str | None: + """Get the last CFD error message, or None if no error.""" + ... + +def get_last_status() -> int: + """Get the last CFD status code.""" + ... + +def get_error_string(status_code: int) -> str: + """Get human-readable description for a status code.""" + ... + +def clear_error() -> None: + """Clear the CFD error state.""" + ... - Raises: - ValueError: If data sizes don't match nx*ny +# Boundary condition backend functions +def bc_get_backend() -> int: + """Get the current boundary condition backend.""" + ... + +def bc_get_backend_name() -> str: + """Get the name of the current boundary condition backend.""" + ... + +def bc_set_backend(backend: int) -> bool: + """Set the boundary condition backend.""" + ... + +def bc_backend_available(backend: int) -> bool: + """Check if a boundary condition backend is available.""" + ... + +# Boundary condition application functions +def bc_apply_scalar(field: list[float], nx: int, ny: int, bc_type: int) -> None: + """Apply boundary conditions to a scalar field (modifies in place).""" + ... + +def bc_apply_velocity(u: list[float], v: list[float], nx: int, ny: int, bc_type: int) -> None: + """Apply boundary conditions to velocity fields (modifies in place).""" + ... + +def bc_apply_dirichlet( + field: list[float], + nx: int, + ny: int, + left: float, + right: float, + bottom: float, + top: float, +) -> None: + """Apply Dirichlet boundary conditions with per-edge values (modifies in place).""" + ... + +def bc_apply_noslip(u: list[float], v: list[float], nx: int, ny: int) -> None: + """Apply no-slip wall boundary conditions (modifies in place).""" + ... + +def bc_apply_inlet_uniform( + u: list[float], + v: list[float], + nx: int, + ny: int, + u_inlet: float, + v_inlet: float, + edge: int = ..., +) -> None: + """Apply uniform inlet boundary conditions (modifies in place).""" + ... + +def bc_apply_inlet_parabolic( + u: list[float], + v: list[float], + nx: int, + ny: int, + max_velocity: float, + edge: int = ..., +) -> None: + """Apply parabolic inlet boundary conditions (modifies in place).""" + ... + +def bc_apply_outlet_scalar(field: list[float], nx: int, ny: int, edge: int = ...) -> None: + """Apply zero-gradient outlet BC to scalar field (modifies in place).""" + ... + +def bc_apply_outlet_velocity( + u: list[float], v: list[float], nx: int, ny: int, edge: int = ... +) -> None: + """Apply zero-gradient outlet BC to velocity fields (modifies in place).""" + ... + +# Derived fields and statistics +def calculate_field_stats(data: list[float]) -> dict[str, float]: + """Compute statistics for a field. + + Returns: + Dictionary with keys: min, max, avg, sum """ ... + +def compute_velocity_magnitude(u: list[float], v: list[float], nx: int, ny: int) -> list[float]: + """Compute velocity magnitude sqrt(u^2 + v^2).""" + ... + +def compute_flow_statistics( + u: list[float], v: list[float], p: list[float], nx: int, ny: int +) -> dict[str, dict[str, float]]: + """Compute statistics for all flow field components. + + Returns: + Dictionary with keys: u, v, p, velocity_magnitude + Each value is a dict with keys: min, max, avg, sum + """ + ... + +# Solver backend availability functions +def backend_is_available(backend: int) -> bool: + """Check if a solver backend is available at runtime.""" + ... + +def backend_get_name(backend: int) -> str | None: + """Get human-readable name for a solver backend.""" + ... + +def list_solvers_by_backend(backend: int) -> list[str]: + """Get list of solver names available for a specific backend.""" + ... + +def get_available_backends() -> list[str]: + """Get list of all available backend names.""" + ... + +# CPU feature detection functions +def get_simd_arch() -> int: + """Get the detected SIMD architecture constant.""" + ... + +def get_simd_name() -> str: + """Get the name of the detected SIMD architecture.""" + ... + +def has_avx2() -> bool: + """Check if AVX2 SIMD is available.""" + ... + +def has_neon() -> bool: + """Check if ARM NEON SIMD is available.""" + ... + +def has_simd() -> bool: + """Check if any SIMD (AVX2 or NEON) is available.""" + ... + +# Library lifecycle (v0.2.0) +def init() -> None: + """Initialize the CFD library.""" + ... + +def finalize() -> None: + """Finalize and clean up the CFD library.""" + ... + +def is_initialized() -> bool: + """Check if the CFD library is initialized.""" + ... + +def get_cfd_version() -> str: + """Get the CFD C library version string.""" + ... + +# Poisson solver functions (v0.2.0) +def get_default_poisson_params() -> dict[str, Any]: + """Get default Poisson solver parameters. + + Returns: + Dictionary with keys: tolerance, absolute_tolerance, max_iterations, + omega, check_interval, verbose, preconditioner + """ + ... + +def poisson_get_backend() -> int: + """Get the current Poisson solver backend.""" + ... + +def poisson_get_backend_name() -> str: + """Get the name of the current Poisson solver backend.""" + ... + +def poisson_set_backend(backend: int) -> bool: + """Set the Poisson solver backend.""" + ... + +def poisson_backend_available(backend: int) -> bool: + """Check if a Poisson solver backend is available.""" + ... + +def poisson_simd_available() -> bool: + """Check if SIMD-accelerated Poisson solver is available.""" + ... + +# GPU device functions (v0.2.0) +def gpu_is_available() -> bool: + """Check if GPU acceleration is available.""" + ... + +def gpu_get_device_info() -> list[dict[str, Any]]: + """Get information about available GPU devices.""" + ... + +def gpu_select_device(device_id: int) -> None: + """Select a GPU device by ID.""" + ... + +def gpu_get_default_config() -> dict[str, Any]: + """Get default GPU configuration.""" + ... + +# Logging (v0.2.0) +def set_log_callback(callback: Callable[[int, str], None] | None) -> None: + """Set a Python callback for CFD library log messages. + + Args: + callback: Function(level: int, message: str), or None to clear. + """ + ... + +# Exception classes +class CFDError(Exception): + status_code: int + message: str + def __init__(self, message: str, status_code: int = -1) -> None: ... + +class CFDMemoryError(CFDError, MemoryError): ... +class CFDInvalidError(CFDError, ValueError): ... +class CFDIOError(CFDError, IOError): ... +class CFDUnsupportedError(CFDError, NotImplementedError): ... +class CFDDivergedError(CFDError): ... +class CFDMaxIterError(CFDError): ... +class CFDLimitExceededError(CFDError, ResourceWarning): ... +class CFDNotFoundError(CFDError, LookupError): ... + +def raise_for_status(status_code: int, context: str = "") -> None: ... diff --git a/cfd_python/_exceptions.py b/cfd_python/_exceptions.py index 1d1b52e..063f4fd 100644 --- a/cfd_python/_exceptions.py +++ b/cfd_python/_exceptions.py @@ -8,6 +8,8 @@ "CFDUnsupportedError", "CFDDivergedError", "CFDMaxIterError", + "CFDLimitExceededError", + "CFDNotFoundError", "raise_for_status", ] @@ -83,6 +85,24 @@ class CFDMaxIterError(CFDError): pass +class CFDLimitExceededError(CFDError, ResourceWarning): + """Raised when a resource limit is exceeded. + + Corresponds to CFD_ERROR_LIMIT_EXCEEDED (-8). + """ + + pass + + +class CFDNotFoundError(CFDError, LookupError): + """Raised when a requested resource is not found. + + Corresponds to CFD_ERROR_NOT_FOUND (-9). + """ + + pass + + # Mapping from status codes to exception classes _STATUS_TO_EXCEPTION = { -1: CFDError, # CFD_ERROR (generic) @@ -92,6 +112,8 @@ class CFDMaxIterError(CFDError): -5: CFDUnsupportedError, # CFD_ERROR_UNSUPPORTED -6: CFDDivergedError, # CFD_ERROR_DIVERGED -7: CFDMaxIterError, # CFD_ERROR_MAX_ITER + -8: CFDLimitExceededError, # CFD_ERROR_LIMIT_EXCEEDED + -9: CFDNotFoundError, # CFD_ERROR_NOT_FOUND } diff --git a/cfd_python/_loader.py b/cfd_python/_loader.py index 0bc6231..255f8da 100644 --- a/cfd_python/_loader.py +++ b/cfd_python/_loader.py @@ -23,6 +23,10 @@ def _check_extension_exists() -> bool: def load_extension(): """Load the C extension module and return exports. + Auto-discovers all public symbols from the C extension via dir(). + SOLVER_* constants are returned separately since they are dynamically + generated from the solver registry at import time. + Returns: tuple: (exports_dict, solver_constants) @@ -32,184 +36,17 @@ def load_extension(): """ try: from . import cfd_python as _cfd_module - from .cfd_python import ( - BACKEND_CUDA, - BACKEND_OMP, - BACKEND_SCALAR, - BACKEND_SIMD, - BC_BACKEND_AUTO, - BC_BACKEND_CUDA, - BC_BACKEND_OMP, - BC_BACKEND_SCALAR, - BC_BACKEND_SIMD, - BC_EDGE_BOTTOM, - BC_EDGE_LEFT, - BC_EDGE_RIGHT, - BC_EDGE_TOP, - BC_TYPE_DIRICHLET, - BC_TYPE_INLET, - BC_TYPE_NEUMANN, - BC_TYPE_NOSLIP, - BC_TYPE_OUTLET, - BC_TYPE_PERIODIC, - CFD_ERROR, - CFD_ERROR_DIVERGED, - CFD_ERROR_INVALID, - CFD_ERROR_IO, - CFD_ERROR_MAX_ITER, - CFD_ERROR_NOMEM, - CFD_ERROR_UNSUPPORTED, - CFD_SUCCESS, - OUTPUT_CSV_CENTERLINE, - OUTPUT_CSV_STATISTICS, - OUTPUT_CSV_TIMESERIES, - OUTPUT_FULL_FIELD, - OUTPUT_VELOCITY, - OUTPUT_VELOCITY_MAGNITUDE, - SIMD_AVX2, - SIMD_NEON, - SIMD_NONE, - backend_get_name, - backend_is_available, - bc_apply_dirichlet, - bc_apply_inlet_parabolic, - bc_apply_inlet_uniform, - bc_apply_noslip, - bc_apply_outlet_scalar, - bc_apply_outlet_velocity, - bc_apply_scalar, - bc_apply_velocity, - bc_backend_available, - bc_get_backend, - bc_get_backend_name, - bc_set_backend, - calculate_field_stats, - clear_error, - compute_flow_statistics, - compute_velocity_magnitude, - create_grid, - create_grid_stretched, - get_available_backends, - get_default_solver_params, - get_error_string, - get_last_error, - get_last_status, - get_simd_arch, - get_simd_name, - get_solver_info, - has_avx2, - has_neon, - has_simd, - has_solver, - list_solvers, - list_solvers_by_backend, - run_simulation, - run_simulation_with_params, - set_output_dir, - write_csv_timeseries, - write_vtk_scalar, - write_vtk_vector, - ) - - # Collect all exports - exports = { - # Simulation functions - "run_simulation": run_simulation, - "run_simulation_with_params": run_simulation_with_params, - "create_grid": create_grid, - "get_default_solver_params": get_default_solver_params, - # Solver functions - "list_solvers": list_solvers, - "has_solver": has_solver, - "get_solver_info": get_solver_info, - # Output functions - "set_output_dir": set_output_dir, - "write_vtk_scalar": write_vtk_scalar, - "write_vtk_vector": write_vtk_vector, - "write_csv_timeseries": write_csv_timeseries, - # Output type constants - "OUTPUT_VELOCITY": OUTPUT_VELOCITY, - "OUTPUT_VELOCITY_MAGNITUDE": OUTPUT_VELOCITY_MAGNITUDE, - "OUTPUT_FULL_FIELD": OUTPUT_FULL_FIELD, - "OUTPUT_CSV_TIMESERIES": OUTPUT_CSV_TIMESERIES, - "OUTPUT_CSV_CENTERLINE": OUTPUT_CSV_CENTERLINE, - "OUTPUT_CSV_STATISTICS": OUTPUT_CSV_STATISTICS, - # Error handling API - "CFD_SUCCESS": CFD_SUCCESS, - "CFD_ERROR": CFD_ERROR, - "CFD_ERROR_NOMEM": CFD_ERROR_NOMEM, - "CFD_ERROR_INVALID": CFD_ERROR_INVALID, - "CFD_ERROR_IO": CFD_ERROR_IO, - "CFD_ERROR_UNSUPPORTED": CFD_ERROR_UNSUPPORTED, - "CFD_ERROR_DIVERGED": CFD_ERROR_DIVERGED, - "CFD_ERROR_MAX_ITER": CFD_ERROR_MAX_ITER, - "get_last_error": get_last_error, - "get_last_status": get_last_status, - "get_error_string": get_error_string, - "clear_error": clear_error, - # Boundary condition type constants - "BC_TYPE_PERIODIC": BC_TYPE_PERIODIC, - "BC_TYPE_NEUMANN": BC_TYPE_NEUMANN, - "BC_TYPE_DIRICHLET": BC_TYPE_DIRICHLET, - "BC_TYPE_NOSLIP": BC_TYPE_NOSLIP, - "BC_TYPE_INLET": BC_TYPE_INLET, - "BC_TYPE_OUTLET": BC_TYPE_OUTLET, - # Boundary edge constants - "BC_EDGE_LEFT": BC_EDGE_LEFT, - "BC_EDGE_RIGHT": BC_EDGE_RIGHT, - "BC_EDGE_BOTTOM": BC_EDGE_BOTTOM, - "BC_EDGE_TOP": BC_EDGE_TOP, - # Boundary condition backend constants - "BC_BACKEND_AUTO": BC_BACKEND_AUTO, - "BC_BACKEND_SCALAR": BC_BACKEND_SCALAR, - "BC_BACKEND_OMP": BC_BACKEND_OMP, - "BC_BACKEND_SIMD": BC_BACKEND_SIMD, - "BC_BACKEND_CUDA": BC_BACKEND_CUDA, - # Boundary condition functions - "bc_get_backend": bc_get_backend, - "bc_get_backend_name": bc_get_backend_name, - "bc_set_backend": bc_set_backend, - "bc_backend_available": bc_backend_available, - "bc_apply_scalar": bc_apply_scalar, - "bc_apply_velocity": bc_apply_velocity, - "bc_apply_dirichlet": bc_apply_dirichlet, - "bc_apply_noslip": bc_apply_noslip, - "bc_apply_inlet_uniform": bc_apply_inlet_uniform, - "bc_apply_inlet_parabolic": bc_apply_inlet_parabolic, - "bc_apply_outlet_scalar": bc_apply_outlet_scalar, - "bc_apply_outlet_velocity": bc_apply_outlet_velocity, - # Derived fields API (Phase 3) - "calculate_field_stats": calculate_field_stats, - "compute_velocity_magnitude": compute_velocity_magnitude, - "compute_flow_statistics": compute_flow_statistics, - # 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, - # CPU Features API (Phase 6) - "SIMD_NONE": SIMD_NONE, - "SIMD_AVX2": SIMD_AVX2, - "SIMD_NEON": SIMD_NEON, - "get_simd_arch": get_simd_arch, - "get_simd_name": get_simd_name, - "has_avx2": has_avx2, - "has_neon": has_neon, - "has_simd": has_simd, - # Grid initialization variants (Phase 6) - "create_grid_stretched": create_grid_stretched, - } - - # Collect dynamic SOLVER_* constants - solver_constants = {} + + # Auto-collect all public symbols from the C extension + exports = {} for name in dir(_cfd_module): - if name.startswith("SOLVER_"): - solver_constants[name] = getattr(_cfd_module, name) + if not name.startswith("_"): + exports[name] = getattr(_cfd_module, name) + + # Separate SOLVER_* constants (dynamically generated from registry) + solver_constants = {k: v for k, v in exports.items() if k.startswith("SOLVER_")} + for k in solver_constants: + del exports[k] return exports, solver_constants diff --git a/src/cfd_python.c b/src/cfd_python.c index 63ac571..11944e4 100644 --- a/src/cfd_python.c +++ b/src/cfd_python.c @@ -9,7 +9,7 @@ #include #include -// Include CFD library headers (v0.1.5+ API) +// Include CFD library headers (v0.2.0 API) #include "cfd/core/grid.h" #include "cfd/core/cfd_status.h" #include "cfd/core/derived_fields.h" @@ -19,6 +19,11 @@ #include "cfd/io/vtk_output.h" #include "cfd/io/csv_output.h" #include "cfd/boundary/boundary_conditions.h" +#include "cfd/core/cfd_init.h" +#include "cfd/core/cfd_version.h" +#include "cfd/solvers/poisson_solver.h" +#include "cfd/core/gpu_device.h" +#include "cfd/core/logging.h" // Module-level solver registry (context-bound) static ns_solver_registry_t* g_registry = NULL; @@ -192,9 +197,9 @@ static PyObject* run_simulation(PyObject* self, PyObject* args, PyObject* kwds) simulation_data* sim_data; if (solver_type) { - sim_data = init_simulation_with_solver(nx, ny, xmin, xmax, ymin, ymax, solver_type); + sim_data = init_simulation_with_solver(nx, ny, 1, xmin, xmax, ymin, ymax, 0.0, 0.0, solver_type); } else { - sim_data = init_simulation(nx, ny, xmin, xmax, ymin, ymax); + sim_data = init_simulation(nx, ny, 1, xmin, xmax, ymin, ymax, 0.0, 0.0); } if (sim_data == NULL) { @@ -214,14 +219,15 @@ static PyObject* run_simulation(PyObject* self, PyObject* args, PyObject* kwds) // Write output if requested if (output_file) { write_vtk_flow_field(output_file, sim_data->field, - sim_data->grid->nx, sim_data->grid->ny, + sim_data->grid->nx, sim_data->grid->ny, sim_data->grid->nz, sim_data->grid->xmin, sim_data->grid->xmax, - sim_data->grid->ymin, sim_data->grid->ymax); + sim_data->grid->ymin, sim_data->grid->ymax, + sim_data->grid->zmin, sim_data->grid->zmax); } // Compute velocity magnitude using derived_fields flow_field* field = sim_data->field; - derived_fields* derived = derived_fields_create(field->nx, field->ny); + derived_fields* derived = derived_fields_create(field->nx, field->ny, field->nz); if (derived == NULL) { free_simulation(sim_data); PyErr_SetString(PyExc_MemoryError, "Failed to create derived fields"); @@ -260,12 +266,18 @@ static PyObject* run_simulation(PyObject* self, PyObject* args, PyObject* kwds) /* * Create a simple grid function */ -static PyObject* create_grid(PyObject* self, PyObject* args) { +static PyObject* create_grid(PyObject* self, PyObject* args, PyObject* kwds) { (void)self; + static const char* const kwlist[] = {"nx", "ny", "xmin", "xmax", "ymin", "ymax", + "nz", "zmin", "zmax", NULL}; Py_ssize_t nx_signed, ny_signed; + Py_ssize_t nz_signed = 1; double xmin, xmax, ymin, ymax; + double zmin = 0.0, zmax = 0.0; - if (!PyArg_ParseTuple(args, "nndddd", &nx_signed, &ny_signed, &xmin, &xmax, &ymin, &ymax)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nndddd|ndd", (char**)kwlist, + &nx_signed, &ny_signed, &xmin, &xmax, &ymin, &ymax, + &nz_signed, &zmin, &zmax)) { return NULL; } @@ -278,6 +290,10 @@ static PyObject* create_grid(PyObject* self, PyObject* args) { PyErr_SetString(PyExc_ValueError, "ny must be at least 2"); return NULL; } + if (nz_signed < 1) { + PyErr_SetString(PyExc_ValueError, "nz must be at least 1"); + return NULL; + } if (xmax <= xmin) { PyErr_SetString(PyExc_ValueError, "xmax must be greater than xmin"); return NULL; @@ -286,11 +302,16 @@ static PyObject* create_grid(PyObject* self, PyObject* args) { PyErr_SetString(PyExc_ValueError, "ymax must be greater than ymin"); return NULL; } + if (nz_signed > 1 && zmax <= zmin) { + PyErr_SetString(PyExc_ValueError, "zmax must be greater than zmin when nz > 1"); + return NULL; + } size_t nx = (size_t)nx_signed; size_t ny = (size_t)ny_signed; + size_t nz = (size_t)nz_signed; - grid* g = grid_create(nx, ny, xmin, xmax, ymin, ymax); + grid* g = grid_create(nx, ny, nz, xmin, xmax, ymin, ymax, zmin, zmax); if (g == NULL) { PyErr_SetString(PyExc_RuntimeError, "Failed to create grid"); return NULL; @@ -315,10 +336,13 @@ static PyObject* create_grid(PyObject* self, PyObject* args) { ADD_TO_DICT(grid_dict, "nx", PyLong_FromSize_t(g->nx)); ADD_TO_DICT(grid_dict, "ny", PyLong_FromSize_t(g->ny)); + ADD_TO_DICT(grid_dict, "nz", PyLong_FromSize_t(g->nz)); ADD_TO_DICT(grid_dict, "xmin", PyFloat_FromDouble(g->xmin)); ADD_TO_DICT(grid_dict, "xmax", PyFloat_FromDouble(g->xmax)); ADD_TO_DICT(grid_dict, "ymin", PyFloat_FromDouble(g->ymin)); ADD_TO_DICT(grid_dict, "ymax", PyFloat_FromDouble(g->ymax)); + ADD_TO_DICT(grid_dict, "zmin", PyFloat_FromDouble(g->zmin)); + ADD_TO_DICT(grid_dict, "zmax", PyFloat_FromDouble(g->zmax)); #undef ADD_TO_DICT @@ -364,6 +388,29 @@ static PyObject* create_grid(PyObject* self, PyObject* args) { Py_DECREF(x_list); Py_DECREF(y_list); + // Add z coordinates when 3D + if (g->nz > 1 && g->z != NULL) { + PyObject* z_list = PyList_New(0); + if (z_list == NULL) { + Py_DECREF(grid_dict); + grid_destroy(g); + return NULL; + } + for (size_t i = 0; i < g->nz; i++) { + PyObject* val = PyFloat_FromDouble(g->z[i]); + if (val == NULL || PyList_Append(z_list, val) < 0) { + Py_XDECREF(val); + Py_DECREF(z_list); + Py_DECREF(grid_dict); + grid_destroy(g); + return NULL; + } + Py_DECREF(val); + } + PyDict_SetItemString(grid_dict, "z_coords", z_list); + Py_DECREF(z_list); + } + grid_destroy(g); return grid_dict; } @@ -423,9 +470,9 @@ static PyObject* run_simulation_with_params(PyObject* self, PyObject* args, PyOb simulation_data* sim_data; if (solver_type) { - sim_data = init_simulation_with_solver(nx, ny, xmin, xmax, ymin, ymax, solver_type); + sim_data = init_simulation_with_solver(nx, ny, 1, xmin, xmax, ymin, ymax, 0.0, 0.0, solver_type); } else { - sim_data = init_simulation(nx, ny, xmin, xmax, ymin, ymax); + sim_data = init_simulation(nx, ny, 1, xmin, xmax, ymin, ymax, 0.0, 0.0); } if (sim_data == NULL) { @@ -455,7 +502,7 @@ static PyObject* run_simulation_with_params(PyObject* self, PyObject* args, PyOb // Compute velocity magnitude using derived_fields flow_field* field = sim_data->field; - derived_fields* derived = derived_fields_create(field->nx, field->ny); + derived_fields* derived = derived_fields_create(field->nx, field->ny, field->nz); if (derived != NULL) { derived_fields_compute_velocity_magnitude(derived, field); @@ -540,9 +587,10 @@ static PyObject* run_simulation_with_params(PyObject* self, PyObject* args, PyOb // Write output if requested if (output_file) { write_vtk_flow_field(output_file, sim_data->field, - sim_data->grid->nx, sim_data->grid->ny, + sim_data->grid->nx, sim_data->grid->ny, sim_data->grid->nz, sim_data->grid->xmin, sim_data->grid->xmax, - sim_data->grid->ymin, sim_data->grid->ymax); + sim_data->grid->ymin, sim_data->grid->ymax, + sim_data->grid->zmin, sim_data->grid->zmax); PyObject* output_str = PyUnicode_FromString(output_file); if (output_str != NULL) { PyDict_SetItemString(results, "output_file", output_str); @@ -578,17 +626,21 @@ static PyObject* set_output_dir(PyObject* self, PyObject* args) { */ static PyObject* write_vtk_scalar(PyObject* self, PyObject* args, PyObject* kwds) { (void)self; - static char* kwlist[] = {"filename", "field_name", "data", "nx", "ny", - "xmin", "xmax", "ymin", "ymax", NULL}; + static const char* const kwlist[] = {"filename", "field_name", "data", "nx", "ny", + "xmin", "xmax", "ymin", "ymax", + "nz", "zmin", "zmax", NULL}; const char* filename; const char* field_name; PyObject* data_list; size_t nx, ny; + size_t nz = 1; double xmin, xmax, ymin, ymax; + double zmin = 0.0, zmax = 0.0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ssOnndddd", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ssOnndddd|ndd", (char**)kwlist, &filename, &field_name, &data_list, - &nx, &ny, &xmin, &xmax, &ymin, &ymax)) { + &nx, &ny, &xmin, &xmax, &ymin, &ymax, + &nz, &zmin, &zmax)) { return NULL; } @@ -598,9 +650,9 @@ static PyObject* write_vtk_scalar(PyObject* self, PyObject* args, PyObject* kwds return NULL; } - size_t size = nx * ny; + size_t size = nx * ny * nz; if ((size_t)PyList_Size(data_list) != size) { - PyErr_Format(PyExc_ValueError, "data list size (%zd) must match nx*ny (%zu)", + PyErr_Format(PyExc_ValueError, "data list size (%zd) must match nx*ny*nz (%zu)", PyList_Size(data_list), size); return NULL; } @@ -624,7 +676,7 @@ static PyObject* write_vtk_scalar(PyObject* self, PyObject* args, PyObject* kwds } } - write_vtk_output(filename, field_name, data, nx, ny, xmin, xmax, ymin, ymax); + write_vtk_output(filename, field_name, data, nx, ny, nz, xmin, xmax, ymin, ymax, zmin, zmax); free(data); Py_RETURN_NONE; @@ -635,18 +687,23 @@ static PyObject* write_vtk_scalar(PyObject* self, PyObject* args, PyObject* kwds */ static PyObject* write_vtk_vector(PyObject* self, PyObject* args, PyObject* kwds) { (void)self; - static char* kwlist[] = {"filename", "field_name", "u_data", "v_data", "nx", "ny", - "xmin", "xmax", "ymin", "ymax", NULL}; + static const char* const kwlist[] = {"filename", "field_name", "u_data", "v_data", "nx", "ny", + "xmin", "xmax", "ymin", "ymax", + "w_data", "nz", "zmin", "zmax", NULL}; const char* filename; const char* field_name; PyObject* u_list; PyObject* v_list; + PyObject* w_list = NULL; size_t nx, ny; + size_t nz = 1; double xmin, xmax, ymin, ymax; + double zmin = 0.0, zmax = 0.0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ssOOnndddd", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ssOOnndddd|Ondd", (char**)kwlist, &filename, &field_name, &u_list, &v_list, - &nx, &ny, &xmin, &xmax, &ymin, &ymax)) { + &nx, &ny, &xmin, &xmax, &ymin, &ymax, + &w_list, &nz, &zmin, &zmax)) { return NULL; } @@ -656,20 +713,38 @@ static PyObject* write_vtk_vector(PyObject* self, PyObject* args, PyObject* kwds return NULL; } - size_t size = nx * ny; + // Treat None as no w_data + if (w_list == Py_None) { + w_list = NULL; + } + + if (w_list != NULL && !PyList_Check(w_list)) { + PyErr_SetString(PyExc_TypeError, "w_data must be a list or None"); + return NULL; + } + + size_t size = nx * ny * nz; if ((size_t)PyList_Size(u_list) != size || (size_t)PyList_Size(v_list) != size) { - PyErr_SetString(PyExc_ValueError, "data list sizes must match nx*ny"); + PyErr_SetString(PyExc_ValueError, "data list sizes must match nx*ny*nz"); return NULL; } - double* u_data = NULL; - double* v_data = NULL; + if (w_list != NULL && (size_t)PyList_Size(w_list) != size) { + PyErr_SetString(PyExc_ValueError, "w_data size must match nx*ny*nz"); + return NULL; + } - u_data = (double*)malloc(size * sizeof(double)); - v_data = (double*)malloc(size * sizeof(double)); - if (u_data == NULL || v_data == NULL) { + double* u_data = (double*)malloc(size * sizeof(double)); + double* v_data = (double*)malloc(size * sizeof(double)); + double* w_data = NULL; + if (w_list != NULL) { + w_data = (double*)malloc(size * sizeof(double)); + } + + if (u_data == NULL || v_data == NULL || (w_list != NULL && w_data == NULL)) { free(u_data); free(v_data); + free(w_data); PyErr_SetString(PyExc_MemoryError, "Failed to allocate data arrays"); return NULL; } @@ -680,20 +755,34 @@ static PyObject* write_vtk_vector(PyObject* self, PyObject* args, PyObject* kwds if (u_item == NULL || v_item == NULL) { free(u_data); free(v_data); + free(w_data); return NULL; } u_data[i] = PyFloat_AsDouble(u_item); v_data[i] = PyFloat_AsDouble(v_item); + if (w_list != NULL) { + PyObject* w_item = PyList_GetItem(w_list, i); + if (w_item == NULL) { + free(u_data); + free(v_data); + free(w_data); + return NULL; + } + w_data[i] = PyFloat_AsDouble(w_item); + } if (PyErr_Occurred()) { free(u_data); free(v_data); + free(w_data); return NULL; } } - write_vtk_vector_output(filename, field_name, u_data, v_data, nx, ny, xmin, xmax, ymin, ymax); + write_vtk_vector_output(filename, field_name, u_data, v_data, w_data, + nx, ny, nz, xmin, xmax, ymin, ymax, zmin, zmax); free(u_data); free(v_data); + free(w_data); Py_RETURN_NONE; } @@ -736,7 +825,7 @@ static PyObject* write_csv_timeseries_py(PyObject* self, PyObject* args, PyObjec } // Allocate and populate flow field - flow_field* field = flow_field_create(nx, ny); + flow_field* field = flow_field_create(nx, ny, 1); if (field == NULL) { PyErr_SetString(PyExc_MemoryError, "Failed to allocate flow field"); return NULL; @@ -1151,7 +1240,7 @@ static PyObject* create_grid_stretched_py(PyObject* self, PyObject* args) { size_t nx = (size_t)nx_signed; size_t ny = (size_t)ny_signed; - grid* g = grid_create(nx, ny, xmin, xmax, ymin, ymax); + grid* g = grid_create(nx, ny, 1, xmin, xmax, ymin, ymax, 0.0, 0.0); if (g == NULL) { PyErr_SetString(PyExc_RuntimeError, "Failed to create grid"); return NULL; @@ -2009,7 +2098,7 @@ static PyObject* compute_velocity_magnitude_py(PyObject* self, PyObject* args) { } // Create a temporary flow_field structure - flow_field* field = flow_field_create(nx, ny); + flow_field* field = flow_field_create(nx, ny, 1); if (field == NULL) { PyErr_SetString(PyExc_MemoryError, "Failed to allocate flow field"); return NULL; @@ -2026,7 +2115,7 @@ static PyObject* compute_velocity_magnitude_py(PyObject* self, PyObject* args) { } // Create derived fields and compute velocity magnitude - derived_fields* derived = derived_fields_create(nx, ny); + derived_fields* derived = derived_fields_create(nx, ny, 1); if (derived == NULL) { flow_field_destroy(field); PyErr_SetString(PyExc_MemoryError, "Failed to allocate derived fields"); @@ -2095,7 +2184,7 @@ static PyObject* compute_flow_statistics_py(PyObject* self, PyObject* args) { } // Create a temporary flow_field structure - flow_field* field = flow_field_create(nx, ny); + flow_field* field = flow_field_create(nx, ny, 1); if (field == NULL) { PyErr_SetString(PyExc_MemoryError, "Failed to allocate flow field"); return NULL; @@ -2113,7 +2202,7 @@ static PyObject* compute_flow_statistics_py(PyObject* self, PyObject* args) { } // Create derived fields and compute statistics - derived_fields* derived = derived_fields_create(nx, ny); + derived_fields* derived = derived_fields_create(nx, ny, 1); if (derived == NULL) { flow_field_destroy(field); PyErr_SetString(PyExc_MemoryError, "Failed to allocate derived fields"); @@ -2201,6 +2290,286 @@ static PyObject* compute_flow_statistics_py(PyObject* self, PyObject* args) { /* * Module definition */ +// ============================================================================ +// Library Lifecycle API (v0.2.0) +// ============================================================================ + +static PyObject* init_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + cfd_status_t status = cfd_init(); + if (status != CFD_SUCCESS) { + return raise_cfd_error(status, "cfd_init"); + } + Py_RETURN_NONE; +} + +static PyObject* finalize_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + cfd_finalize(); + Py_RETURN_NONE; +} + +static PyObject* is_initialized_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + int result = cfd_is_initialized(); + return PyBool_FromLong(result); +} + +static PyObject* get_cfd_version_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + const char* version = cfd_get_version_string(); + return PyUnicode_FromString(version); +} + +// ============================================================================ +// Poisson Solver API (v0.2.0) +// ============================================================================ + +static PyObject* get_default_poisson_params_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + poisson_solver_params_t params = poisson_solver_params_default(); + + PyObject* dict = PyDict_New(); + if (dict == NULL) return NULL; + + #define ADD_POISSON_PARAM(key, py_val) do { \ + PyObject* tmp = (py_val); \ + if (tmp == NULL) { Py_DECREF(dict); return NULL; } \ + PyDict_SetItemString(dict, key, tmp); \ + Py_DECREF(tmp); \ + } while(0) + + ADD_POISSON_PARAM("tolerance", PyFloat_FromDouble(params.tolerance)); + ADD_POISSON_PARAM("absolute_tolerance", PyFloat_FromDouble(params.absolute_tolerance)); + ADD_POISSON_PARAM("max_iterations", PyLong_FromLong(params.max_iterations)); + ADD_POISSON_PARAM("omega", PyFloat_FromDouble(params.omega)); + ADD_POISSON_PARAM("check_interval", PyLong_FromLong(params.check_interval)); + ADD_POISSON_PARAM("verbose", PyBool_FromLong(params.verbose)); + ADD_POISSON_PARAM("preconditioner", PyLong_FromLong((long)params.preconditioner)); + + #undef ADD_POISSON_PARAM + + return dict; +} + +static PyObject* poisson_get_backend_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + poisson_solver_backend_t backend = poisson_solver_get_backend(); + return PyLong_FromLong((long)backend); +} + +static PyObject* poisson_get_backend_name_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + const char* name = poisson_solver_get_backend_name(); + return PyUnicode_FromString(name); +} + +static PyObject* poisson_set_backend_py(PyObject* self, PyObject* args) { + (void)self; + int backend; + + if (!PyArg_ParseTuple(args, "i", &backend)) { + return NULL; + } + + bool success = poisson_solver_set_backend((poisson_solver_backend_t)backend); + return PyBool_FromLong(success); +} + +static PyObject* poisson_backend_available_py(PyObject* self, PyObject* args) { + (void)self; + int backend; + + if (!PyArg_ParseTuple(args, "i", &backend)) { + return NULL; + } + + bool available = poisson_solver_backend_available((poisson_solver_backend_t)backend); + return PyBool_FromLong(available); +} + +static PyObject* poisson_simd_available_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + bool available = poisson_solver_simd_available(); + return PyBool_FromLong(available); +} + +// ============================================================================ +// GPU Device API (v0.2.0) +// ============================================================================ + +static PyObject* gpu_is_available_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + int available = gpu_is_available(); + return PyBool_FromLong(available); +} + +static PyObject* gpu_get_device_info_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + gpu_device_info_t devices[8]; + int count = gpu_get_device_info(devices, 8); + + PyObject* list = PyList_New(0); + if (list == NULL) return NULL; + + for (int i = 0; i < count; i++) { + PyObject* info = PyDict_New(); + if (info == NULL) { + Py_DECREF(list); + return NULL; + } + + #define ADD_GPU_INFO(key, py_val) do { \ + PyObject* tmp = (py_val); \ + if (tmp == NULL) { Py_DECREF(info); Py_DECREF(list); return NULL; } \ + PyDict_SetItemString(info, key, tmp); \ + Py_DECREF(tmp); \ + } while(0) + + ADD_GPU_INFO("device_id", PyLong_FromLong(devices[i].device_id)); + ADD_GPU_INFO("name", PyUnicode_FromString(devices[i].name)); + ADD_GPU_INFO("total_memory", PyLong_FromSize_t(devices[i].total_memory)); + ADD_GPU_INFO("free_memory", PyLong_FromSize_t(devices[i].free_memory)); + ADD_GPU_INFO("compute_capability_major", PyLong_FromLong(devices[i].compute_capability_major)); + ADD_GPU_INFO("compute_capability_minor", PyLong_FromLong(devices[i].compute_capability_minor)); + ADD_GPU_INFO("multiprocessor_count", PyLong_FromLong(devices[i].multiprocessor_count)); + ADD_GPU_INFO("max_threads_per_block", PyLong_FromLong(devices[i].max_threads_per_block)); + ADD_GPU_INFO("warp_size", PyLong_FromLong(devices[i].warp_size)); + ADD_GPU_INFO("is_available", PyBool_FromLong(devices[i].is_available)); + + #undef ADD_GPU_INFO + + if (PyList_Append(list, info) < 0) { + Py_DECREF(info); + Py_DECREF(list); + return NULL; + } + Py_DECREF(info); + } + + return list; +} + +static PyObject* gpu_select_device_py(PyObject* self, PyObject* args) { + (void)self; + int device_id; + + if (!PyArg_ParseTuple(args, "i", &device_id)) { + return NULL; + } + + cfd_status_t status = gpu_select_device(device_id); + if (status != CFD_SUCCESS) { + return raise_cfd_error(status, "gpu_select_device"); + } + Py_RETURN_NONE; +} + +static PyObject* gpu_get_default_config_py(PyObject* self, PyObject* args) { + (void)self; + (void)args; + + gpu_config_t config = gpu_config_default(); + + PyObject* dict = PyDict_New(); + if (dict == NULL) return NULL; + + #define ADD_GPU_CONFIG(key, py_val) do { \ + PyObject* tmp = (py_val); \ + if (tmp == NULL) { Py_DECREF(dict); return NULL; } \ + PyDict_SetItemString(dict, key, tmp); \ + Py_DECREF(tmp); \ + } while(0) + + ADD_GPU_CONFIG("enable_gpu", PyBool_FromLong(config.enable_gpu)); + ADD_GPU_CONFIG("min_grid_size", PyLong_FromSize_t(config.min_grid_size)); + ADD_GPU_CONFIG("min_steps", PyLong_FromLong(config.min_steps)); + ADD_GPU_CONFIG("block_size_x", PyLong_FromLong(config.block_size_x)); + ADD_GPU_CONFIG("block_size_y", PyLong_FromLong(config.block_size_y)); + ADD_GPU_CONFIG("poisson_max_iter", PyLong_FromLong(config.poisson_max_iter)); + ADD_GPU_CONFIG("poisson_tolerance", PyFloat_FromDouble(config.poisson_tolerance)); + ADD_GPU_CONFIG("persistent_memory", PyBool_FromLong(config.persistent_memory)); + ADD_GPU_CONFIG("async_transfers", PyBool_FromLong(config.async_transfers)); + ADD_GPU_CONFIG("sync_after_kernel", PyBool_FromLong(config.sync_after_kernel)); + ADD_GPU_CONFIG("verbose", PyBool_FromLong(config.verbose)); + + #undef ADD_GPU_CONFIG + + return dict; +} + +// ============================================================================ +// Logging API (v0.2.0) +// ============================================================================ + +static PyObject* g_log_callback = NULL; + +static void python_log_callback(cfd_log_level_t level, const char* message) { + PyGILState_STATE gstate = PyGILState_Ensure(); + + if (g_log_callback != NULL && g_log_callback != Py_None) { + const char* safe_message = (message != NULL) ? message : ""; + PyObject* result = PyObject_CallFunction( + g_log_callback, "is", (int)level, safe_message); + Py_XDECREF(result); + if (PyErr_Occurred()) { + PyErr_Clear(); + } + } + + PyGILState_Release(gstate); +} + +static PyObject* set_log_callback_py(PyObject* self, PyObject* args) { + (void)self; + PyObject* callback; + + if (!PyArg_ParseTuple(args, "O", &callback)) { + return NULL; + } + + if (callback == Py_None) { + Py_XDECREF(g_log_callback); + g_log_callback = NULL; + cfd_set_log_callback(NULL); + Py_RETURN_NONE; + } + + if (!PyCallable_Check(callback)) { + PyErr_SetString(PyExc_TypeError, "callback must be callable or None"); + return NULL; + } + + Py_XDECREF(g_log_callback); + Py_INCREF(callback); + g_log_callback = callback; + cfd_set_log_callback(python_log_callback); + + Py_RETURN_NONE; +} + +// ============================================================================ + static PyMethodDef cfd_python_methods[] = { {"run_simulation", (PyCFunction)run_simulation, METH_VARARGS | METH_KEYWORDS, "Run a complete CFD simulation.\n\n" @@ -2216,7 +2585,7 @@ static PyMethodDef cfd_python_methods[] = { " output_file (str, optional): VTK output file path\n\n" "Returns:\n" " list: Velocity magnitude values as a flat list"}, - {"create_grid", create_grid, METH_VARARGS, + {"create_grid", (PyCFunction)create_grid, METH_VARARGS | METH_KEYWORDS, "Create a computational grid and return its properties.\n\n" "Args:\n" " nx (int): Number of grid points in x direction\n" @@ -2224,7 +2593,10 @@ static PyMethodDef cfd_python_methods[] = { " xmin (float): Minimum x coordinate\n" " xmax (float): Maximum x coordinate\n" " ymin (float): Minimum y coordinate\n" - " ymax (float): Maximum y coordinate\n\n" + " ymax (float): Maximum y coordinate\n" + " nz (int, optional): Grid points in z direction (default: 1)\n" + " zmin (float, optional): Minimum z coordinate (default: 0.0)\n" + " zmax (float, optional): Maximum z coordinate (default: 0.0)\n\n" "Returns:\n" " dict: Grid properties including coordinates"}, {"get_default_solver_params", get_default_solver_params, METH_NOARGS, @@ -2473,6 +2845,75 @@ static PyMethodDef cfd_python_methods[] = { "Returns:\n" " bool: True if any SIMD instruction set is available"}, // Grid Initialization Variants (Phase 6) + // Library Lifecycle API (v0.2.0) + {"init", init_py, METH_NOARGS, + "Initialize the CFD library.\n\n" + "Raises:\n" + " RuntimeError: If initialization fails"}, + {"finalize", finalize_py, METH_NOARGS, + "Finalize and clean up the CFD library."}, + {"is_initialized", is_initialized_py, METH_NOARGS, + "Check if the CFD library is initialized.\n\n" + "Returns:\n" + " bool: True if initialized"}, + {"get_cfd_version", get_cfd_version_py, METH_NOARGS, + "Get the CFD C library version string.\n\n" + "Returns:\n" + " str: Version string (e.g., '0.2.0')"}, + // Poisson Solver API (v0.2.0) + {"get_default_poisson_params", get_default_poisson_params_py, METH_NOARGS, + "Get default Poisson solver parameters.\n\n" + "Returns:\n" + " dict: Parameters (tolerance, absolute_tolerance, max_iterations, omega, etc.)"}, + {"poisson_get_backend", poisson_get_backend_py, METH_NOARGS, + "Get the current Poisson solver backend.\n\n" + "Returns:\n" + " int: Backend type (POISSON_BACKEND_*)"}, + {"poisson_get_backend_name", poisson_get_backend_name_py, METH_NOARGS, + "Get the name of the current Poisson solver backend.\n\n" + "Returns:\n" + " str: Backend name"}, + {"poisson_set_backend", poisson_set_backend_py, METH_VARARGS, + "Set the Poisson solver backend.\n\n" + "Args:\n" + " backend (int): Backend type constant\n\n" + "Returns:\n" + " bool: True if backend was set successfully"}, + {"poisson_backend_available", poisson_backend_available_py, METH_VARARGS, + "Check if a Poisson solver backend is available.\n\n" + "Args:\n" + " backend (int): Backend type constant\n\n" + "Returns:\n" + " bool: True if available"}, + {"poisson_simd_available", poisson_simd_available_py, METH_NOARGS, + "Check if SIMD-accelerated Poisson solver is available.\n\n" + "Returns:\n" + " bool: True if SIMD Poisson solver is available"}, + // GPU Device API (v0.2.0) + {"gpu_is_available", gpu_is_available_py, METH_NOARGS, + "Check if GPU acceleration is available.\n\n" + "Returns:\n" + " bool: True if GPU is available"}, + {"gpu_get_device_info", gpu_get_device_info_py, METH_NOARGS, + "Get information about available GPU devices.\n\n" + "Returns:\n" + " list[dict]: Device info (name, memory, compute capability, etc.)"}, + {"gpu_select_device", gpu_select_device_py, METH_VARARGS, + "Select a GPU device by ID.\n\n" + "Args:\n" + " device_id (int): Device ID to select\n\n" + "Raises:\n" + " RuntimeError: If device selection fails"}, + {"gpu_get_default_config", gpu_get_default_config_py, METH_NOARGS, + "Get default GPU configuration.\n\n" + "Returns:\n" + " dict: GPU config parameters"}, + // Logging API (v0.2.0) + {"set_log_callback", set_log_callback_py, METH_VARARGS, + "Set a Python callback for CFD library log messages.\n\n" + "Args:\n" + " callback (callable or None): Function(level: int, message: str). Pass None to clear."}, + // Grid Initialization Variants {"create_grid_stretched", create_grid_stretched_py, METH_VARARGS, "Create a grid with stretched (non-uniform) spacing.\n\n" "WARNING: The current implementation has a known bug. The cosh-based formula\n" @@ -2493,10 +2934,17 @@ static PyMethodDef cfd_python_methods[] = { {NULL, NULL, 0, NULL} }; +static void cfd_python_free(void* module) { + (void)module; + Py_XDECREF(g_log_callback); + g_log_callback = NULL; + cfd_set_log_callback(NULL); +} + static struct PyModuleDef cfd_python_module = { PyModuleDef_HEAD_INIT, "cfd_python", - "Python bindings for CFD simulation library v0.1.5+ with pluggable solver support.\n\n" + "Python bindings for CFD simulation library v0.2.0 with pluggable solver support.\n\n" "Available functions:\n" " - list_solvers(): Get available solver types\n" " - has_solver(name): Check if a solver exists\n" @@ -2523,7 +2971,11 @@ static struct PyModuleDef cfd_python_module = { " - 'explicit_euler_gpu': GPU-accelerated Euler solver\n" " - 'projection_jacobi_gpu': GPU-accelerated projection solver", -1, - cfd_python_methods + cfd_python_methods, + NULL, /* m_slots */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + cfd_python_free /* m_free */ }; PyMODINIT_FUNC PyInit_cfd_python(void) { @@ -2604,7 +3056,9 @@ PyMODINIT_FUNC PyInit_cfd_python(void) { PyModule_AddIntConstant(m, "CFD_ERROR_IO", CFD_ERROR_IO) < 0 || PyModule_AddIntConstant(m, "CFD_ERROR_UNSUPPORTED", CFD_ERROR_UNSUPPORTED) < 0 || PyModule_AddIntConstant(m, "CFD_ERROR_DIVERGED", CFD_ERROR_DIVERGED) < 0 || - PyModule_AddIntConstant(m, "CFD_ERROR_MAX_ITER", CFD_ERROR_MAX_ITER) < 0) { + PyModule_AddIntConstant(m, "CFD_ERROR_MAX_ITER", CFD_ERROR_MAX_ITER) < 0 || + PyModule_AddIntConstant(m, "CFD_ERROR_LIMIT_EXCEEDED", CFD_ERROR_LIMIT_EXCEEDED) < 0 || + PyModule_AddIntConstant(m, "CFD_ERROR_NOT_FOUND", CFD_ERROR_NOT_FOUND) < 0) { Py_DECREF(m); return NULL; } @@ -2615,7 +3069,8 @@ PyMODINIT_FUNC PyInit_cfd_python(void) { PyModule_AddIntConstant(m, "BC_TYPE_DIRICHLET", BC_TYPE_DIRICHLET) < 0 || PyModule_AddIntConstant(m, "BC_TYPE_NOSLIP", BC_TYPE_NOSLIP) < 0 || PyModule_AddIntConstant(m, "BC_TYPE_INLET", BC_TYPE_INLET) < 0 || - PyModule_AddIntConstant(m, "BC_TYPE_OUTLET", BC_TYPE_OUTLET) < 0) { + PyModule_AddIntConstant(m, "BC_TYPE_OUTLET", BC_TYPE_OUTLET) < 0 || + PyModule_AddIntConstant(m, "BC_TYPE_SYMMETRY", BC_TYPE_SYMMETRY) < 0) { Py_DECREF(m); return NULL; } @@ -2624,7 +3079,9 @@ PyMODINIT_FUNC PyInit_cfd_python(void) { if (PyModule_AddIntConstant(m, "BC_EDGE_LEFT", BC_EDGE_LEFT) < 0 || PyModule_AddIntConstant(m, "BC_EDGE_RIGHT", BC_EDGE_RIGHT) < 0 || PyModule_AddIntConstant(m, "BC_EDGE_BOTTOM", BC_EDGE_BOTTOM) < 0 || - PyModule_AddIntConstant(m, "BC_EDGE_TOP", BC_EDGE_TOP) < 0) { + PyModule_AddIntConstant(m, "BC_EDGE_TOP", BC_EDGE_TOP) < 0 || + PyModule_AddIntConstant(m, "BC_EDGE_FRONT", BC_EDGE_FRONT) < 0 || + PyModule_AddIntConstant(m, "BC_EDGE_BACK", BC_EDGE_BACK) < 0) { Py_DECREF(m); return NULL; } @@ -2656,5 +3113,63 @@ PyMODINIT_FUNC PyInit_cfd_python(void) { return NULL; } + // Add version constants (v0.2.0) + if (PyModule_AddIntConstant(m, "CFD_VERSION_MAJOR", CFD_VERSION_MAJOR) < 0 || + PyModule_AddIntConstant(m, "CFD_VERSION_MINOR", CFD_VERSION_MINOR) < 0 || + PyModule_AddIntConstant(m, "CFD_VERSION_PATCH", CFD_VERSION_PATCH) < 0) { + Py_DECREF(m); + return NULL; + } + + // Add Poisson solver method constants (v0.2.0) + if (PyModule_AddIntConstant(m, "POISSON_METHOD_JACOBI", POISSON_METHOD_JACOBI) < 0 || + PyModule_AddIntConstant(m, "POISSON_METHOD_GAUSS_SEIDEL", POISSON_METHOD_GAUSS_SEIDEL) < 0 || + PyModule_AddIntConstant(m, "POISSON_METHOD_SOR", POISSON_METHOD_SOR) < 0 || + PyModule_AddIntConstant(m, "POISSON_METHOD_REDBLACK_SOR", POISSON_METHOD_REDBLACK_SOR) < 0 || + PyModule_AddIntConstant(m, "POISSON_METHOD_CG", POISSON_METHOD_CG) < 0 || + PyModule_AddIntConstant(m, "POISSON_METHOD_BICGSTAB", POISSON_METHOD_BICGSTAB) < 0 || + PyModule_AddIntConstant(m, "POISSON_METHOD_MULTIGRID", POISSON_METHOD_MULTIGRID) < 0) { + Py_DECREF(m); + return NULL; + } + + // Add Poisson solver backend constants (v0.2.0) + if (PyModule_AddIntConstant(m, "POISSON_BACKEND_AUTO", POISSON_BACKEND_AUTO) < 0 || + PyModule_AddIntConstant(m, "POISSON_BACKEND_SCALAR", POISSON_BACKEND_SCALAR) < 0 || + PyModule_AddIntConstant(m, "POISSON_BACKEND_OMP", POISSON_BACKEND_OMP) < 0 || + PyModule_AddIntConstant(m, "POISSON_BACKEND_SIMD", POISSON_BACKEND_SIMD) < 0 || + PyModule_AddIntConstant(m, "POISSON_BACKEND_GPU", POISSON_BACKEND_GPU) < 0) { + Py_DECREF(m); + return NULL; + } + + // Add Poisson solver type preset constants (v0.2.0) + if (PyModule_AddIntConstant(m, "POISSON_SOLVER_SOR_SCALAR", POISSON_SOLVER_SOR_SCALAR) < 0 || + PyModule_AddIntConstant(m, "POISSON_SOLVER_JACOBI_SIMD", POISSON_SOLVER_JACOBI_SIMD) < 0 || + PyModule_AddIntConstant(m, "POISSON_SOLVER_REDBLACK_SIMD", POISSON_SOLVER_REDBLACK_SIMD) < 0 || + PyModule_AddIntConstant(m, "POISSON_SOLVER_REDBLACK_OMP", POISSON_SOLVER_REDBLACK_OMP) < 0 || + PyModule_AddIntConstant(m, "POISSON_SOLVER_REDBLACK_SCALAR", POISSON_SOLVER_REDBLACK_SCALAR) < 0 || + PyModule_AddIntConstant(m, "POISSON_SOLVER_CG_SCALAR", POISSON_SOLVER_CG_SCALAR) < 0 || + PyModule_AddIntConstant(m, "POISSON_SOLVER_CG_SIMD", POISSON_SOLVER_CG_SIMD) < 0 || + PyModule_AddIntConstant(m, "POISSON_SOLVER_CG_OMP", POISSON_SOLVER_CG_OMP) < 0) { + Py_DECREF(m); + return NULL; + } + + // Add Poisson preconditioner constants (v0.2.0) + if (PyModule_AddIntConstant(m, "POISSON_PRECOND_NONE", POISSON_PRECOND_NONE) < 0 || + PyModule_AddIntConstant(m, "POISSON_PRECOND_JACOBI", POISSON_PRECOND_JACOBI) < 0) { + Py_DECREF(m); + return NULL; + } + + // Add log level constants (v0.2.0) + if (PyModule_AddIntConstant(m, "CFD_LOG_LEVEL_INFO", CFD_LOG_LEVEL_INFO) < 0 || + PyModule_AddIntConstant(m, "CFD_LOG_LEVEL_WARNING", CFD_LOG_LEVEL_WARNING) < 0 || + PyModule_AddIntConstant(m, "CFD_LOG_LEVEL_ERROR", CFD_LOG_LEVEL_ERROR) < 0) { + Py_DECREF(m); + return NULL; + } + return m; }