LCOV - code coverage report
Current view: top level - colors/private - linear_interp.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 38.5 % 130 50
Test Date: 2026-11-11 22:04:34 Functions: 60.0 % 5 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              : !   This program is distributed in the hope that it will be useful,
      11              : !   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12              : !   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
      13              : !   See the GNU Lesser General Public License for more details.
      14              : !
      15              : !   You should have received a copy of the GNU Lesser General Public License
      16              : !   along with this program. If not, see <https://www.gnu.org/licenses/>.
      17              : !
      18              : ! ***********************************************************************
      19              : 
      20              : ! linear interpolation for SEDs
      21              : !
      22              : ! data-loading strategy selected by rq%cube_loaded:
      23              : !   .true.  -> use the preloaded 4-D flux cube on the handle
      24              : !   .false. -> load individual SED files via the lookup table (fallback)
      25              : 
      26              : module linear_interp
      27              :    use const_def, only: dp
      28              :    use colors_def, only: Colors_General_Info
      29              :    use colors_utils, only: dilute_flux, find_containing_cell, find_interval, &
      30              :                            find_nearest_point, find_bracket_index, &
      31              :                            load_sed_cached, load_stencil
      32              :    use utils_lib, only: is_inf, is_nan, mesa_error
      33              :    implicit none
      34              : 
      35              :    private
      36              :    public :: construct_sed_linear, trilinear_interp
      37              : 
      38              : contains
      39              : 
      40              :    ! main entry point -- construct a SED using trilinear interpolation
      41              :    ! strategy controlled by rq%cube_loaded (set at init)
      42            1 :    subroutine construct_sed_linear(rq, teff, log_g, metallicity, R, d, &
      43              :                                    stellar_model_dir, wavelengths, fluxes)
      44              :       type(Colors_General_Info), intent(inout) :: rq
      45              :       real(dp), intent(in) :: teff, log_g, metallicity, R, d
      46              :       character(len=*), intent(in) :: stellar_model_dir
      47              :       real(dp), dimension(:), allocatable, intent(out) :: wavelengths, fluxes
      48              : 
      49              :       integer :: n_lambda
      50            1 :       real(dp), dimension(:), allocatable :: interp_flux, diluted_flux
      51              : 
      52            1 :       if (rq%cube_loaded) then
      53              :          ! fast path: use preloaded cube from handle
      54            1 :          n_lambda = size(rq%cube_wavelengths)
      55              : 
      56            3 :          allocate (wavelengths(n_lambda))
      57            5 :          wavelengths = rq%cube_wavelengths
      58              : 
      59              :          ! vectorised interpolation -- cell location computed once, reused across n_lambda
      60            2 :          allocate (interp_flux(n_lambda))
      61              :          call trilinear_interp_vector(teff, log_g, metallicity, &
      62              :                                       rq%cube_teff_grid, rq%cube_logg_grid, &
      63              :                                       rq%cube_meta_grid, &
      64            1 :                                       rq%cube_flux, n_lambda, interp_flux)
      65              :       else
      66              :          ! fallback path: load individual SED files from lookup table
      67              :          call construct_sed_from_files(rq, teff, log_g, metallicity, &
      68            0 :                                        stellar_model_dir, interp_flux, wavelengths)
      69            0 :          n_lambda = size(wavelengths)
      70              :       end if
      71              : 
      72            3 :       allocate (diluted_flux(n_lambda))
      73            1 :       call dilute_flux(interp_flux, R, d, diluted_flux)
      74            6 :       fluxes = diluted_flux
      75              : 
      76            1 :    end subroutine construct_sed_linear
      77              : 
      78              :    ! fallback: build a 2x2x2 sub-cube from SED files, then trilinear-interpolate
      79              :    ! unlike hermite, no derivative context needed -- stencil is exactly the 2x2x2 cell corners
      80            0 :    subroutine construct_sed_from_files(rq, teff, log_g, metallicity, &
      81              :                                        stellar_model_dir, interp_flux, wavelengths)
      82              :       use colors_utils, only: resolve_path, build_grid_to_lu_map
      83              :       type(Colors_General_Info), intent(inout) :: rq
      84              :       real(dp), intent(in) :: teff, log_g, metallicity
      85              :       character(len=*), intent(in) :: stellar_model_dir
      86              :       real(dp), dimension(:), allocatable, intent(out) :: interp_flux, wavelengths
      87              : 
      88              :       integer :: i_t, i_g, i_m   ! bracketing indices in unique grids
      89              :       integer :: lo_t, hi_t, lo_g, hi_g, lo_m, hi_m   ! stencil bounds
      90              :       integer :: nt, ng, nm, n_lambda
      91              :       character(len=512) :: resolved_dir
      92              :       logical :: need_reload
      93              : 
      94            0 :       resolved_dir = trim(resolve_path(stellar_model_dir))
      95              : 
      96              :       ! ensure the grid-to-lu mapping exists (built once, then reused)
      97            0 :       if (.not. rq%grid_map_built) call build_grid_to_lu_map(rq)
      98              : 
      99              :       ! find bracketing cell in the unique grids
     100            0 :       call find_bracket_index(rq%u_teff, teff, i_t)
     101            0 :       call find_bracket_index(rq%u_logg, log_g, i_g)
     102            0 :       call find_bracket_index(rq%u_meta, metallicity, i_m)
     103              : 
     104              :       ! check if the stencil cache is still valid for this cell
     105            0 :       need_reload = .true.
     106              :       if (rq%stencil_valid .and. &
     107              :           i_t == rq%stencil_i_t .and. &
     108            0 :           i_g == rq%stencil_i_g .and. &
     109              :           i_m == rq%stencil_i_m) then
     110              :          need_reload = .false.
     111              :       end if
     112              : 
     113              :       if (need_reload) then
     114              :          ! trilinear needs exactly the 2x2x2 cell corners -- no extension
     115            0 :          nt = size(rq%u_teff)
     116            0 :          ng = size(rq%u_logg)
     117            0 :          nm = size(rq%u_meta)
     118              : 
     119            0 :          if (nt < 2) then
     120            0 :             lo_t = 1; hi_t = 1
     121              :          else
     122            0 :             lo_t = i_t
     123            0 :             hi_t = min(nt, i_t + 1)
     124              :          end if
     125              : 
     126            0 :          if (ng < 2) then
     127            0 :             lo_g = 1; hi_g = 1
     128              :          else
     129            0 :             lo_g = i_g
     130            0 :             hi_g = min(ng, i_g + 1)
     131              :          end if
     132              : 
     133            0 :          if (nm < 2) then
     134            0 :             lo_m = 1; hi_m = 1
     135              :          else
     136            0 :             lo_m = i_m
     137            0 :             hi_m = min(nm, i_m + 1)
     138              :          end if
     139              : 
     140              :          ! load SEDs for every stencil point (using memory cache)
     141            0 :          call load_stencil(rq, resolved_dir, lo_t, hi_t, lo_g, hi_g, lo_m, hi_m)
     142              : 
     143              :          ! store subgrid arrays on the handle
     144            0 :          if (allocated(rq%stencil_teff)) deallocate (rq%stencil_teff)
     145            0 :          if (allocated(rq%stencil_logg)) deallocate (rq%stencil_logg)
     146            0 :          if (allocated(rq%stencil_meta)) deallocate (rq%stencil_meta)
     147              : 
     148            0 :          allocate (rq%stencil_teff(hi_t - lo_t + 1))
     149            0 :          allocate (rq%stencil_logg(hi_g - lo_g + 1))
     150            0 :          allocate (rq%stencil_meta(hi_m - lo_m + 1))
     151            0 :          rq%stencil_teff = rq%u_teff(lo_t:hi_t)
     152            0 :          rq%stencil_logg = rq%u_logg(lo_g:hi_g)
     153            0 :          rq%stencil_meta = rq%u_meta(lo_m:hi_m)
     154              : 
     155            0 :          rq%stencil_i_t = i_t
     156            0 :          rq%stencil_i_g = i_g
     157            0 :          rq%stencil_i_m = i_m
     158            0 :          rq%stencil_valid = .true.
     159              :       end if
     160              : 
     161            0 :       n_lambda = size(rq%stencil_wavelengths)
     162            0 :       allocate (wavelengths(n_lambda))
     163            0 :       wavelengths = rq%stencil_wavelengths
     164              : 
     165            0 :       allocate (interp_flux(n_lambda))
     166              :       call trilinear_interp_vector(teff, log_g, metallicity, &
     167              :                                    rq%stencil_teff, rq%stencil_logg, rq%stencil_meta, &
     168            0 :                                    rq%stencil_fluxes, n_lambda, interp_flux)
     169              : 
     170            0 :    end subroutine construct_sed_from_files
     171              : 
     172              :    ! vectorised trilinear interpolation over all wavelengths
     173              :    ! cell location depends only on (teff, logg, meta) -- computed once, reused across n_lambda
     174            1 :    subroutine trilinear_interp_vector(x_val, y_val, z_val, &
     175            1 :                                       x_grid, y_grid, z_grid, &
     176            1 :                                       f_values_4d, n_lambda, result_flux)
     177              :       real(dp), intent(in) :: x_val, y_val, z_val
     178              :       real(dp), intent(in) :: x_grid(:), y_grid(:), z_grid(:)
     179              :       real(dp), intent(in) :: f_values_4d(:, :, :, :)   ! (nx, ny, nz, n_lambda)
     180              :       integer, intent(in) :: n_lambda
     181              :       real(dp), intent(out) :: result_flux(n_lambda)
     182              : 
     183              :       integer :: i_x, i_y, i_z, nearest_x, nearest_y, nearest_z, lam
     184              :       integer :: nx, ny, nz
     185              :       integer :: ix_max, iy_max, iz_max
     186              :       integer :: ox, oy, oz
     187              :       real(dp) :: t_x, t_y, t_z
     188              :       real(dp) :: w_x(0:1), w_y(0:1), w_z(0:1)
     189              :       real(dp) :: accum, weight
     190              :       real(dp), parameter :: tiny_value = 1.0d-300
     191              : 
     192            1 :       nx = size(x_grid)
     193            1 :       ny = size(y_grid)
     194            1 :       nz = size(z_grid)
     195              : 
     196              :       if (nx < 1 .or. ny < 1 .or. nz < 1 .or. n_lambda < 1 .or. &
     197              :           size(f_values_4d, 1) /= nx .or. size(f_values_4d, 2) /= ny .or. &
     198            1 :           size(f_values_4d, 3) /= nz .or. size(f_values_4d, 4) /= n_lambda) then
     199            0 :          write (*, *) 'colors: invalid array sizes in trilinear_interp_vector'
     200            0 :          call mesa_error(__FILE__, __LINE__)
     201              :       end if
     202              : 
     203              :       call find_containing_cell(x_val, y_val, z_val, x_grid, y_grid, z_grid, &
     204            1 :                                 i_x, i_y, i_z, t_x, t_y, t_z)
     205              : 
     206            1 :       call setup_axis(nx, x_val, x_grid, i_x, t_x, ix_max, w_x)
     207            1 :       call setup_axis(ny, y_val, y_grid, i_y, t_y, iy_max, w_y)
     208            1 :       call setup_axis(nz, z_val, z_grid, i_z, t_z, iz_max, w_z)
     209              : 
     210            4 :       do lam = 1, n_lambda
     211            3 :          accum = 0.0_dp
     212              : 
     213            6 :          do oz = 0, iz_max
     214            9 :             do oy = 0, iy_max
     215           12 :                do ox = 0, ix_max
     216            6 :                   weight = w_x(ox)*w_y(oy)*w_z(oz)
     217            9 :                   accum = accum + weight*f_values_4d(i_x + ox, i_y + oy, i_z + oz, lam)
     218              :                end do
     219              :             end do
     220              :          end do
     221              : 
     222            3 :          if (is_nan(accum) .or. is_inf(accum) .or. accum <= 0.0_dp) then
     223              :             call find_nearest_point(x_val, y_val, z_val, x_grid, y_grid, z_grid, &
     224            0 :                                     nearest_x, nearest_y, nearest_z)
     225            0 :             accum = f_values_4d(nearest_x, nearest_y, nearest_z, lam)
     226              :          end if
     227              : 
     228            3 :          if (is_nan(accum) .or. is_inf(accum) .or. accum < 0.0_dp) then
     229            0 :             write (*, *) 'colors: linear and nearest-grid interpolation returned bad flux'
     230            0 :             call mesa_error(__FILE__, __LINE__)
     231              :          end if
     232            4 :          result_flux(lam) = max(tiny_value, accum)
     233              :       end do
     234              : 
     235            1 :    end subroutine trilinear_interp_vector
     236              : 
     237              :    ! scalar trilinear interpolation (external callers / single-wavelength use)
     238              :    ! retained for backward compatibility
     239            0 :    function trilinear_interp(x_val, y_val, z_val, x_grid, y_grid, z_grid, f_values) result(f_interp)
     240              :       real(dp), intent(in) :: x_val, y_val, z_val
     241              :       real(dp), intent(in) :: x_grid(:), y_grid(:), z_grid(:)
     242              :       real(dp), intent(in) :: f_values(:, :, :)
     243              :       real(dp) :: f_interp
     244              : 
     245              :       integer :: i_x, i_y, i_z
     246              :       integer :: nx, ny, nz
     247              :       integer :: ix_max, iy_max, iz_max
     248              :       integer :: ox, oy, oz
     249              :       real(dp) :: t_x, t_y, t_z
     250              :       real(dp) :: w_x(0:1), w_y(0:1), w_z(0:1)
     251              :       real(dp) :: accum, weight
     252              :       real(dp), parameter :: tiny_value = 1.0d-300
     253              : 
     254            0 :       nx = size(x_grid)
     255            0 :       ny = size(y_grid)
     256            0 :       nz = size(z_grid)
     257              : 
     258              :       if (nx < 1 .or. ny < 1 .or. nz < 1 .or. &
     259            0 :           size(f_values, 1) /= nx .or. size(f_values, 2) /= ny .or. &
     260              :           size(f_values, 3) /= nz) then
     261            0 :          write (*, *) 'colors: invalid array sizes in trilinear_interp'
     262            0 :          call mesa_error(__FILE__, __LINE__)
     263              :       end if
     264              : 
     265              :       call find_containing_cell(x_val, y_val, z_val, x_grid, y_grid, z_grid, &
     266            0 :                                 i_x, i_y, i_z, t_x, t_y, t_z)
     267              : 
     268            0 :       call setup_axis(nx, x_val, x_grid, i_x, t_x, ix_max, w_x)
     269            0 :       call setup_axis(ny, y_val, y_grid, i_y, t_y, iy_max, w_y)
     270            0 :       call setup_axis(nz, z_val, z_grid, i_z, t_z, iz_max, w_z)
     271              : 
     272            0 :       accum = 0.0_dp
     273              : 
     274            0 :       do oz = 0, iz_max
     275            0 :          do oy = 0, iy_max
     276            0 :             do ox = 0, ix_max
     277            0 :                weight = w_x(ox)*w_y(oy)*w_z(oz)
     278            0 :                accum = accum + weight*f_values(i_x + ox, i_y + oy, i_z + oz)
     279              :             end do
     280              :          end do
     281              :       end do
     282              : 
     283            0 :       if (is_nan(accum) .or. is_inf(accum) .or. accum <= 0.0_dp) then
     284              :          call find_nearest_point(x_val, y_val, z_val, x_grid, y_grid, z_grid, &
     285            0 :                                  i_x, i_y, i_z)
     286            0 :          accum = f_values(i_x, i_y, i_z)
     287              :       end if
     288              : 
     289            0 :       if (is_nan(accum) .or. is_inf(accum) .or. accum < 0.0_dp) then
     290            0 :          write (*, *) 'colors: linear and nearest-grid interpolation returned bad flux'
     291            0 :          call mesa_error(__FILE__, __LINE__)
     292              :       end if
     293            0 :       f_interp = max(tiny_value, accum)
     294              : 
     295            0 :    end function trilinear_interp
     296              : 
     297            3 :    subroutine setup_axis(n, x_val, grid, i, t, i_max, w)
     298              :       integer, intent(in) :: n
     299              :       real(dp), intent(in) :: x_val
     300              :       real(dp), intent(in) :: grid(:)
     301              :       integer, intent(inout) :: i
     302              :       real(dp), intent(inout) :: t
     303              :       integer, intent(out) :: i_max
     304              :       real(dp), intent(out) :: w(0:1)
     305              : 
     306            3 :       if (n <= 1) then
     307            2 :          i = 1
     308            2 :          t = 0.0_dp
     309            2 :          i_max = 0
     310            2 :          w(0) = 1.0_dp
     311            2 :          w(1) = 0.0_dp
     312            2 :          return
     313              :       end if
     314              : 
     315            1 :       if (x_val <= grid(1)) then
     316            0 :          i = 1
     317            0 :          t = 0.0_dp
     318            1 :       else if (x_val >= grid(n)) then
     319            0 :          i = n - 1
     320            0 :          t = 1.0_dp
     321              :       else
     322            1 :          if (i < 1) i = 1
     323            1 :          if (i >= n) i = n - 1
     324            1 :          t = max(0.0_dp, min(1.0_dp, t))
     325              :       end if
     326              : 
     327            1 :       i_max = 1
     328            1 :       w(0) = 1.0_dp - t
     329            1 :       w(1) = t
     330              :    end subroutine setup_axis
     331              : 
     332              : end module linear_interp
        

Generated by: LCOV version 2.0-1