!----------------------------------------------------------------------------- ! ! spreading.f90 -- Expand bias corrections to grids, and write output file. ! ! This is the top level routine of the spreading module, ! one of the four main components of the NOAA NCO/ARL/PSD ! bias correction system for CMAQ forecast outputs. ! ! 2014-jul-10 Original top-level spreading control module. ! By Dave Allured, NOAA/PSD/CIRES. ! ! 2016-jan-20 Rename main routine for name conflict with fortran intrinsic! ! 2016-feb-09 Add output_limit_method parameter. ! ! 2019-may-30 Interface change for read_gridded_vars. ! Add consistency checks for input file mismatch. ! ! 2020-aug-02 Return bias corrected forecast grids to calling program, ! for other uses such as probability forecast. ! Add option to suppress writing output file, calculate only. ! ! 2021-mar-24 Add support for mixed forecast lengths, rather than failing. ! Match output lengths to the uncorrected forecast input file. ! 2021-apr-20 Add site bias arrays to main output file, for diagnostics. ! ! 2022-apr-10 Add support for hourly gridded input files for RRFS-CMAQ. ! The number of forecast hours is now normally pre-determined ! in the config file. However, continue to adapt to the ! gridded input data set, and support best effort in case ! of various possible mismatches. ! 2022-may-25 Convert units of site arrays from ppm to ppb, as needed to ! match forecast files. ! Add units attributes to output files. ! 2022-jun-02 Add alternate netcdf output subroutine for new RRFS format. ! ! * Remember to update the date in the module_id below. ! ! This spreading module incorporates three major functions: ! ! * Read raw gridded forecasts for a given forecast cycle. ! * Expand site bias corrections to entire forecast grids. ! * Write bias corrected output file. ! ! Primary inputs: ! ! * Gridded raw forecast file for current forecast cycle. ! * Uncorrected forecasts interpolated to site locations. ! * Bias corrected forecasts at site locations. ! * Auxiliary grid coordinate file, as needed. ! * Site coordinates. ! ! Primary outputs: ! ! * Single bias corrected gridded file containing: ! 1. Bias corrected grids for all forecast hours. ! 2. Bias grids for all forecast hours. ! * Bias corrected grids returned to calling program. ! ! Notes: ! ! Input and output file paths are specified with path templates. ! These contain YYYY MM DD HH substitution strings that will ! resolve to desired file names for the given forecast cycle. ! Templates may include full or relative paths, and they may ! begin with an $ENV environnment variable. ! ! A separate grid coordinate file is needed only when the gridded ! raw forecast files do not include their own grid coordinates. ! ! Forecast date index: Integer Gregorian date, relative to ! 1 = January 1 of base_year. See the index_to_date library ! routine for details. ! !----------------------------------------------------------------------------- module spreading_mod contains subroutine spreading (in_gridded_template, reader_code_gridded, & grid_coord_file, output_file_template, target_var, output_limit_method, & forecast_date, cycle_time, base_year, calendar, uncorr_sites, & corr_sites, site_ids, site_lats, site_lons, vmiss, diag, corr_grids) use config, only : dp use expand__filename use index_to_date_mod use read__gridded_vars use spread__bias use stdlit, only : normal use string_utils use write__corrected_netcdf_v1 use write__corrected_netcdf_rrfs implicit none character(*), parameter :: module_id = 'spreading.f90 version 2022-jun-02' ! Input arguments. character(*), intent(in) :: in_gridded_template ! gridded input template character(*), intent(in) :: reader_code_gridded ! reader for gridded input character(*), intent(in) :: grid_coord_file ! aux. grid coordinate file character(*), intent(in) :: output_file_template ! output filename template character(*), intent(in) :: target_var ! target variable name character(*), intent(in) :: output_limit_method ! output limit method name integer, intent(in) :: forecast_date ! target date index integer, intent(in) :: cycle_time ! forecast cycle time integer, intent(in) :: base_year ! base year for date indexes character(*), intent(in) :: calendar ! calendar system in use real(dp), intent(in) :: uncorr_sites(:,:) ! uncorrected forecasts ! at sites (hours, sites) real(dp), intent(in) :: corr_sites(:,:) ! bias corrected forecasts ! at sites (hours, sites) character(*), intent(in) :: site_ids(:) ! site ID's (sites) real(dp), intent(in) :: site_lats(:) ! site coordinates (sites) real(dp), intent(in) :: site_lons(:) real(dp), intent(in) :: vmiss ! common missing value code integer, intent(in) :: diag ! diag verbosity level, 0-N ! Output argument. real(dp), allocatable, intent(out) :: corr_grids (:,:,:) ! (X, Y, hours) ! bias corrected grids ! Local variables. character fdate_str*24, fmt1*60, fmt2*60, outfile*200 character(60) filter_units, target_units, units2 character yes_no*3, format_code*15 integer year, month, day integer nhours_sites, nsites, nhours_out, nhours_common integer nhours_config, nhours_actual_max integer ivar, status, multiplier logical need_conversion ! Dynamic arrays. character(60), allocatable :: units(:) ! target units attribute integer, allocatable :: nhours_actual(:) ! actual hours for input var real(dp), allocatable :: grid_lats(:,:) ! grid coordinates (X, Y) real(dp), allocatable :: grid_lons(:,:) real, allocatable :: uncorr_grids(:,:,:,:) ! input raw forecast grids ! (X, Y, [var], hours) ! note single precision here real(dp), allocatable :: bias_grids (:,:,:) ! output bias grids ! (X, Y, hours) real(dp), allocatable :: uncorr_sites2(:,:) ! site input arrays real(dp), allocatable :: corr_sites2(:,:) ! after size matching !------------------------------------------------------- ! Read raw gridded forecast data for target variable. !------------------------------------------------------- if (diag >= 2) print * if (diag >= 2) print *, 'spreading: Begin spreading module.' if (diag >= 2) print *, ' Module ID = ' // module_id ! Decode the date index for the current forecast cycle. call index_to_date (forecast_date, year, month, day, base_year, calendar) ! get current Y M D integers ! Get dimensions for input site arrays. nhours_sites = size (uncorr_sites, 1) nsites = size (uncorr_sites, 2) ! Expect same number of hours when reading the uncorrected forecast grids. ! Actual forecast hours for target var in file are usually the same, ! but may differ with mixed or mismatched length training data. nhours_config = nhours_sites ! Utilize the same gridded reader used by the interpolation program. ! To include all possible file configurations, this routine also ! reads the companion grid coordinates. ! This routine reads forecast grids in the original single precision. ! Later they will be converted on the fly to doubles. ! Allow for possible over-dimensioning of the number of forecast hours. ! Be sure to use nhours_actual(1), not size of the returned uncorr_grids. ! In effect this means that nhours_out will actually be adaptive. if (diag >= 4) then print '(3a)', ' in_gridded_template = [', trim (in_gridded_template),']' end if call read_gridded_vars ( (/ target_var /), (/ reader_code_gridded /), & (/ in_gridded_template /), grid_coord_file, year, month, day, & cycle_time, nhours_config, real (vmiss), diag, uncorr_grids, & nhours_actual, nhours_actual_max, units, grid_lats, grid_lons, status) ! read single target var into 4-D (X, Y, [var], hours) target_units = units(1) ! Abort if raw forecast grids for target variable are not available. ! Reader may have also printed some diagnostic details. if (status /= normal) then fmt1 = '(a,i0.4,3(1x,i0.2),"Z")' print * print fmt1, '*** spreading: Raw ' // trim (target_var) & // ' forecast grids for target forecast cycle are not available.' print fmt1, '*** Target forecast cycle = ', year, month, day, cycle_time print fmt1, '*** Fatal, can not complete bias correction for this' & // ' forecast cycle.' call exit (1) end if ! Warn if forecast lengths are mismatched, but continue processing. if ( nhours_actual(1) /= nhours_config & .or. nhours_actual_max /= nhours_config) then fmt1 = '(a,i0.4,3(1x,i0.2),"Z")' fmt2 = '(a,i0)' print * print fmt1, '*** spreading: Warning, inconsistent forecast lengths.' print fmt2, '*** nhours_config = ', nhours_config print fmt2, '*** nhours_actual(1) = ', nhours_actual(1) print fmt2, '*** nhours_actual_max = ', nhours_actual_max print fmt1, '*** Input data sets are mixed lengths, or data set mismatch.' print fmt1, '*** Target forecast cycle = ', year, month, day, cycle_time end if !----------------------------------------------------------- ! Expand or contract site arrays to match forecast files. !----------------------------------------------------------- ! Support mixed forecast lengths between training data and target forecast. nhours_out = nhours_actual(1) nhours_common = min (nhours_sites, nhours_out) if (diag >= 2) then fmt1 = '(3x,a,i0)' print * print *, 'spreading: Expand or contract site arrays to match' & // ' forecast file.' print fmt1, ' Site hours input = ', nhours_sites print fmt1, ' Site hours after matching = ', nhours_out print * end if allocate (uncorr_sites2(nhours_out, nsites)) allocate (corr_sites2 (nhours_out, nsites)) ! Pad with missing values as needed, to neutralize bias correction. ! This method is valid to both expand and contract. uncorr_sites2(:,:) = vmiss corr_sites2 (:,:) = vmiss uncorr_sites2(1:nhours_common,:) = uncorr_sites(1:nhours_common,:) corr_sites2 (1:nhours_common,:) = corr_sites (1:nhours_common,:) !----------------------------------------------------------- ! Convert units of site arrays to match forecast files. !----------------------------------------------------------- ! 2022 May 25: Adjust for one known variation, which is ppb instead ! of the traditional ppm/ppmV. Incoming site arrays from analog ! filter are always in ppm/ppmV, or equivalent. If forecast files ! are in ppb, then convert site arrays to ppb, before spreading and ! output. if (diag >= 2) print *, 'spreading: Check for units conversion.' filter_units = 'ppm' units2 = target_units call lowercase (units2) need_conversion = (any (units2 == (/ 'ppb ', 'ppbv' /) )) yes_no = merge ('YES', 'NO ', need_conversion) if (diag >= 2) then print *, ' Var name 1 = uncorr_sites2' print *, ' Var name 2 = corr_sites2' print *, ' Units needed = ', trim (target_units) print *, ' Units conversion needed = ', trim (yes_no) print * end if convert_to_ppb: & if (need_conversion) then multiplier = 1000 ! ppm to ppb if (diag >= 2) then print *, 'spreading: Apply units conversion to site arrays,', & ' before spreading.' print *, ' Units from analog filter = ', trim (filter_units) print *, ' Units needed = ', trim (target_units) print '(1x,a,i0)', & ' Multiplier = ', multiplier print * end if where (uncorr_sites2(:,:) /= vmiss) uncorr_sites2(:,:) = uncorr_sites2(:,:) * multiplier end where where (corr_sites2(:,:) /= vmiss) corr_sites2(:,:) = corr_sites2(:,:) * multiplier end where end if convert_to_ppb !------------------------------------------------------- ! Spread bias corrections to forecast grids. !------------------------------------------------------- call fdate (fdate_str) print '(2a)', fdate_str, ' spreading: Spread bias corrections to grids.' ! Convert gridded input to double precision on the fly, for calculations. ivar = 1 ! index for target variable input grids; ! is 1 because only single var was just read call spread_bias (dble (uncorr_grids(:,:,ivar,1:nhours_out)), & uncorr_sites2, corr_sites2, grid_lats, grid_lons, site_lats, site_lons, & vmiss, diag, output_limit_method, bias_grids, corr_grids) !------------------------------------------------------- ! Write bias corrected output file. !------------------------------------------------------- write_select: & if (output_file_template == 'none') then print *, & '*** spreading: Hourly output file is suppressed. No file written.' else call fdate (fdate_str) print '(2a)', fdate_str, ' spreading: Write output file.' ! Create actual output file name for current forecast cycle. call expand_filename (output_file_template, year, month, day, & cycle_time, outfile) ! Identify the output file format to match the input forecast file. if (reader_code_gridded == 'reader.hourly') then format_code = 'rrfs' else format_code = 'original' end if ! Write output file. Include both grids and site bias data, for diagnostics. ! Select the writer subroutine to match the input file layout. ! Data and coordinates will be converted from double to single precision, ! when writing to file. if (format_code == 'original') then call write_corrected_netcdf_v1 (outfile, target_var, target_units, & grid_lats, grid_lons, corr_grids, bias_grids, uncorr_sites2, & corr_sites2, site_ids, site_lats, site_lons, vmiss, diag) else call write_corrected_netcdf_rrfs (outfile, target_var, target_units, & grid_lats, grid_lons, corr_grids, bias_grids, uncorr_sites2, & corr_sites2, site_ids, site_lats, site_lons, vmiss, diag) end if end if write_select if (diag >= 3) print *, 'spreading: Return.' end subroutine spreading end module spreading_mod