!> A Fortran 90 module for creating 1D and 2D lookup tables. These tables can be !> used to efficiently interpolate one or more values. !> !> Author: Jannis Teunissen module mod_lookup_table implicit none private ! The precision of the real numbers used in the tables integer, parameter :: dp = kind(1.0d0) ! ** Routines for finding indices in sorted lists ** public :: find_index_linear public :: find_index_bsearch public :: find_index_adaptive !> The lookup table type. There can be one or more columns, for which values !> can be looked up for a given 'x-coordinate'. type LT_t integer :: n_points !< The number of points integer :: n_cols !< The number of columns real(dp) :: x_min !< The minimum lookup coordinate real(dp) :: dx !< The x-spacing in the lookup coordinate real(dp) :: inv_dx !< The inverse x-spacing ! The table is stored in two ways, to speed up different types of lookups. real(dp), allocatable :: cols_rows(:, :) !< The table in column-major order real(dp), allocatable :: rows_cols(:, :) !< The table in row-major order end type LT_t !> The 2D lookup table type type LT2_t integer :: n_points(2) !< The size of the table integer :: n_cols !< The number of columns/variables real(dp) :: x_min(2) !< The minimum lookup coordinate real(dp) :: dx(2) !< The x-spacing in the lookup coordinate real(dp) :: inv_dx(2) !< The inverse x-spacing real(dp), allocatable :: rows_cols(:, :, :) end type LT2_t !> The 3D lookup table type type LT3_t integer :: n_points(3) !< The size of the table integer :: n_cols !< The number of columns/variables real(dp) :: x_min(3) !< The minimum lookup coordinate real(dp) :: dx(3) !< The x-spacing in the lookup coordinate real(dp) :: inv_dx(3) !< The inverse x-spacing real(dp), allocatable :: rows_cols(:, :, :, :) end type LT3_t !> Type to indicate a location in the lookup table, which can be used to speed !> up multiple lookups of different columns. type LT_loc_t private integer :: low_ix !< The x-value lies between low_ix and low_ix+1 real(dp) :: low_frac !< The distance from low_ix (up to low_ix+1), given !< as a real number between 0 and 1. end type LT_loc_t !> Type to indicate a location in a 2D lookup table type LT2_loc_t private !> The x-value lies between low_ix and low_ix+1 integer :: low_ix(2) !> The distance from low_ix (up to low_ix+1), given as a real number !> between 0 and 1. real(dp) :: low_frac(2) end type LT2_loc_t !> Type to indicate a location in a 3D lookup table type LT3_loc_t private !> The x-value lies between low_ix and low_ix+1 integer :: low_ix(3) !> The distance from low_ix (up to low_ix+1), given as a real number !> between 0 and 1. real(dp) :: low_frac(3) end type LT3_loc_t ! Public types public :: LT_t public :: LT_loc_t public :: LT2_t public :: LT2_loc_t public :: LT3_t public :: LT3_loc_t ! Public methods public :: LT_create ! Create a new lookup table public :: LT_create_from_data ! Create a new lookup table from existing data public :: LT_get_xdata ! Get the x-values of a table public :: LT_get_spaced_data ! Convert values to regularly spaced public :: LT_set_col ! Set one table column public :: LT_add_col ! Add a column public :: LT_get_loc ! Get the index (row) of a value public :: LT_get_col ! Interpolate one column public :: LT_get_mcol ! Interpolate multiple columns public :: LT_get_col_at_loc ! Get one column at location public :: LT_get_mcol_at_loc ! Get multiple columns at location public :: LT_get_data ! Get all the data of the table public :: LT_lin_interp_list ! Linearly interpolate a list public :: LT_to_file ! Store lookup table in file public :: LT_from_file ! Restore lookup table from file ! Public methods public :: LT2_create ! Create a new lookup table public :: LT2_create_from_data !Create a new lookup table from existing data public :: LT2_set_col ! Set one table column public :: LT2_get_loc ! Get the index (row) of a value public :: LT2_get_col ! Interpolate one column public :: LT2_get_col_at_loc ! Get one column at location public :: LT3_create ! Create a new lookup table public :: LT3_create_from_data !Create a new lookup table from existing data public :: LT3_get_loc ! Get the index (row) of a value public :: LT3_get_col ! Interpolate one column public :: LT3_get_col_at_loc ! Get one column at location contains ! ** Routines for finding indices ** !> Linear search of sorted list for the smallest ix such that list(ix) >= val. !> On failure, returns size(list)+1 pure function find_index_linear(list, val) result(ix) real(dp), intent(in) :: list(:) !< Sorted list real(dp), intent(in) :: val !< Value to search for integer :: ix do ix = 1, size(list) if (list(ix) >= val) exit end do end function find_index_linear !> Binary search of sorted list for the smallest ix such that list(ix) >= val. !> On failure, returns size(list)+1 pure function find_index_bsearch(list, val) result(ix) real(dp), intent(in) :: list(:) !< Sorted list real(dp), intent(in) :: val !< Value to search for integer :: ix, i_min, i_max, i_middle i_min = 1 i_max = size(list) do while (i_min < i_max) ! This safely performs: i_middle = (i_max + i_min) / 2 i_middle = i_min + ishft(i_max - i_min, -1) if (list(i_middle) >= val) then i_max = i_middle else i_min = i_middle + 1 end if end do ix = i_min if (val > list(ix)) ix = size(list) + 1 end function find_index_bsearch !> Adaptive search (combination of linear and binary search) of sorted list !> for the smallest ix such that list(ix) >= val. On failure, returns !> size(list)+1 pure function find_index_adaptive(list, val) result(ix) real(dp), intent(in) :: list(:) !< Sorted list real(dp), intent(in) :: val !< Value to search for integer :: ix integer, parameter :: binary_search_limit = 40 if (size(list) < binary_search_limit) then ix = find_index_linear(list, val) else ix = find_index_bsearch(list, val) end if end function find_index_adaptive ! ** 1D lookup table routines ** !> This function returns a new lookup table function LT_create(x_min, x_max, n_points, n_cols) result(my_lt) real(dp), intent(in) :: x_min !< Minimum x-coordinate real(dp), intent(in) :: x_max !< Maximum x-coordinate integer, intent(in) :: n_points !< How many x-values to store integer, intent(in) :: n_cols !< Number of variables that will be looked up type(LT_t) :: my_lt if (x_max <= x_min) stop "set_xdata: x_max should be > x_min" if (n_points <= 1) stop "set_xdata: n_points should be bigger than 1" my_lt%n_points = n_points my_lt%x_min = x_min my_lt%dx = (x_max - x_min) / (n_points - 1) my_lt%inv_dx = 1 / my_lt%dx allocate(my_lt%cols_rows(n_cols, n_points)) allocate(my_lt%rows_cols(n_points, n_cols)) my_lt%cols_rows = 0 my_lt%rows_cols = 0 my_lt%n_cols = n_cols end function LT_create !> This function returns a new lookup table from regularly spaced data function LT_create_from_data(x_min, x_max, spaced_data) result(my_lt) real(dp), intent(in) :: x_min !< Minimum coordinate real(dp), intent(in) :: x_max !< Maximum coordinate real(dp), intent(in) :: spaced_data(:, :) !< Input data of shape (n_points, n_cols) integer :: n_points, n_cols type(LT_t) :: my_lt n_points = size(spaced_data, 1) n_cols = size(spaced_data, 2) if (x_max <= x_min) stop "LT_create error: x_max <= x_min" if (n_points <= 1) stop "LT_create error: n_points <= 1" my_lt%n_cols = n_cols my_lt%n_points = n_points my_lt%x_min = x_min my_lt%dx = (x_max - x_min) / (n_points - 1) my_lt%inv_dx = 1 / my_lt%dx my_lt%rows_cols = spaced_data my_lt%cols_rows = transpose(spaced_data) end function LT_create_from_data !> Returns the x-coordinates of the lookup table pure function LT_get_xdata(x_min, dx, n_points) result(xdata) real(dp), intent(in) :: x_min, dx integer, intent(in) :: n_points real(dp) :: xdata(n_points) integer :: ix do ix = 1, n_points xdata(ix) = x_min + (ix-1) * dx end do end function LT_get_xdata !> Linearly interpolate the (x, y) input data to the new_x coordinates pure function LT_get_spaced_data(in_x, in_y, new_x) result(out_yy) real(dp), intent(in) :: in_x(:), in_y(:), new_x(:) real(dp) :: out_yy(size(new_x)) integer :: ix do ix = 1, size(new_x) call LT_lin_interp_list(in_x, in_y, new_x(ix), out_yy(ix)) end do end function LT_get_spaced_data !> Fill the column with index col_ix using the linearly interpolated (x, y) !> data pure subroutine LT_set_col(my_lt, col_ix, x, y) type(LT_t), intent(inout) :: my_lt integer, intent(in) :: col_ix real(dp), intent(in) :: x(:), y(:) my_lt%cols_rows(col_ix, :) = LT_get_spaced_data(x, y,& LT_get_xdata(my_lt%x_min, my_lt%dx, my_lt%n_points)) my_lt%rows_cols(:, col_ix) = my_lt%cols_rows(col_ix, :) end subroutine LT_set_col !> Add a new column by linearly interpolating the (x, y) data pure subroutine LT_add_col(my_lt, x, y) type(LT_t), intent(inout) :: my_lt real(dp), intent(in) :: x(:), y(:) type(LT_t) :: temp_lt temp_lt = my_lt deallocate(my_lt%cols_rows) deallocate(my_lt%rows_cols) allocate(my_lt%cols_rows(my_lt%n_cols+1, my_lt%n_points)) allocate(my_lt%rows_cols(my_lt%n_points, my_lt%n_cols+1)) my_lt%cols_rows(1:my_lt%n_cols, :) = temp_lt%cols_rows my_lt%rows_cols(:, 1:my_lt%n_cols) = temp_lt%rows_cols my_lt%n_cols = my_lt%n_cols + 1 my_lt%cols_rows(my_lt%n_cols, :) = LT_get_spaced_data(x, y,& LT_get_xdata(my_lt%x_min, my_lt%dx, my_lt%n_points)) my_lt%rows_cols(:, my_lt%n_cols) = my_lt%cols_rows(my_lt%n_cols, :) end subroutine LT_add_col !> Get a location in the lookup table elemental function LT_get_loc(my_lt, x) result(my_loc) type(LT_t), intent(in) :: my_lt real(dp), intent(in) :: x type(LT_loc_t) :: my_loc real(dp) :: frac frac = (x - my_lt%x_min) * my_lt%inv_dx my_loc%low_ix = ceiling(frac) my_loc%low_frac = my_loc%low_ix - frac ! Check bounds if (my_loc%low_ix < 1) then my_loc%low_ix = 1 my_loc%low_frac = 1 else if (my_loc%low_ix >= my_lt%n_points) then my_loc%low_ix = my_lt%n_points - 1 my_loc%low_frac = 0 end if end function LT_get_loc !> Get the values of all columns at x pure function LT_get_mcol(my_lt, x) result(col_values) type(LT_t), intent(in) :: my_lt real(dp), intent(in) :: x real(dp) :: col_values(my_lt%n_cols) type(LT_loc_t) :: loc loc = LT_get_loc(my_lt, x) col_values = LT_get_mcol_at_loc(my_lt, loc) end function LT_get_mcol !> Get the value of a single column at x elemental function LT_get_col(my_lt, col_ix, x) result(col_value) type(LT_t), intent(in) :: my_lt integer, intent(in) :: col_ix real(dp), intent(in) :: x real(dp) :: col_value type(LT_loc_t) :: loc loc = LT_get_loc(my_lt, x) col_value = LT_get_col_at_loc(my_lt, col_ix, loc) end function LT_get_col !> Get the values of all columns at a location pure function LT_get_mcol_at_loc(my_lt, loc) result(col_values) type(LT_t), intent(in) :: my_lt type(LT_loc_t), intent(in) :: loc real(dp) :: col_values(my_lt%n_cols) col_values = loc%low_frac * my_lt%cols_rows(:,& loc%low_ix) + (1-loc%low_frac) * my_lt%cols_rows(:, loc%low_ix+1) end function LT_get_mcol_at_loc !> Get the value of a single column at a location elemental function LT_get_col_at_loc(my_lt, col_ix, loc) result(col_value) type(LT_t), intent(in) :: my_lt integer, intent(in) :: col_ix type(LT_loc_t), intent(in) :: loc real(dp) :: col_value col_value = loc%low_frac * my_lt%rows_cols(loc%low_ix,& col_ix) + (1-loc%low_frac) * my_lt%rows_cols(loc%low_ix+1, col_ix) end function LT_get_col_at_loc !> Get the x-coordinates and the columns of the lookup table pure subroutine LT_get_data(my_lt, x_data, cols_rows) type(LT_t), intent(in) :: my_lt real(dp), intent(out) :: x_data(:), cols_rows(:, :) x_data = LT_get_xdata(my_lt%x_min, my_lt%dx, my_lt%n_points) cols_rows = my_lt%cols_rows end subroutine LT_get_data !> Compute by use of linear interpolation the value in the middle ! of a domain D = [x_list(1) , x_list(size(x_list))]. ! If x_value is left of domain D, ! then the value becomes the value at the left side of D, ! if x_value is right of domain D, ! then the value becomes the value at the rigth side of D pure subroutine LT_lin_interp_list(x_list, y_list, x_value, y_value) real(dp), intent(in) :: x_list(:), y_list(:) real(dp), intent(in) :: x_value real(dp), intent(out) :: y_value integer :: ix, iMin, iMax real(dp) :: temp iMin = 1 iMax = size(x_list) if (x_value <= x_list(iMin)) then y_value = y_list(iMin) else if (x_value >= x_list(iMax)) then y_value = y_list(iMax) else ix = find_index_adaptive(x_list, x_value) temp = (x_value - x_list(ix-1)) / (x_list(ix) - x_list(ix-1)) y_value = (1 - temp) * y_list(ix-1) + temp * y_list(ix) end if end subroutine LT_lin_interp_list !> Write the lookup table to file (in binary, potentially unportable) subroutine LT_to_file(my_lt, filename) type(LT_t), intent(in) :: my_lt character(len=*), intent(in) :: filename integer :: my_unit open(newunit=my_unit, file=trim(filename), form='UNFORMATTED',& access='STREAM', status='REPLACE') write(my_unit) my_lt%n_points, my_lt%n_cols write(my_unit) my_lt%x_min, my_lt%dx, my_lt%inv_dx write(my_unit) my_lt%cols_rows close(my_unit) end subroutine LT_to_file !> Read the lookup table from file (in binary, potentially unportable) subroutine LT_from_file(my_lt, filename) type(LT_t), intent(inout) :: my_lt character(len=*), intent(in) :: filename integer :: my_unit open(newunit=my_unit, file=trim(filename), form='UNFORMATTED',& access='STREAM', status='OLD') read(my_unit) my_lt%n_points, my_lt%n_cols read(my_unit) my_lt%x_min, my_lt%dx, my_lt%inv_dx allocate(my_lt%cols_rows(my_lt%n_cols, my_lt%n_points)) allocate(my_lt%rows_cols(my_lt%n_points, my_lt%n_cols)) read(my_unit) my_lt%cols_rows my_lt%rows_cols = transpose(my_lt%cols_rows) close(my_unit) end subroutine LT_from_file ! ** 2D lookup table routines ** !> This function returns a new lookup table function LT2_create(x_min, x_max, n_points, n_cols) result(my_lt) real(dp), intent(in) :: x_min(2) !< Minimum coordinate real(dp), intent(in) :: x_max(2) !< Maximum coordinate integer, intent(in) :: n_points(2) !< How many values to store integer, intent(in) :: n_cols !< Number of variables that will be looked up type(LT2_t) :: my_lt if (any(x_max <= x_min)) stop "LT2_create error: x_max <= x_min" if (any(n_points <= 1)) stop "LT2_create error: n_points <= 1" my_lt%n_points = n_points my_lt%x_min = x_min my_lt%dx = (x_max - x_min) / (n_points - 1) my_lt%inv_dx = 1 / my_lt%dx allocate(my_lt%rows_cols(n_points(1), n_points(2), n_cols)) my_lt%rows_cols = 0 my_lt%n_cols = n_cols end function LT2_create !> This function returns a new lookup table from regularly spaced data function LT2_create_from_data(x_min, x_max, spaced_data) result(my_lt) real(dp), intent(in) :: x_min(2) !< Minimum coordinate real(dp), intent(in) :: x_max(2) !< Maximum coordinate real(dp), intent(in) :: spaced_data(:, :, :) !< Input data of shape (nx, ny, n_cols) integer :: n_points(2), n_cols type(LT2_t) :: my_lt n_points(1) = size(spaced_data, 1) n_points(2) = size(spaced_data, 2) n_cols = size(spaced_data, 3) if (any(x_max <= x_min)) stop "LT2_create error: x_max <= x_min" if (any(n_points <= 1)) stop "LT2_create error: n_points <= 1" my_lt%n_cols = n_cols my_lt%n_points = n_points my_lt%x_min = x_min my_lt%dx = (x_max - x_min) / (n_points - 1) my_lt%inv_dx = 1 / my_lt%dx my_lt%rows_cols = spaced_data end function LT2_create_from_data !> Fill the column with index col_ix using linearly interpolated data pure subroutine LT2_set_col(my_lt, col_ix, x1, x2, y) type(LT2_t), intent(inout) :: my_lt integer, intent(in) :: col_ix real(dp), intent(in) :: x1(:), x2(:), y(:, :) real(dp), allocatable :: tmp(:, :), c1(:), c2(:) integer :: ix allocate(c1(my_lt%n_points(1)),c2(my_lt%n_points(2))) c1 = LT_get_xdata(my_lt%x_min(1), my_lt%dx(1), my_lt%n_points(1)) c2 = LT_get_xdata(my_lt%x_min(2), my_lt%dx(2), my_lt%n_points(2)) allocate(tmp(my_lt%n_points(1), size(x2))) ! Interpolate along first coordinate do ix = 1, size(x2) tmp(:, ix) = LT_get_spaced_data(x1, y(:, ix), c1) end do ! Interpolate along second coordinate do ix = 1, my_lt%n_points(1) my_lt%rows_cols(ix, :, col_ix) = LT_get_spaced_data(x2, tmp(ix, :), c2) end do deallocate(c1,c2) end subroutine LT2_set_col !> Get a location in the lookup table elemental function LT2_get_loc(my_lt, x1, x2) result(my_loc) type(LT2_t), intent(in) :: my_lt real(dp), intent(in) :: x1, x2 type(LT2_loc_t) :: my_loc real(dp) :: frac(2) frac = ([x1, x2] - my_lt%x_min) * my_lt%inv_dx my_loc%low_ix = ceiling(frac) my_loc%low_frac = my_loc%low_ix - frac ! Check bounds where (my_loc%low_ix < 1) my_loc%low_ix = 1 my_loc%low_frac = 1 end where where (my_loc%low_ix >= my_lt%n_points) my_loc%low_ix = my_lt%n_points - 1 my_loc%low_frac = 0 end where end function LT2_get_loc !> Get the value of a single column at x elemental function LT2_get_col(my_lt, col_ix, x1, x2) result(col_value) type(LT2_t), intent(in) :: my_lt integer, intent(in) :: col_ix real(dp), intent(in) :: x1, x2 real(dp) :: col_value type(LT2_loc_t) :: loc loc = LT2_get_loc(my_lt, x1, x2) col_value = LT2_get_col_at_loc(my_lt, col_ix, loc) end function LT2_get_col !> Get the value of a single column at a location elemental function LT2_get_col_at_loc(my_lt, col_ix, loc) result(col_value) type(LT2_t), intent(in) :: my_lt integer, intent(in) :: col_ix type(LT2_loc_t), intent(in) :: loc integer :: ix(2) real(dp) :: w(2, 2) real(dp) :: col_value ! Bilinear interpolation w(1, 1) = loc%low_frac(1) * loc%low_frac(2) w(2, 1) = (1 - loc%low_frac(1)) * loc%low_frac(2) w(1, 2) = loc%low_frac(1) * (1 - loc%low_frac(2)) w(2, 2) = (1 - loc%low_frac(1)) * (1 - loc%low_frac(2)) ix = loc%low_ix col_value = sum(w * my_lt%rows_cols(ix(1):ix(1)+1, ix(2):ix(2)+1, col_ix)) end function LT2_get_col_at_loc ! ** 3D lookup table routines ** !> This function returns a new lookup table function LT3_create(x_min, x_max, n_points, n_cols) result(my_lt) real(dp), intent(in) :: x_min(3) !< Minimum coordinate real(dp), intent(in) :: x_max(3) !< Maximum coordinate integer, intent(in) :: n_points(3) !< How many values to store integer, intent(in) :: n_cols !< Number of variables that will be looked up type(LT3_t) :: my_lt if (any(x_max <= x_min)) stop "LT3_create error: x_max <= x_min" if (any(n_points <= 1)) stop "LT3_create error: n_points <= 1" my_lt%n_points = n_points my_lt%x_min = x_min my_lt%dx = (x_max - x_min) / (n_points - 1) my_lt%inv_dx = 1 / my_lt%dx allocate(my_lt%rows_cols(n_points(1), n_points(2), n_points(3), n_cols)) my_lt%rows_cols = 0 my_lt%n_cols = n_cols end function LT3_create !> This function returns a new lookup table from regularly spaced data function LT3_create_from_data(x_min, x_max, spaced_data) result(my_lt) real(dp), intent(in) :: x_min(3) !< Minimum coordinate real(dp), intent(in) :: x_max(3) !< Maximum coordinate real(dp), intent(in) :: spaced_data(:, :, :, :) !< Input data of shape (nx, ny, nz, n_cols) integer :: n_points(3), n_cols type(LT3_t) :: my_lt n_points(1) = size(spaced_data, 1) n_points(2) = size(spaced_data, 2) n_points(3) = size(spaced_data, 3) n_cols = size(spaced_data, 4) if (any(x_max <= x_min)) stop "LT3_create error: x_max <= x_min" if (any(n_points <= 1)) stop "LT3_create error: n_points <= 1" my_lt%n_cols = n_cols my_lt%n_points = n_points my_lt%x_min = x_min my_lt%dx = (x_max - x_min) / (n_points - 1) my_lt%inv_dx = 1 / my_lt%dx !TODO modified so that it does not rely on automatic allocation allocate(my_lt%rows_cols(n_points(1), n_points(2), n_points(3), n_cols)) my_lt%rows_cols = spaced_data end function LT3_create_from_data !> Get a location in the lookup table elemental function LT3_get_loc(my_lt, x1, x2, x3) result(my_loc) type(LT3_t), intent(in) :: my_lt real(dp), intent(in) :: x1, x2, x3 type(LT3_loc_t) :: my_loc real(dp) :: frac(3) frac = ([x1, x2, x3] - my_lt%x_min) * my_lt%inv_dx my_loc%low_ix = ceiling(frac) my_loc%low_frac = my_loc%low_ix - frac ! Check bounds where (my_loc%low_ix < 1) my_loc%low_ix = 1 my_loc%low_frac = 1 end where where (my_loc%low_ix >= my_lt%n_points) my_loc%low_ix = my_lt%n_points - 1 my_loc%low_frac = 0 end where end function LT3_get_loc !> Get the value of a single column at x elemental function LT3_get_col(my_lt, col_ix, x1, x2, x3) result(col_value) type(LT3_t), intent(in) :: my_lt integer, intent(in) :: col_ix real(dp), intent(in) :: x1, x2, x3 real(dp) :: col_value type(LT3_loc_t) :: loc loc = LT3_get_loc(my_lt, x1, x2, x3) col_value = LT3_get_col_at_loc(my_lt, col_ix, loc) end function LT3_get_col !> Get the value of a single column at a location elemental function LT3_get_col_at_loc(my_lt, col_ix, loc) result(col_value) type(LT3_t), intent(in) :: my_lt integer, intent(in) :: col_ix type(LT3_loc_t), intent(in) :: loc integer :: ix(3) real(dp) :: w(2, 2, 2) real(dp) :: col_value ! Bilinear interpolation w(1, 1, 1) = loc%low_frac(1) * loc%low_frac(2) w(2, 1, 1) = (1 - loc%low_frac(1)) * loc%low_frac(2) w(1, 2, 1) = loc%low_frac(1) * (1 - loc%low_frac(2)) w(2, 2, 1) = (1 - loc%low_frac(1)) * (1 - loc%low_frac(2)) w(:, :, 2) = w(:, :, 1) * (1 - loc%low_frac(3)) w(:, :, 1) = w(:, :, 1) * loc%low_frac(3) ix = loc%low_ix col_value = sum(w * my_lt%rows_cols(ix(1):ix(1)+1, ix(2):ix(2)+1,& ix(3):ix(3)+1, col_ix)) end function LT3_get_col_at_loc end module mod_lookup_table