fortplot_coordinate_validation.f90 Source File


Source Code

module fortplot_coordinate_validation
    !! Input validation for coordinate arrays and edge case handling
    !! 
    !! This module provides comprehensive validation for coordinate data to prevent
    !! silent failures and ensure robust plotting across all backends.
    !! Addresses Issue #436 - single point plotting failures.
    
    use, intrinsic :: iso_fortran_env, only: wp => real64
    use, intrinsic :: ieee_arithmetic, only: ieee_is_finite, ieee_is_nan
    use fortplot_logging, only: log_error, log_warning, log_info
    implicit none
    
    private
    public :: validate_coordinate_arrays, coordinate_validation_result_t
    public :: is_valid_single_point, is_empty_array, has_machine_precision_issues
    public :: validate_coordinate_ranges, suggest_marker_for_single_point
    
    ! Validation result type
    type :: coordinate_validation_result_t
        logical :: is_valid = .false.
        logical :: is_single_point = .false.  
        logical :: is_empty = .false.
        logical :: has_precision_issues = .false.
        logical :: has_large_values = .false.
        logical :: should_use_markers = .false.
        character(len=256) :: message = ""
        character(len=32) :: suggested_marker = ""
    end type coordinate_validation_result_t
    
    ! Constants for validation
    real(wp), parameter :: LARGE_COORDINATE_THRESHOLD = 1.0e10_wp
    real(wp), parameter :: PRECISION_THRESHOLD = 100.0_wp * epsilon(1.0_wp)
    
