Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 36 additions & 23 deletions MIGRATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,29 +365,42 @@ cpu_features_t cfd_get_cpu_features(void);

**Actual effort:** < 1 day

### Phase 4: Add Error Handling API (Important)
### Phase 4: Add Error Handling API (Important) ✅ COMPLETED

**Priority:** P1 - Better debugging

**Tasks:**

- [ ] **4.1 Expose error functions**
- `get_last_error()` → Python string
- `get_last_status()` → Python enum
- `clear_error()`

- [ ] **4.2 Create Python exceptions**
- `CFDError` base exception
- `CFDMemoryError`
- `CFDInvalidError`
- `CFDUnsupportedError`
**Status:** Completed on 2026-01-02

- [ ] **4.3 Integrate with all functions**
- Check return codes
- Raise appropriate exceptions
- Include error messages
**Tasks:**

**Estimated effort:** 1 day
- [x] **4.1 Expose error functions**
- `get_last_error()` → Python string (already in C extension)
- `get_last_status()` → Python enum (already in C extension)
- `get_error_string(code)` → Python string (already in C extension)
- `clear_error()` (already in C extension)

- [x] **4.2 Create Python exceptions**
- Created `cfd_python/_exceptions.py` with exception hierarchy
- `CFDError` base exception with `status_code` and `message` attributes
- `CFDMemoryError(CFDError, MemoryError)` - for CFD_ERROR_NOMEM (-2)
- `CFDInvalidError(CFDError, ValueError)` - for CFD_ERROR_INVALID (-3)
- `CFDIOError(CFDError, IOError)` - for CFD_ERROR_IO (-4)
- `CFDUnsupportedError(CFDError, NotImplementedError)` - for CFD_ERROR_UNSUPPORTED (-5)
- `CFDDivergedError(CFDError)` - for CFD_ERROR_DIVERGED (-6)
- `CFDMaxIterError(CFDError)` - for CFD_ERROR_MAX_ITER (-7)

- [x] **4.3 Implement raise_for_status helper**
- `raise_for_status(status_code, context="")` - Raises appropriate exception based on status code
- Maps status codes to exception classes
- Includes error message from C library when available

- [x] **4.4 Add tests**
- Added tests to `tests/test_errors.py`
- Tests for exception class hierarchy and inheritance
- Tests for `raise_for_status` function with all error codes
- Tests for export verification

**Actual effort:** < 0.5 days

### Phase 5: Add Backend Availability API (v0.1.6 Feature) ✅ COMPLETED

Expand Down Expand Up @@ -547,12 +560,12 @@ find_library(CFD_LIBRARY cfd_library) # Unified library name
| Phase 2: Boundary Conditions | ~~3-4 days~~ ✅ 1 day | ~~5-7 days~~ 2 days |
| Phase 2.5: CI/Build System (v0.1.6) | ✅ 1 day | 3 days |
| Phase 3: Derived Fields | ~~1-2 days~~ ✅ < 1 day | 3.5 days |
| Phase 4: Error Handling | 1 day | 4.5 days |
| Phase 5: Backend Availability (v0.1.6) | ✅ 0.5 days | 4 days |
| Phase 6: CPU Features | 1 day | 5 days |
| Phase 7: Docs & Tests | 2 days | 7 days |
| Phase 4: Error Handling | ~~1 day~~ ✅ < 0.5 days | 4 days |
| Phase 5: Backend Availability (v0.1.6) | ✅ 0.5 days | 4.5 days |
| Phase 6: CPU Features | 1 day | 5.5 days |
| Phase 7: Docs & Tests | 2 days | 7.5 days |

**Total estimated effort:** ~~9-10 days~~ ~7 days (4 days completed)
**Total estimated effort:** ~~9-10 days~~ ~7.5 days (4.5 days completed)

---

Expand Down
19 changes: 19 additions & 0 deletions cfd_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@
- get_available_backends(): Get list of all available backends
"""

from ._exceptions import (
CFDDivergedError,
CFDError,
CFDInvalidError,
CFDIOError,
CFDMaxIterError,
CFDMemoryError,
CFDUnsupportedError,
raise_for_status,
)
from ._version import get_version

__version__ = get_version()
Expand Down Expand Up @@ -166,6 +176,15 @@
"backend_get_name",
"list_solvers_by_backend",
"get_available_backends",
# Exception classes (Phase 4)
"CFDError",
"CFDMemoryError",
"CFDInvalidError",
"CFDIOError",
"CFDUnsupportedError",
"CFDDivergedError",
"CFDMaxIterError",
"raise_for_status",
]

# Load C extension and populate module namespace
Expand Down
129 changes: 129 additions & 0 deletions cfd_python/_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""CFD Python exception classes for structured error handling."""

__all__ = [
"CFDError",
"CFDMemoryError",
"CFDInvalidError",
"CFDIOError",
"CFDUnsupportedError",
"CFDDivergedError",
"CFDMaxIterError",
"raise_for_status",
]


class CFDError(Exception):
"""Base exception for CFD library errors.

Attributes:
status_code: The CFD status code that triggered the error.
message: Human-readable error message.
"""

def __init__(self, message: str, status_code: int = -1):
self.status_code = status_code
self.message = message
super().__init__(f"{message} (status={status_code})")


class CFDMemoryError(CFDError, MemoryError):
"""Raised when CFD library fails to allocate memory.

Corresponds to CFD_ERROR_NOMEM (-2).
"""

pass


class CFDInvalidError(CFDError, ValueError):
"""Raised when an invalid argument is passed to a CFD function.

Corresponds to CFD_ERROR_INVALID (-3).
"""

pass


class CFDIOError(CFDError, IOError):
"""Raised when a file I/O operation fails.

Corresponds to CFD_ERROR_IO (-4).
"""

pass


class CFDUnsupportedError(CFDError, NotImplementedError):
"""Raised when an unsupported operation is requested.

Corresponds to CFD_ERROR_UNSUPPORTED (-5).
This typically occurs when requesting a backend that is not available.
"""

pass


class CFDDivergedError(CFDError):
"""Raised when the solver diverges during computation.

Corresponds to CFD_ERROR_DIVERGED (-6).
This indicates numerical instability, often due to time step being too large.
"""

pass


class CFDMaxIterError(CFDError):
"""Raised when solver reaches maximum iteration limit without converging.

Corresponds to CFD_ERROR_MAX_ITER (-7).
Consider increasing max_iter or adjusting solver parameters.
"""

pass


# Mapping from status codes to exception classes
_STATUS_TO_EXCEPTION = {
-1: CFDError, # CFD_ERROR (generic)
-2: CFDMemoryError, # CFD_ERROR_NOMEM
-3: CFDInvalidError, # CFD_ERROR_INVALID
-4: CFDIOError, # CFD_ERROR_IO
-5: CFDUnsupportedError, # CFD_ERROR_UNSUPPORTED
-6: CFDDivergedError, # CFD_ERROR_DIVERGED
-7: CFDMaxIterError, # CFD_ERROR_MAX_ITER
}


def raise_for_status(status_code: int, context: str = "") -> None:
"""Raise an appropriate exception if status_code indicates an error.

Args:
status_code: The CFD status code to check.
context: Optional context string to include in the error message.

Raises:
CFDError: Or an appropriate subclass based on the status code.

Example:
>>> status = some_cfd_operation()
>>> raise_for_status(status, "during simulation step")
"""
if status_code >= 0:
return # Success

# Try to get error message from CFD library
try:
from . import get_error_string, get_last_error

error_msg = get_last_error()
if not error_msg:
error_msg = get_error_string(status_code)
except (ImportError, AttributeError):
error_msg = f"CFD error code {status_code}"

if context:
error_msg = f"{context}: {error_msg}"

exception_class = _STATUS_TO_EXCEPTION.get(status_code, CFDError)
raise exception_class(error_msg, status_code)
Loading
Loading