Skip to content

Add comprehensive error handling for geometry file loading #1662

@Aaryan-549

Description

@Aaryan-549

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:

  1. File not found: Generic FileNotFoundError without context
  2. Corrupted file format: Deep stack traces from parsing libraries
  3. Permission errors: System-level errors that don't explain what's needed
  4. Invalid file structure: Unclear which part of the file format is wrong
  5. 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 issue

Proposed 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

  1. Better user experience: Clear, actionable error messages
  2. Faster debugging: Users can identify issues immediately
  3. Reduced support burden: Fewer "file not loading" issues
  4. Helpful suggestions: Automatically suggests similar files or common fixes
  5. Proper error hierarchy: Custom exceptions allow targeted error handling
  6. Logging support: Integration with Python logging for debugging
  7. Validation: Catches invalid data early before it causes issues downstream

Implementation Plan

  1. Create custom exception classes (GeometryFileError, etc.)
  2. Add comprehensive error handling to file loading functions
  3. Add file validation (exists, readable, not empty, correct format)
  4. Add data validation after loading (physical constraints)
  5. Add helpful error messages with suggestions
  6. Add logging throughout the loading process
  7. Update documentation with common error scenarios
  8. Add tests for all error conditions

Files to Modify

  • torax/geometry.py - Add error handling to geometry loading
  • torax/geometry/__init__.py - Export custom exceptions
  • torax/geometry/chease.py - Add CHEASE-specific error handling
  • torax/geometry/fbt.py - Add FBT-specific error handling
  • torax/geometry/eqdsk.py - Add EQDSK-specific error handling
  • docs/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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions