LCOV - code coverage report
Current view: top level - colors/private - hermite_interp_bounded.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 95.8 % 48 46
Test Date: 2026-11-11 22:04:34 Functions: 100.0 % 3 3

            Line data    Source code
       1              : ! ***********************************************************************
       2              : !
       3              : !   Copyright (C) 2026  Niall Miller & The MESA Team
       4              : !
       5              : !   This program is free software: you can redistribute it and/or modify
       6              : !   it under the terms of the GNU Lesser General Public License
       7              : !   as published by the Free Software Foundation,
       8              : !   either version 3 of the License, or (at your option) any later version.
       9              : !
      10              : ! ***********************************************************************
      11              : 
      12              : module hermite_interp_bounded
      13              :    use const_def, only: dp
      14              :    use colors_def, only: Colors_General_Info
      15              :    use colors_utils, only: simpson_integration
      16              :    use hermite_interp, only: construct_sed_hermite
      17              :    use linear_interp, only: construct_sed_linear
      18              :    use utils_lib, only: is_inf, is_nan, mesa_error
      19              : 
      20              :    implicit none
      21              : 
      22              :    private
      23              :    public :: construct_sed_hermite_bounded
      24              : 
      25              :    ! Accept negligible Hermite undershoot when both its integrated magnitude
      26              :    ! and its peak amplitude are below 0.1% of the positive SED.
      27              :    real(dp), parameter :: negative_flux_fraction_tol = 1.0d-3
      28              : 
      29              : contains
      30              : 
      31            7 :    subroutine construct_sed_hermite_bounded(rq, teff, log_g, metallicity, R, d, &
      32              :                                             stellar_model_dir, wavelengths, fluxes)
      33              :       type(Colors_General_Info), intent(inout) :: rq
      34              :       real(dp), intent(in) :: teff, log_g, metallicity, R, d
      35              :       character(len=*), intent(in) :: stellar_model_dir
      36              :       real(dp), allocatable, intent(out) :: wavelengths(:), fluxes(:)
      37              : 
      38              :       real(dp) :: integrated_flux
      39              :       logical :: interp_ok
      40              : 
      41              :       call construct_sed_hermite(rq, teff, log_g, metallicity, R, d, &
      42            7 :                                  stellar_model_dir, wavelengths, fluxes)
      43              : 
      44              :       ! Accept Hermite whenever it is finite and physically usable.  Negligible
      45              :       ! negative undershoots are projected onto the physical flux bound.
      46            7 :       interp_ok = valid_sed(wavelengths, fluxes, .false.)
      47            7 :       if (interp_ok) call repair_negative_flux(wavelengths, fluxes, interp_ok)
      48              : 
      49            7 :       if (interp_ok) then
      50            6 :          call simpson_integration(wavelengths, fluxes, integrated_flux)
      51              :          interp_ok = .not. is_nan(integrated_flux) .and. &
      52            6 :                      .not. is_inf(integrated_flux) .and. integrated_flux > 0.0_dp
      53              :       end if
      54              : 
      55            7 :       if (interp_ok) return
      56              : 
      57              :       ! Hermite was unusable.  Fall back to the positivity-preserving linear
      58              :       ! interpolant so a Colors failure does not terminate the MESA run.
      59              :       call construct_sed_linear(rq, teff, log_g, metallicity, R, d, &
      60            1 :                                 stellar_model_dir, wavelengths, fluxes)
      61              : 
      62            1 :       interp_ok = valid_sed(wavelengths, fluxes, .true.)
      63            1 :       if (interp_ok) then
      64            1 :          call simpson_integration(wavelengths, fluxes, integrated_flux)
      65              :          interp_ok = .not. is_nan(integrated_flux) .and. &
      66            1 :                      .not. is_inf(integrated_flux) .and. integrated_flux > 0.0_dp
      67              :       end if
      68              : 
      69            1 :       if (.not. interp_ok) then
      70            0 :          write (*, *) 'colors: Hermite and linear interpolation returned invalid SEDs'
      71            0 :          call mesa_error(__FILE__, __LINE__)
      72              :       end if
      73              :    end subroutine construct_sed_hermite_bounded
      74              : 
      75            7 :    subroutine repair_negative_flux(wavelengths, fluxes, repair_ok)
      76              :       real(dp), intent(in) :: wavelengths(:)
      77              :       real(dp), intent(inout) :: fluxes(:)
      78              :       logical, intent(out) :: repair_ok
      79              : 
      80            7 :       real(dp), allocatable :: flux_part(:)
      81              :       real(dp) :: negative_integral, positive_integral
      82              :       real(dp) :: negative_peak, positive_peak
      83              :       integer :: n
      84              : 
      85            7 :       repair_ok = .false.
      86            7 :       n = size(fluxes)
      87              : 
      88            7 :       if (n < 2 .or. size(wavelengths) /= n) return
      89              : 
      90              :       ! Nothing needs repairing.  Zero flux is already physically safe.
      91         2411 :       if (.not. any(fluxes < 0.0_dp)) then
      92           12 :          repair_ok = any(fluxes > 0.0_dp)
      93            3 :          return
      94              :       end if
      95              : 
      96              :       ! A useful SED must retain a positive interior.
      97           62 :       if (.not. any(fluxes > 0.0_dp)) return
      98              : 
      99            9 :       allocate (flux_part(n))
     100              : 
     101         2407 :       flux_part = max(0.0_dp, -fluxes)
     102            3 :       call simpson_integration(wavelengths, flux_part, negative_integral)
     103         2404 :       negative_peak = maxval(flux_part)
     104              : 
     105         2407 :       flux_part = max(0.0_dp, fluxes)
     106            3 :       call simpson_integration(wavelengths, flux_part, positive_integral)
     107         2404 :       positive_peak = maxval(flux_part)
     108              : 
     109            3 :       if (is_nan(negative_integral) .or. is_inf(negative_integral)) return
     110            3 :       if (is_nan(positive_integral) .or. is_inf(positive_integral)) return
     111            3 :       if (is_nan(negative_peak) .or. is_inf(negative_peak)) return
     112            3 :       if (is_nan(positive_peak) .or. is_inf(positive_peak)) return
     113            3 :       if (positive_integral <= 0.0_dp .or. positive_peak <= 0.0_dp) return
     114              : 
     115            3 :       if (negative_integral/positive_integral > negative_flux_fraction_tol) return
     116            3 :       if (negative_peak/positive_peak > negative_flux_fraction_tol) return
     117              : 
     118              :       ! The undershoot is negligible; project only the unphysical samples onto
     119              :       ! the physical constraint f_lambda >= 0.
     120         2404 :       where (fluxes < 0.0_dp) fluxes = 0.0_dp
     121              : 
     122         2462 :       repair_ok = all(fluxes >= 0.0_dp) .and. any(fluxes > 0.0_dp)
     123            7 :    end subroutine repair_negative_flux
     124              : 
     125            8 :    logical function valid_sed(wavelengths, fluxes, require_positive_flux)
     126              :       real(dp), intent(in) :: wavelengths(:), fluxes(:)
     127              :       logical, intent(in) :: require_positive_flux
     128              : 
     129            8 :       valid_sed = size(wavelengths) >= 2 .and. size(wavelengths) == size(fluxes)
     130              :       if (.not. valid_sed) return
     131              : 
     132              :       valid_sed = .not. any(is_nan(wavelengths)) .and. .not. any(is_inf(wavelengths)) .and. &
     133        19264 :                   .not. any(is_nan(fluxes)) .and. .not. any(is_inf(fluxes))
     134              :       if (.not. valid_sed) return
     135              : 
     136              :       valid_sed = all(wavelengths > 0.0_dp) .and. &
     137         9616 :                   all(wavelengths(2:) > wavelengths(:size(wavelengths) - 1))
     138            8 :       if (.not. valid_sed) return
     139              : 
     140           11 :       if (require_positive_flux) valid_sed = all(fluxes > 0.0_dp)
     141              :    end function valid_sed
     142              : 
     143              : end module hermite_interp_bounded
        

Generated by: LCOV version 2.0-1