contains

    function validate_coordinate_arrays(x, y, context) result(validation)
        !! Comprehensive validation of coordinate arrays for plotting
        !! Returns validation result with detailed information about the data
        real(wp), intent(in) :: x(:), y(:)
        character(len=*), intent(in), optional :: context
        type(coordinate_validation_result_t) :: validation
        
        character(len=64) :: ctx
        integer :: n
        
        ! Set context for error messages
        if (present(context)) then
            ctx = context
        else
            ctx = "coordinate validation"
        end if
        
        n = size(x)
        
        ! Initialize result
        validation%is_valid = .false.
        validation%is_single_point = .false.
        validation%is_empty = .false.
        validation%has_precision_issues = .false.
        validation%has_large_values = .false.
        validation%should_use_markers = .false.
        validation%message = ""
        validation%suggested_marker = ""
        
        ! Check array size consistency
        if (size(x) /= size(y)) then
            validation%message = trim(ctx) // ": x and y arrays must have the same size"
            call log_error(trim(validation%message))
            return
        end if
        
        ! Check for empty arrays
        if (n == 0) then
            validation%is_empty = .true.
            validation%is_valid = .true.  ! Empty is valid, just a special case
            validation%message = trim(ctx) // ": empty arrays provided"
            call log_info(trim(validation%message))
            return
        end if
        
        ! Check for single point
        if (n == 1) then
            validation%is_single_point = .true.
            validation%should_use_markers = .true.
            validation%suggested_marker = "o"  ! Circle marker for single points
        end if
        
        ! Validate individual coordinate values
        if (.not. validate_coordinate_ranges(x, y, validation)) then
            validation%message = trim(ctx) // ": invalid coordinate values detected"
            call log_error(trim(validation%message))
            return
        end if
        
        ! Check for machine precision issues (coordinates too close together)
        if (n > 1) then
            if (has_machine_precision_issues(x, y)) then
                validation%has_precision_issues = .true.
                validation%message = trim(ctx) // ": coordinates may be too close for reliable rendering"
                call log_warning(trim(validation%message))
            end if
        end if
        
        ! All validations passed
        validation%is_valid = .true.
        
        ! Generate informative message
        if (validation%is_single_point) then
            validation%message = trim(ctx) // ": single point detected - consider using markers"
        else if (validation%has_precision_issues) then
            validation%message = trim(ctx) // ": coordinates validated with precision warnings"
        else
            validation%message = trim(ctx) // ": coordinates validated successfully"
        end if
        
    end function validate_coordinate_arrays

    function validate_coordinate_ranges(x, y, validation) result(is_valid)
        !! Validate that coordinate values are finite and reasonable
        real(wp), intent(in) :: x(:), y(:)
        type(coordinate_validation_result_t), intent(inout) :: validation
        logical :: is_valid
        
        integer :: i
        real(wp) :: max_abs_x, max_abs_y
        
        is_valid = .true.
        max_abs_x = 0.0_wp
        max_abs_y = 0.0_wp
        
        ! Check each coordinate
        do i = 1, size(x)
            ! Check for NaN or infinite values
            if (.not. ieee_is_finite(x(i)) .or. .not. ieee_is_finite(y(i))) then
                call log_error("Invalid coordinate detected: NaN or infinite value")
                is_valid = .false.
                return
            end if
            
            ! Track maximum values for large coordinate detection
            max_abs_x = max(max_abs_x, abs(x(i)))
            max_abs_y = max(max_abs_y, abs(y(i)))
        end do
        
        ! Check for excessively large coordinates
        if (max_abs_x > LARGE_COORDINATE_THRESHOLD .or. max_abs_y > LARGE_COORDINATE_THRESHOLD) then
            validation%has_large_values = .true.
            call log_warning("Large coordinate values detected - may cause rendering issues")
        end if
        
    end function validate_coordinate_ranges

    function has_machine_precision_issues(x, y) result(has_issues)
        !! Check if coordinates are too close together for reliable rendering
        real(wp), intent(in) :: x(:), y(:)
        logical :: has_issues
        
        integer :: i
        real(wp) :: range_x, range_y, min_diff_x, min_diff_y
        
        has_issues = .false.
        
        if (size(x) < 2) return
        
        ! Calculate data ranges
        range_x = maxval(x) - minval(x)
        range_y = maxval(y) - minval(y)
        
        ! Find minimum differences between adjacent points
        min_diff_x = huge(1.0_wp)
        min_diff_y = huge(1.0_wp)
        
        do i = 2, size(x)
            min_diff_x = min(min_diff_x, abs(x(i) - x(i-1)))
            min_diff_y = min(min_diff_y, abs(y(i) - y(i-1)))
        end do
        
        ! Check if minimum differences are too small relative to range
        if (range_x > 0.0_wp .and. min_diff_x / range_x < PRECISION_THRESHOLD) then
            has_issues = .true.
        end if
        
        if (range_y > 0.0_wp .and. min_diff_y / range_y < PRECISION_THRESHOLD) then
            has_issues = .true.
        end if
        
    end function has_machine_precision_issues

    function is_valid_single_point(x, y) result(is_single)
        !! Check if arrays represent a valid single point
        real(wp), intent(in) :: x(:), y(:)
        logical :: is_single
        
        is_single = (size(x) == 1 .and. size(y) == 1 .and. &
                    ieee_is_finite(x(1)) .and. ieee_is_finite(y(1)))
                    
    end function is_valid_single_point

    function is_empty_array(x, y) result(is_empty)
        !! Check if arrays are empty
        real(wp), intent(in) :: x(:), y(:)
        logical :: is_empty
        
        is_empty = (size(x) == 0 .or. size(y) == 0)
        
    end function is_empty_array

    function suggest_marker_for_single_point(backend_type) result(marker)
        !! Suggest appropriate marker type for single point based on backend
        character(len=*), intent(in) :: backend_type
        character(len=8) :: marker
        
        select case (trim(backend_type))
        case ('ascii')
            marker = "*"      ! Asterisk works well in ASCII
        case ('png', 'pdf')
            marker = "o"      ! Circle for graphical backends
        case default
            marker = "o"      ! Default to circle
        end select
        
    end function suggest_marker_for_single_point

end module fortplot_coordinate_validation