-
Notifications
You must be signed in to change notification settings - Fork 95
Description
Description
The geometry file loading functions (CHEASE, FBT, EQDSK) in TORAX lack proper error handling for common failure scenarios. This leads to unhelpful stack traces and cryptic error messages when users encounter file-related issues, making debugging difficult and frustrating for users.
Problem
When users attempt to load geometry files, several common failure modes produce unclear error messages:
- File not found: Generic
FileNotFoundErrorwithout context - Corrupted file format: Deep stack traces from parsing libraries
- Permission errors: System-level errors that don't explain what's needed
- Invalid file structure: Unclear which part of the file format is wrong
- Wrong file type: No validation that file matches expected format
Example of current poor error experience:
# User runs:
geometry = load_chease_file('/wrong/path/file.chease')
# Gets unhelpful error:
FileNotFoundError: [Errno 2] No such file or directory: '/wrong/path/file.chease'
# User doesn't know if path is wrong, file doesn't exist, or permissions issueProposed Solution
Add comprehensive error handling with clear, actionable error messages:
import os
from pathlib import Path
from typing import Union, Optional
import logging
logger = logging.getLogger(__name__)
class GeometryFileError(Exception):
"""Base exception for geometry file errors."""
pass
class GeometryFileNotFoundError(GeometryFileError):
"""Raised when geometry file doesn't exist."""
pass
class GeometryFileFormatError(GeometryFileError):
"""Raised when geometry file format is invalid."""
pass
class GeometryFilePermissionError(GeometryFileError):
"""Raised when geometry file has permission issues."""
pass
def load_geometry_file(
file_path: Union[str, Path],
file_type: str,
validate_format: bool = True
) -> GeometryData:
"""Load geometry file with comprehensive error handling.
Args:
file_path: Path to geometry file (CHEASE, FBT, or EQDSK)
file_type: Type of geometry file ('chease', 'fbt', 'eqdsk')
validate_format: Whether to validate file format before parsing
Returns:
GeometryData object containing parsed geometry
Raises:
GeometryFileNotFoundError: If file doesn't exist
GeometryFilePermissionError: If file can't be read
GeometryFileFormatError: If file format is invalid
Example:
>>> geometry = load_geometry_file('iter_hybrid.chease', 'chease')
"""
path = Path(file_path)
# Check file exists
if not path.exists():
# Provide helpful suggestions
parent_dir = path.parent
similar_files = []
if parent_dir.exists():
# Find similar filenames
similar_files = [
f.name for f in parent_dir.glob(f"*{path.suffix}")
][:5]
error_msg = (
f"Geometry file not found: {file_path}\n"
f"Please check:\n"
f" 1. File path is correct\n"
f" 2. File exists at the specified location\n"
f" 3. Filename spelling is correct"
)
if similar_files:
error_msg += f"\n\nSimilar files found in directory:\n - " + "\n - ".join(similar_files)
logger.error(error_msg)
raise GeometryFileNotFoundError(error_msg)
# Check file permissions
if not os.access(path, os.R_OK):
error_msg = (
f"No read permission for geometry file: {file_path}\n"
f"Please check file permissions and run:\n"
f" chmod +r {file_path}"
)
logger.error(error_msg)
raise GeometryFilePermissionError(error_msg)
# Check file is not empty
if path.stat().st_size == 0:
error_msg = f"Geometry file is empty: {file_path}"
logger.error(error_msg)
raise GeometryFileFormatError(error_msg)
# Validate file type
expected_extensions = {
'chease': ['.chease', '.txt'],
'fbt': ['.fbt', '.dat'],
'eqdsk': ['.eqdsk', '.geqdsk']
}
if file_type.lower() in expected_extensions:
valid_ext = path.suffix.lower() in expected_extensions[file_type.lower()]
if not valid_ext and validate_format:
logger.warning(
f"File extension '{path.suffix}' unusual for {file_type.upper()} files. "
f"Expected: {expected_extensions[file_type.lower()]}"
)
# Parse file with detailed error handling
try:
logger.info(f"Loading {file_type.upper()} geometry file: {file_path}")
data = _parse_geometry_file(path, file_type)
logger.info(f"Successfully loaded geometry file with {data.num_points} points")
return data
except ValueError as e:
error_msg = (
f"Failed to parse {file_type.upper()} geometry file: {file_path}\n"
f"Error: {str(e)}\n"
f"Please verify:\n"
f" 1. File format matches {file_type.upper()} specification\n"
f" 2. File is not corrupted\n"
f" 3. File was generated by compatible software\n"
f"\nFor format specifications, see: https://torax.readthedocs.io/geometry-formats"
)
logger.error(error_msg)
raise GeometryFileFormatError(error_msg) from e
except UnicodeDecodeError as e:
error_msg = (
f"Failed to decode {file_type.upper()} geometry file: {file_path}\n"
f"File appears to be binary or has incorrect encoding.\n"
f"Expected: UTF-8 or ASCII text file"
)
logger.error(error_msg)
raise GeometryFileFormatError(error_msg) from e
except Exception as e:
error_msg = (
f"Unexpected error loading geometry file: {file_path}\n"
f"Error type: {type(e).__name__}\n"
f"Error message: {str(e)}\n"
f"Please report this issue at: https://github.com/google-deepmind/torax/issues"
)
logger.error(error_msg)
raise GeometryFileError(error_msg) from e
def validate_geometry_data(data: GeometryData, file_type: str) -> None:
"""Validate loaded geometry data meets physical constraints.
Args:
data: Loaded geometry data
file_type: Type of geometry file
Raises:
GeometryFileFormatError: If data fails validation
"""
issues = []
# Check for required fields
if not hasattr(data, 'R'):
issues.append("Missing major radius (R) data")
if not hasattr(data, 'Z'):
issues.append("Missing vertical position (Z) data")
# Check physical constraints
if hasattr(data, 'R') and (data.R <= 0).any():
issues.append("Major radius (R) must be positive")
if hasattr(data, 'rho') and (data.rho < 0).any():
issues.append("Normalized radius (rho) must be non-negative")
if hasattr(data, 'psi') and (data.psi < 0).any():
issues.append("Poloidal flux (psi) contains negative values (unusual)")
if issues:
error_msg = (
f"Geometry data validation failed for {file_type.upper()} file:\n" +
"\n".join(f" - {issue}" for issue in issues)
)
raise GeometryFileFormatError(error_msg)
# Convenience functions for each format
def load_chease_file(file_path: Union[str, Path]) -> GeometryData:
"""Load CHEASE geometry file with error handling."""
return load_geometry_file(file_path, 'chease')
def load_fbt_file(file_path: Union[str, Path]) -> GeometryData:
"""Load FBT geometry file with error handling."""
return load_geometry_file(file_path, 'fbt')
def load_eqdsk_file(file_path: Union[str, Path]) -> GeometryData:
"""Load EQDSK geometry file with error handling."""
return load_geometry_file(file_path, 'eqdsk')Benefits
- Better user experience: Clear, actionable error messages
- Faster debugging: Users can identify issues immediately
- Reduced support burden: Fewer "file not loading" issues
- Helpful suggestions: Automatically suggests similar files or common fixes
- Proper error hierarchy: Custom exceptions allow targeted error handling
- Logging support: Integration with Python logging for debugging
- Validation: Catches invalid data early before it causes issues downstream
Implementation Plan
- Create custom exception classes (
GeometryFileError, etc.) - Add comprehensive error handling to file loading functions
- Add file validation (exists, readable, not empty, correct format)
- Add data validation after loading (physical constraints)
- Add helpful error messages with suggestions
- Add logging throughout the loading process
- Update documentation with common error scenarios
- Add tests for all error conditions
Files to Modify
torax/geometry.py- Add error handling to geometry loadingtorax/geometry/__init__.py- Export custom exceptionstorax/geometry/chease.py- Add CHEASE-specific error handlingtorax/geometry/fbt.py- Add FBT-specific error handlingtorax/geometry/eqdsk.py- Add EQDSK-specific error handlingdocs/troubleshooting.md- Document common file loading errors
Testing
Create comprehensive tests for error conditions:
def test_file_not_found():
"""Test clear error when file doesn't exist."""
with pytest.raises(GeometryFileNotFoundError) as exc_info:
load_geometry_file('/nonexistent/file.chease', 'chease')
assert "not found" in str(exc_info.value).lower()
assert "Please check" in str(exc_info.value)
def test_permission_error(tmp_path):
"""Test clear error for permission issues."""
file_path = tmp_path / "readonly.chease"
file_path.write_text("data")
file_path.chmod(0o000) # Remove all permissions
with pytest.raises(GeometryFilePermissionError) as exc_info:
load_geometry_file(file_path, 'chease')
assert "permission" in str(exc_info.value).lower()
assert "chmod" in str(exc_info.value)
def test_empty_file(tmp_path):
"""Test clear error for empty files."""
file_path = tmp_path / "empty.chease"
file_path.touch()
with pytest.raises(GeometryFileFormatError) as exc_info:
load_geometry_file(file_path, 'chease')
assert "empty" in str(exc_info.value).lower()
def test_invalid_format(tmp_path):
"""Test clear error for invalid file format."""
file_path = tmp_path / "invalid.chease"
file_path.write_text("not a valid chease file")
with pytest.raises(GeometryFileFormatError) as exc_info:
load_geometry_file(file_path, 'chease')
assert "format" in str(exc_info.value).lower()
assert "specification" in str(exc_info.value).lower()
def test_similar_files_suggestion(tmp_path):
"""Test that similar files are suggested."""
(tmp_path / "file1.chease").touch()
(tmp_path / "file2.chease").touch()
with pytest.raises(GeometryFileNotFoundError) as exc_info:
load_geometry_file(tmp_path / "file3.chease", 'chease')
assert "Similar files found" in str(exc_info.value)Example Error Messages
Before (current behavior):
FileNotFoundError: [Errno 2] No such file or directory: 'geometry.chease'
After (proposed behavior):
GeometryFileNotFoundError: Geometry file not found: geometry.chease
Please check:
1. File path is correct
2. File exists at the specified location
3. Filename spelling is correct
Similar files found in directory:
- geometry_backup.chease
- geometry_old.chease
- iter_hybrid.chease
Additional Context
This improvement aligns with best practices for scientific software:
- NumPy, SciPy, and pandas all provide detailed error messages for file I/O
- Clear error messages significantly reduce support burden
- Proper exception hierarchies enable better error handling in user code
- Validation catches issues early before they propagate
Checklist
- Create custom exception classes
- Add file existence and permission checks
- Add file format validation
- Add data validation after loading
- Add helpful error messages with suggestions
- Add logging support
- Write comprehensive tests for all error conditions
- Update documentation with troubleshooting guide
- Add examples of error handling in documentation
Priority: High
Type: Bug / Enhancement
Component: Geometry Module
Estimated Effort: 4-6 hours
Labels: error-handling, user-experience, geometry
Assignee: @Aaryan-549