Index: flexpart_code/checkgrib/Makefile
===================================================================
--- flexpart_code/checkgrib/Makefile	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
+++ flexpart_code/checkgrib/Makefile	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
@@ -0,0 +1,23 @@
+BINARY = checkGRIB
+FC = gfortran
+FFLAGS = -Wall -g -O2 -mcmodel=medium -fconvert=little-endian -frecord-marker=4
+
+#GRIB_API = /opt/grib_api-1.14.0
+GRIB_API = /dvlscratch/ATM/morton/atm-software-20190112
+INCLUDES = -I${GRIB_API}/include
+LIBS = -L${GRIB_API}/lib -lgrib_api_f90 -lgrib_api -ljasper
+
+
+all : ${BINARY}
+
+${BINARY} : ${BINARY}.o cgutils.o
+	${FC} -o ${BINARY} ${BINARY}.o cgutils.o ${LIBS}
+
+${BINARY}.o : ${BINARY}.F90 cgutils.o
+	${FC} -c ${FFLAGS} ${INCLUDES} ${BINARY}.F90
+
+cgutils.o : cgutils.F90
+	${FC} -c ${FFLAGS} ${INCLUDES} cgutils.F90
+
+clean :
+	$(RM) ${BINARY} *.o *.mod
Index: flexpart_code/checkgrib/README.md
===================================================================
--- flexpart_code/checkgrib/README.md	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
+++ flexpart_code/checkgrib/README.md	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
@@ -0,0 +1,393 @@
+
+
+# checkGRIB
+
+This utility was created primarily for CTBTO operations to facilitate a check 
+of incoming ECMWF, NCEP and NCEPFV3 GRIB2 files before they are staged into the ATM 
+pipeline.  The idea is to catch problems in the files long before they are 
+actually used, rather than to have the problems discovered, mysteriously, in 
+a termination of an application that relies on the files.
+
+## Author information
+
+Don Morton
+Boreal Scientific Computing
+Fairbanks, Alaska, USA
+Don.Morton@borealscicomp.com
+
+
+
+I'm horrible at legalese but, as far as I'm concerned, this utility is totally 
+free and open to the public for use and modification, and is included with 
+the FLEXPART distribution with the same permissions.  
+
+----
+
+## Installation
+
+Code in this directory is self-contained, but relies on an installation of 
+GRIB-API library.  Additionally, a test package is provided that depends 
+on Python v2.7 (also works with Python 3.6) and GRIB-API command line tools.
+
+Compilation simply requires setting the `GRIB_API` variable in the *Makefile* 
+to the local location, and then invoking *make*
+
+----
+
+## Usage
+
+Usage is pretty straightforward, requiring flags `--source` and `--levels`, 
+where the source is expected to be `ECMWF`, `NCEP` or `NCEPFV3`.
+Originally, we assumed GRIB2 (all messages) files, but we've learned that 
+is not valid for EF files, so `--checkgrib2` is now an optional test, not 
+performed by default.  The number of levels should be the expected number 
+of levels for 3D variables.
+
+The utility currently checks that all variables required for FLEXPART 
+(including dry and wet deposition) are in the GRIB file, that all expected 
+levels are included, that the source is, indeed the expected source. 
+
+There are many more ambitious (and time-consuming) checks that could be 
+added in the future.  For example, the GRIB messages have fields for max, 
+min and mean values, and we could actually read in each field and verify these.
+
+The utility prints a simple message to *stdout* if all is well with a file, 
+and if an error is found, a message is printed to *stdout*, and the utility 
+aborts with a system return code of 1 to indicate a problem with the GRIB file,
+ and a 2 to indicate a problem with the utility.
+
+Some examples follow:
+
+### A normal, successful test
+```
+$ ./checkGRIB --source ECMWF --levels 137 test/gribfiles/ecmwf0p5/EN19011112
+ Passed: test/gribfiles/ecmwf0p5/EN19011112
+
+$ echo $?
+0
+```
+
+### Checking with an incorrect source parameter
+
+```
+$ ./checkGRIB --source NCEP --levels 137 test/gribfiles/ecmwf0p5/EN19011112
+ ERROR: Unexpected grib_centre:           98
+ Failed on: test/gribfiles/ecmwf0p5/EN19011112
+
+$ echo $?
+1
+```
+
+### Non-existent file
+
+```
+$ ./checkGRIB --source NCEP --levels 137 Huh???
+GRIB_API ERROR   :  IO ERROR: No such file or directory: Huh??? (No such file or directory)
+ ERROR: problem opening GRIB file: Huh???
+ Failed on: Huh???
+
+$ echo $?
+2
+```
+
+### Finding more levels than expected
+
+```
+$ ./checkGRIB --source NCEP --levels 27 test/gribfiles/ncep0p5/GD19010818
+ERROR: Found more than  27 levels
+ Failed on: test/gribfiles/ncep0p5/GD19010818
+
+$ echo $?
+1
+```
+
+### Finding less levels than expected
+
+```
+$ ./checkGRIB --source NCEP --levels 48 test/gribfiles/ncep0p5/GD19010818
+ERROR: Only found   31 levels
+ Failed on: test/gribfiles/ncep0p5/GD19010818
+
+$ echo $?
+1
+```
+----
+
+## Structure
+
+All of the GRIB-related code is in the module, *cgutils.F90*.  There is a 
+lot of "file-specific" code in here for ECMWF, NCEP  and NCEP FV3 GRIB files.  
+The main program, *checkGRIB.F90* merely collects and parses command 
+line arguments, and invokes appropriate routines in the *cgutils* module.
+
+----
+
+## Testing
+
+A comprehensive testing package, located in the *test* subdirectory,  
+was developed to test this utility under a wide variety of conditions, 
+including successful execution, bad command line arguments, missing levels 
+and missing variables, etc.  Obviously, not every potential problem is tested, 
+but a broad assortment of "spot-checking" leaves me confident that this 
+is robust.  
+
+The testing package requires the default Python v2.7 found on *devlan* 
+(it also runs with Python v3.6), and the command line grib tools from 
+GRIB-API.  The grib tools are required because some of the tests require 
+taking a good GRIB file and making it bad.  I decided it would be better 
+to do this programatically rather than requiring a large number of GRIB 
+files for testing.
+
+
+
+
+
+The current tests include:
+
+```
+--------test_missing_source_cli_arg_detected--------
+--------test_invalid_source_cli_arg_detected-------
+--------test_missing_levels_cli_arg_detected--------
+--------test_checkanl_and_checkfcst_detected--------
+--------test_reports_no_cli_args--------
+--------test_reports_no_path_args--------
+--------test_successful_ecmwfgrib2_check--------
+--------test_successful_ecmwfgrib_ef_check--------
+--------test_reports_ecmwfgrib_ef_fails_with_checkgrib2--------
+--------test_reports_unexpected_ecmwf_grib_center--------
+--------test_reports_unexpected_ecmwf_level--------
+--------test_reports_missing_ecmwf_levels--------
+--------test_reports_failed_ecmwf_anl_check--------
+--------test_reports_failed_ecmwf_fcst_check--------
+--------test_ecmwf_detects_missing_level_14--------
+--------test_ecmwf_detects_missing_level_14_etadot--------
+--------test_ecmwf_detects_missing_var_lsp--------
+--------test_ecmwf_detects_grib1_nsss--------
+--------test_reports_unexpected_ncep_grib_center--------
+--------test_successful_ncepgrib2_check--------
+--------test_detects_ncep_more_pressure_levels_than_expected------
+--------test_detects_ncep_fewer_pressure_levels_than_expected------
+--------test_ncep_detects_missing_level_850------
+--------test_ncep_detects_missing_level_100_r------
+--------test_ncep_detects_missing_var_10u------
+--------test_ncep_detects_missing_var_tsig1------
+--------test_ncep_detects_grib1_2t------
+--------test_detects_ncepfv3_more_pressure_levels_than_expected------
+--------test_detects_ncepfv3_fewer_pressure_levels_than_expected------
+--------test_ncepfv3_detects_missing_level_850------
+--------test_ncepfv3_detects_missing_level_100_r------
+--------test_ncepfv3_detects_missing_var_10u------
+--------test_ncepfv3_detects_missing_var_tsig1------
+--------test_ncepfv3_detects_grib1_2t------
+```
+
+Successful execution of the tests looks like
+
+```
+$ ./checkgrib_test.py
+Compiling checkGRIB in dir: /home/morton/git/MyFlexpartTools/CTBTO_SWEATM/WO02/checkgrib
+compile_passed: True
+--------test_missing_source_cli_arg_detected--------
+ args missing or out of sync
+  
+ Usage: 
+  
+ checkgrib --source [ECMWF | NCEP | NCEPFV3] --levels <int> 
+           [ --checkfcst | --checkanl | --checkgrib2 ]
+           path1 path2 ...
+  
+passed: True
+-----------------------------------------------
+.
+.
+.
+--------test_ecmwf_detects_missing_var_lsp--------
+ ERROR: lsp: not found
+ Failed on: /tmp/1fbd642c-316e-4fc6-910a-24870fc2611b.gr2
+passed: True
+-----------------------------------------------
+.
+.
+.
+--------test_ncep_detects_grib1_2t------
+ Grib message not GRIB2
+ ERROR: Bad gribmsg_shortname: 2t, gribmsg_level:            2
+ Failed on: /tmp/badeefa8-2ab8-4474-849c-45ac2ec4ac44.gr2
+passed: True
+-----------------------------------------------
+.
+.
+.
+--------test_ncepfv3_detects_grib1_2t------
+ Grib message not GRIB2
+ ERROR: Bad gribmsg_shortname: 2t, gribmsg_level:            2
+ Failed on: /tmp/da9dd1fc-d843-4b9e-ab74-82ea8769fbe3.gr2
+passed: True
+-----------------------------------------------
+
+************************
+Passed tests: 35
+Failed tests: 0
+************************
+
+```
+
+
+## Testing files
+
+The testing package requires a set of GRIB files.  I have this set up
+so that it can use a prepared package of files or files in place at
+CTBTO
+
+### Prepared test files
+
+These are available at http://borealscicomp.com/CTBTO_SWEATM/checkgrib_testfiles/gribfiles/.  Unfortunately, because they contain ECMWF files and ECMWF is
+very strict about posting of such files, I have to make these non-readable.
+If somebody wants to retrieve them, they should notify me and I can make them
+temporarily readable.  Once readable, they can be placed in the test
+package by going to the *checkgrib/test/* directory, then
+
+```
+$ wget --recursive --no-parent --cut-dirs=2 -nH -R "index.html*" --execute robots=off http://borealscicomp.com/CTBTO_SWEATM/checkgrib_testfiles/gribfiles
+```
+
+Then, in *checkgrib_test.py* be sure to set the following
+
+```
+ECMWF_PREFIX = 'gribfiles/ecmwf0p5'
+NCEP_PREFIX = 'gribfiles/ncep0p5'
+NCEPFV3_PREFIX = 'gribfiles/ncepfv30p5'
+```
+
+
+There is a *gribfiles* entry in *test/.gitignore* so that these large files 
+won't be committed to the repo.
+
+### Using CTBTO files
+
+#WARNING - this section is not relevant right now#
+
+The ncep subdir structure now has subdirectories *0.5* and
+*0.5.fv3*, which would require some recoding to make this work
+correctly.
+
+In *checkgrib_test.py* one would want to set (for example)
+
+```
+CTBTO_PREFIX = '/ops/data/atm'
+ECMWF_PREFIX = CTBTO_PREFIX + '/ecmwf/2019/01/11/0.5'
+NCEP_PREFIX = CTBTO_PREFIX + '/ncep/2019/01/08/0.5'
+```
+
+Of course, you would want to make sure that the paths are actually valid,
+as they may change over time.
+
+
+----
+
+## Notes
+
+
+### ECMWF notes
+
+* Depending on the version of *GRIB-API* used, snow depth in ECMWF files may 
+have a *shortName* of  *sd* or *sde.*  The utility will handle both cases.  
+
+* EF files seem to have the old standard GRIB2 for model level variables
+and GRIB1 for the surface variables, and tests have been adjusted to
+account for this. 
+
+
+
+
+
+
+### NCEP notes
+
+* Vertical wind, *w*, seems to be only available at pressure levels of 100mb 
+and lower in altitude, so that's all I'm checking for, and it's done in its 
+own function, *ncep_all_expected_w_levels_present()*.  I have verified 
+through my own operations with other NCEP files that *w* is not available 
+for levels higher than 100mb.
+
+* It doesn't seem like total precipitation, *tc*, convective precipitation,
+*acpcp*, or total cloud cover, *tcc* are available in the NCEP files being 
+downloaded to CTBTO.  They used to be available.  So, I'm not checking for 
+those right now (I've commented out the checks in *ncep_all_2dvars_present()*).  
+* For 2-meter RH, one needs to look for leveltype *heightAboveGround* and 
+then, for older GRIB-API, look for *r* specifically at *level* 2, because 
+there are other messages with *shortName* of *r*.   But, for newer GRIB-API 
+the *shortName* to look for is a unique *2r* (also at *level* 2, but that 
+doesn't really matter once you have the *2r*).  The utility handles both 
+situations.
+
+
+
+### NCEP FV3 notes
+
+* The notes concerning NCEP, above, apply.  
+
+* NCEP has added 15 mb and 40 mb pressure levels to the GRIB files, but only for
+the temperature variable and some other variables we don't care about.  This
+ends up breaking the NCEP-checking code, and FV3-specific code has been added.
+
+
+
+### forecast vs analysis messages
+
+This got complicated and confusing and is not complete, pending more 
+complete future understanding of how we really want to categorise a GRIB 
+file as being *analysis* or *forecast*
+
+* GRIB messages are checked for analysis or forecast by looking at the 
+*dataType* field for values of *an* or *fc*.  I'm a little confused on
+these values, and I have some suspicion that they may not always be correct.  
+There is also a *forecastTime* field that "seems" to be zero for what might 
+be analysis messages and nonzero (number of hours since last analysis) 
+for forecast messages.
+
+* For ECMWF, it appears that the analysis files (as determined by a 
+*dataType* of *an* are 12Z.  Most of the messages are *an* in the 12Z files, 
+yet there are six messages in these files that are *fc*:
+
+
+```
+2            ecmf         20190111     fc           regular_ll   surface      0            0            lsp          grid_jpeg   
+2            ecmf         20190111     fc           regular_ll   surface      0            0            acpcp        grid_jpeg   
+2            ecmf         20190111     fc           regular_ll   surface      0            0            sshf         grid_jpeg   
+2            ecmf         20190111     fc           regular_ll   surface      0            0            ewss         grid_jpeg   
+2            ecmf         20190111     fc           regular_ll   surface      0            0            nsss         grid_jpeg   
+2            ecmf         20190111     fc           regular_ll   surface      0            0            ssr          grid_jpeg   
+```
+
+
+* Meanwhile, the non-12Z ECMWF files seem to have *dataType* of *fc* for all 
+messages except for four:
+
+
+```
+2            ecmf         20190111     an           regular_ll   surface      0            0            sdor         grid_jpeg   
+2            ecmf         20190111     an           regular_ll   surface      0            0            cvl          grid_jpeg   
+2            ecmf         20190111     an           regular_ll   surface      0            0            cvh          grid_jpeg   
+2            ecmf         20190111     an           regular_ll   surface      0            0            sr           grid_jpeg   
+```
+
+
+The problem with all of this is that a test for a successful *fcst* or *anl*
+file will always fail, since they are currently mixed.
+
+* For NCEP files, it appears that all messages in all files are *fc*.
+
+Meanwhile, correspondence with both Leo and Henrik suggest that people 
+"generally" assume that 00/06/12/18 files are *analysis* (though some 
+messages will still be *forecast*).  So, the problems are
+
+* For ECMWF files, I "think" we will always have a mixture of *analysis* 
+and *forecast* messages
+* The use of *dataType* values of *fc* or *an* doesn't seem to correlate 
+with the expected 00/06/12/18 *analyses* files.
+
+So, in short, we need to better define what we want to look for.  
+The code is generally in the utility, but would need to be modified for 
+specific definitions.
+
Index: flexpart_code/checkgrib/cgutils.F90
===================================================================
--- flexpart_code/checkgrib/cgutils.F90	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
+++ flexpart_code/checkgrib/cgutils.F90	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
@@ -0,0 +1,2061 @@
+MODULE cgutils
+
+    IMPLICIT NONE
+
+    PRIVATE
+
+    ! All publicly accessible functions and variables need to be 
+    ! declared as such here.  All public names are prefixed with
+    ! "gributils_"
+    PUBLIC :: cgutils_check_ecmwf_grib, &
+&             cgutils_check_ncep_grib2, &
+&             cgutils_check_fv3_grib2
+
+    ! These  codes depict the origin/model of the GRIB file
+    INTEGER, PARAMETER :: GRIB_CENTRE_NCEP = 7, &
+&                         GRIB_CENTRE_ECMWF = 98
+
+
+    ! Return codes
+    INTEGER, PARAMETER :: RETURN_CODE_NORMAL = 0,          &
+&                         RETURN_CODE_GRIBFILE_ERROR = 1,  &
+&                         RETURN_CODE_UTILITY_ERROR = 2
+
+    ! String lengths
+    INTEGER, PARAMETER :: GRIB_ATTRIBUTE_STR_LEN = 64
+
+    ! Global (within this module) variables
+    ! All global variables are prefixed with "global_"
+
+    ! 3D variables of interest, T/F flag for each level present 
+    ! 
+    ! Variables names correspond more to "Flexpart" names than 
+    ! actual GRIB names
+    ! 
+    ! ECMWF uses - u, v, t, q, etadot
+    ! 
+    ! NCEP uses - u, v, t, rh, w
+    LOGICAL, DIMENSION(:), ALLOCATABLE :: global_levels_u,      &
+&                                         global_levels_v,      &
+&                                         global_levels_t,      &
+&                                         global_levels_q,      &
+&                                         global_levels_etadot, &
+&                                         global_levels_w,      &
+&                                         global_levels_rh 
+
+
+    ! 2D variables of interest, T/F flag for each 
+    ! 
+    ! Variables names correspond more to "Flexpart" names than 
+    ! actual GRIB names
+    ! 
+    ! ECMWF uses - ps, sd, msl, tcc, u10, v10, t2, td2, lsprec,
+    !              convprec, shf, sr, ewss, nsss, oro, excessoro, lsm
+    ! 
+    ! NCEP uses - ps, sd, msl, u10, v10, t2, td2, tcc, lsprec, convprec,
+    !             oro, lsm, hmix, rh2, tsig1, usig1, vsig1
+    LOGICAL :: global_2dvar_ps,        &
+&              global_2dvar_sd,        &
+&              global_2dvar_msl,       &
+&              global_2dvar_tcc,       &
+&              global_2dvar_u10,       &
+&              global_2dvar_v10,       &
+&              global_2dvar_t2,        &
+&              global_2dvar_td2,       &
+&              global_2dvar_lsprec,    &
+&              global_2dvar_convprec,  &
+&              global_2dvar_shf,       &
+&              global_2dvar_sr,        &
+&              global_2dvar_ewss,      &
+&              global_2dvar_nsss,      &
+&              global_2dvar_oro,       &
+&              global_2dvar_excessoro, &
+&              global_2dvar_lsm,       &
+&              global_2dvar_hmix,      &
+&              global_2dvar_rh2,       &
+&              global_2dvar_tsig1,     &
+&              global_2dvar_usig1,     &
+&              global_2dvar_vsig1 
+
+
+CONTAINS
+
+    SUBROUTINE allocate_and_init_flags(expected_num_levels)
+    IMPLICIT NONE
+
+        ! This is used by ECMWF, NCEP and FV3 routines.  There are
+        ! several variables used by one, that isn't used by the
+        ! other, but I think the memory load is negligible and
+        ! prefer to take this opportunity to present a little bit
+        ! of clean, reusable code.
+
+        INTEGER, INTENT(IN) :: expected_num_levels
+
+        ! 3D variables (NCEP, FV3 and ECMWF)
+        ALLOCATE(global_levels_u(expected_num_levels))
+        global_levels_u = .FALSE.
+
+        ALLOCATE(global_levels_v(expected_num_levels))
+        global_levels_v = .FALSE.
+
+        ALLOCATE(global_levels_t(expected_num_levels))
+        global_levels_t = .FALSE.
+
+        ALLOCATE(global_levels_q(expected_num_levels))
+        global_levels_q = .FALSE.
+
+        ALLOCATE(global_levels_etadot(expected_num_levels))
+        global_levels_etadot = .FALSE.
+
+        ALLOCATE(global_levels_w(expected_num_levels))
+        global_levels_w = .FALSE.
+
+        ALLOCATE(global_levels_rh(expected_num_levels))
+        global_levels_rh = .FALSE.
+
+
+
+        ! 2D variables (both NCEP, ECMWF and FV3)
+        global_2dvar_ps = .FALSE.
+        global_2dvar_sd = .FALSE.
+        global_2dvar_msl = .FALSE.
+        global_2dvar_tcc = .FALSE.
+        global_2dvar_u10 = .FALSE.
+        global_2dvar_v10 = .FALSE.
+        global_2dvar_t2 = .FALSE.
+        global_2dvar_td2 = .FALSE.
+        global_2dvar_lsprec = .FALSE.
+        global_2dvar_convprec = .FALSE.
+        global_2dvar_shf = .FALSE.
+        global_2dvar_sr = .FALSE.
+        global_2dvar_ewss = .FALSE.
+        global_2dvar_nsss = .FALSE.
+        global_2dvar_oro = .FALSE.
+        global_2dvar_excessoro = .FALSE.
+        global_2dvar_lsm = .FALSE.
+        global_2dvar_hmix = .FALSE.
+        global_2dvar_rh2 = .FALSE.
+        global_2dvar_tsig1 = .FALSE.
+        global_2dvar_usig1 = .FALSE.
+        global_2dvar_vsig1 = .FALSE.
+
+    END SUBROUTINE allocate_and_init_flags
+
+
+
+    SUBROUTINE delete_levels_flags
+    IMPLICIT NONE
+        DEALLOCATE(global_levels_u)
+        DEALLOCATE(global_levels_v)
+        DEALLOCATE(global_levels_t)
+        DEALLOCATE(global_levels_q)
+        DEALLOCATE(global_levels_etadot)
+        DEALLOCATE(global_levels_w)
+        DEALLOCATE(global_levels_rh)
+    END SUBROUTINE delete_levels_flags
+
+
+
+    LOGICAL FUNCTION ecmwf_all_2dvars_present()
+    IMPLICIT NONE
+
+        ! Checks logical flags for each 2d variable, testing for
+        ! all true.
+        !
+        ! For sake of "situational awareness," this routine will
+        ! go through the logical flags of each 2d array of interest,
+        ! printing error message for each one with a problem, then
+        ! returning the error code after all that.
+
+        ecmwf_all_2dvars_present = .TRUE.
+
+        ! surface levels
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_ps)
+        IF ( .NOT. global_2dvar_ps) &
+&                   WRITE(6,*) 'ERROR: sp: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_sd)
+        IF ( .NOT. global_2dvar_sd) &
+&                   WRITE(6,*) 'ERROR: sd (or sde): not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_tcc)
+        IF ( .NOT. global_2dvar_tcc) &
+&                   WRITE(6,*) 'ERROR: tcc: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_lsprec)
+        IF ( .NOT. global_2dvar_lsprec) &
+&                   WRITE(6,*) 'ERROR: lsp: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_convprec)
+        IF ( .NOT. global_2dvar_convprec) &
+&                   WRITE(6,*) 'ERROR: acpcp: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_shf)
+        IF ( .NOT. global_2dvar_shf) &
+&                   WRITE(6,*) 'ERROR: sshf: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_sr)
+        IF ( .NOT. global_2dvar_sr) &
+&                   WRITE(6,*) 'ERROR: ssr: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_ewss)
+        IF ( .NOT. global_2dvar_ewss) &
+&                   WRITE(6,*) 'ERROR: ewss: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_nsss)
+        IF ( .NOT. global_2dvar_nsss) &
+&                   WRITE(6,*) 'ERROR: nsss: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_oro)
+        IF ( .NOT. global_2dvar_oro) &
+&                   WRITE(6,*) 'ERROR: z: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_excessoro)
+        IF ( .NOT. global_2dvar_excessoro) &
+&                   WRITE(6,*) 'ERROR: sdor: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_lsm)
+        IF ( .NOT. global_2dvar_lsm) &
+&                   WRITE(6,*) 'ERROR: lsm: not found'
+
+
+        ! heightAboveGround levels
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_u10)
+        IF ( .NOT. global_2dvar_u10) &
+&                   WRITE(6,*) 'ERROR: 10u: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_v10)
+        IF ( .NOT. global_2dvar_v10) &
+&                   WRITE(6,*) 'ERROR: 10v: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_t2)
+        IF ( .NOT. global_2dvar_t2) &
+&                   WRITE(6,*) 'ERROR: 2t: not found'
+
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_td2)
+        IF ( .NOT. global_2dvar_td2) &
+&                   WRITE(6,*) 'ERROR: 2d: not found'
+
+
+        ! meanSea levels
+        ecmwf_all_2dvars_present = &
+&                   (ecmwf_all_2dvars_present .AND. global_2dvar_msl)
+        IF ( .NOT. global_2dvar_msl) &
+&                   WRITE(6,*) 'ERROR: msl: not found'
+
+
+        RETURN
+
+
+
+    END FUNCTION ecmwf_all_2dvars_present
+
+
+    LOGICAL FUNCTION ncep_all_2dvars_present()
+    IMPLICIT NONE
+
+        ! Checks logical flags for each 2d variable, testing for
+        ! all true.
+        !
+        ! For sake of "situational awareness," this routine will
+        ! go through the logical flags of each 2d array of interest,
+        ! printing error message for each one with a problem, then
+        ! returning the error code after all that.
+
+        ncep_all_2dvars_present = .TRUE.
+
+        ! surface levels
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_ps)
+        IF ( .NOT. global_2dvar_ps) &
+&                   WRITE(6,*) 'ERROR: sp: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_sd)
+        IF ( .NOT. global_2dvar_sd) &
+&                   WRITE(6,*) 'ERROR: sdwe: not found'
+
+
+
+
+!!! 2019-01-20 - These two variables are currently not part of what I find in 
+!!! NCEP GRIB files at CTBTO.  I think they used to be available.  So, I'm 
+!!! commenting out their testing right now.
+!        ncep_all_2dvars_present = &
+!&                   (ncep_all_2dvars_present .AND. global_2dvar_lsprec)
+!        IF ( .NOT. global_2dvar_lsprec) &
+!&                   WRITE(6,*) 'ERROR: tp: not found'
+!
+!        ncep_all_2dvars_present = &
+!&                   (ncep_all_2dvars_present .AND. global_2dvar_convprec)
+!        IF ( .NOT. global_2dvar_convprec) &
+!&                   WRITE(6,*) 'ERROR: acpcp: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_oro)
+        IF ( .NOT. global_2dvar_oro) &
+&                   WRITE(6,*) 'ERROR: orog: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_lsm)
+        IF ( .NOT. global_2dvar_lsm) &
+&                   WRITE(6,*) 'ERROR: lsm: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_hmix)
+        IF ( .NOT. global_2dvar_hmix) &
+&                   WRITE(6,*) 'ERROR: hpbl: not found'
+
+        ! heightAboveGround levels
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_u10)
+        IF ( .NOT. global_2dvar_u10) &
+&                   WRITE(6,*) 'ERROR: 10u: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_v10)
+        IF ( .NOT. global_2dvar_v10) &
+&                   WRITE(6,*) 'ERROR: 10v: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_t2)
+        IF ( .NOT. global_2dvar_t2) &
+&                   WRITE(6,*) 'ERROR: 2t: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_td2)
+        IF ( .NOT. global_2dvar_td2) &
+&                   WRITE(6,*) 'ERROR: 2d: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_rh2)
+        IF ( .NOT. global_2dvar_rh2) &
+&                   WRITE(6,*) 'ERROR: 2m r: not found'
+
+
+        ! meanSea levels
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_msl)
+        IF ( .NOT. global_2dvar_msl) &
+&                   WRITE(6,*) 'ERROR: prmsl: not found'
+
+        ! sigma levels
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_tsig1)
+        IF ( .NOT. global_2dvar_tsig1) &
+&                   WRITE(6,*) 'ERROR: sigma t: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_usig1)
+        IF ( .NOT. global_2dvar_usig1) &
+&                   WRITE(6,*) 'ERROR: sigma u: not found'
+
+        ncep_all_2dvars_present = &
+&                   (ncep_all_2dvars_present .AND. global_2dvar_vsig1)
+        IF ( .NOT. global_2dvar_vsig1) &
+&                   WRITE(6,*) 'ERROR: sigma v: not found'
+
+
+
+
+
+
+!!! 2019-01-20 - These variable is currently not part of what I find in 
+!!! NCEP GRIB files at CTBTO.  I think it used to be available.  So, I'm 
+!!! commenting out the testing right now.
+        ! Low cloud layer levels
+!        ncep_all_2dvars_present = &
+!&                   (ncep_all_2dvars_present .AND. global_2dvar_tcc)
+!        IF ( .NOT. global_2dvar_tcc) &
+!&                   WRITE(6,*) 'ERROR: tcc: not found'
+
+
+        RETURN
+
+
+
+    END FUNCTION ncep_all_2dvars_present
+
+
+    LOGICAL FUNCTION fv3_all_2dvars_present()
+    IMPLICIT NONE
+
+        ! Checks logical flags for each 2d variable, testing for
+        ! all true.
+        !
+        ! For sake of "situational awareness," this routine will
+        ! go through the logical flags of each 2d array of interest,
+        ! printing error message for each one with a problem, then
+        ! returning the error code after all that.
+
+        fv3_all_2dvars_present = .TRUE.
+
+        ! surface levels
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_ps)
+        IF ( .NOT. global_2dvar_ps) &
+&                   WRITE(6,*) 'ERROR: sp: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_sd)
+        IF ( .NOT. global_2dvar_sd) &
+&                   WRITE(6,*) 'ERROR: sdwe: not found'
+
+
+
+
+!!! 2019-01-20 - These two variables are currently not part of what I find in 
+!!! NCEP GRIB files at CTBTO.  I think they used to be available.  So, I'm 
+!!! commenting out their testing right now.
+!        ncep_all_2dvars_present = &
+!&                   (ncep_all_2dvars_present .AND. global_2dvar_lsprec)
+!        IF ( .NOT. global_2dvar_lsprec) &
+!&                   WRITE(6,*) 'ERROR: tp: not found'
+!
+!        ncep_all_2dvars_present = &
+!&                   (ncep_all_2dvars_present .AND. global_2dvar_convprec)
+!        IF ( .NOT. global_2dvar_convprec) &
+!&                   WRITE(6,*) 'ERROR: acpcp: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_oro)
+        IF ( .NOT. global_2dvar_oro) &
+&                   WRITE(6,*) 'ERROR: orog: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_lsm)
+        IF ( .NOT. global_2dvar_lsm) &
+&                   WRITE(6,*) 'ERROR: lsm: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_hmix)
+        IF ( .NOT. global_2dvar_hmix) &
+&                   WRITE(6,*) 'ERROR: hpbl: not found'
+
+        ! heightAboveGround levels
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_u10)
+        IF ( .NOT. global_2dvar_u10) &
+&                   WRITE(6,*) 'ERROR: 10u: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_v10)
+        IF ( .NOT. global_2dvar_v10) &
+&                   WRITE(6,*) 'ERROR: 10v: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_t2)
+        IF ( .NOT. global_2dvar_t2) &
+&                   WRITE(6,*) 'ERROR: 2t: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_td2)
+        IF ( .NOT. global_2dvar_td2) &
+&                   WRITE(6,*) 'ERROR: 2d: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_rh2)
+        IF ( .NOT. global_2dvar_rh2) &
+&                   WRITE(6,*) 'ERROR: 2m r: not found'
+
+
+        ! meanSea levels
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_msl)
+        IF ( .NOT. global_2dvar_msl) &
+&                   WRITE(6,*) 'ERROR: prmsl: not found'
+
+        ! sigma levels
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_tsig1)
+        IF ( .NOT. global_2dvar_tsig1) &
+&                   WRITE(6,*) 'ERROR: sigma t: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_usig1)
+        IF ( .NOT. global_2dvar_usig1) &
+&                   WRITE(6,*) 'ERROR: sigma u: not found'
+
+        fv3_all_2dvars_present = &
+&                   (fv3_all_2dvars_present .AND. global_2dvar_vsig1)
+        IF ( .NOT. global_2dvar_vsig1) &
+&                   WRITE(6,*) 'ERROR: sigma v: not found'
+
+
+
+
+
+
+!!! 2019-01-20 - These variable is currently not part of what I find in 
+!!! NCEP GRIB files at CTBTO.  I think it used to be available.  So, I'm 
+!!! commenting out the testing right now.
+        ! Low cloud layer levels
+!        ncep_all_2dvars_present = &
+!&                   (ncep_all_2dvars_present .AND. global_2dvar_tcc)
+!        IF ( .NOT. global_2dvar_tcc) &
+!&                   WRITE(6,*) 'ERROR: tcc: not found'
+
+
+        RETURN
+
+
+
+    END FUNCTION fv3_all_2dvars_present
+
+
+    LOGICAL FUNCTION ecmwf_all_levels_present()
+    IMPLICIT NONE
+
+        ! Checks logical flags for each 3d variable, testing for
+        ! all true.
+        !
+        ! For sake of "situational awareness," this routine will
+        ! go through the logical flags of each 3d array of interest,
+        ! printing error message for each one with a problem, then
+        ! returning the error code after all that.
+
+        ecmwf_all_levels_present = .TRUE.
+
+        ecmwf_all_levels_present = &
+&                   (ecmwf_all_levels_present .AND. ALL(global_levels_u))
+        IF ( .NOT. ALL(global_levels_u) ) &
+&                   WRITE(6,*) 'ERROR: u: not all levels found'
+
+        ecmwf_all_levels_present = &
+&                   (ecmwf_all_levels_present .AND. ALL(global_levels_v))
+        IF ( .NOT. ALL(global_levels_v) ) &
+&                   WRITE(6,*) 'ERROR: v: not all levels found'
+
+        ecmwf_all_levels_present = &
+&                   (ecmwf_all_levels_present .AND. ALL(global_levels_t))
+        IF ( .NOT. ALL(global_levels_t) ) &
+&                   WRITE(6,*) 'ERROR: t: not all levels found'
+
+        ecmwf_all_levels_present = &
+&                   (ecmwf_all_levels_present .AND. ALL(global_levels_q))
+        IF ( .NOT. ALL(global_levels_q) ) &
+&                   WRITE(6,*) 'ERROR: q: not all levels found'
+
+        ecmwf_all_levels_present = &
+&                   (ecmwf_all_levels_present .AND. ALL(global_levels_etadot))
+        IF ( .NOT. ALL(global_levels_etadot) ) &
+&                   WRITE(6,*) 'ERROR: etadot: not all levels found'
+
+
+    END FUNCTION ecmwf_all_levels_present
+
+
+
+    LOGICAL FUNCTION ncep_all_levels_present()
+    IMPLICIT NONE
+
+        ! Checks logical flags for each 3d variable, testing for
+        ! all true.
+        !
+        ! For sake of "situational awareness," this routine will
+        ! go through the logical flags of each 3d array of interest,
+        ! printing error message for each one with a problem, then
+        ! returning the error code after all that.
+
+        ncep_all_levels_present = .TRUE.
+
+        ncep_all_levels_present = &
+&                   (ncep_all_levels_present .AND. ALL(global_levels_u))
+        IF ( .NOT. ALL(global_levels_u) ) &
+&                   WRITE(6,*) 'ERROR: u: not all levels found'
+
+        ncep_all_levels_present = &
+&                   (ncep_all_levels_present .AND. ALL(global_levels_v))
+        IF ( .NOT. ALL(global_levels_v) ) &
+&                   WRITE(6,*) 'ERROR: v: not all levels found'
+
+        ncep_all_levels_present = &
+&                   (ncep_all_levels_present .AND. ALL(global_levels_t))
+        IF ( .NOT. ALL(global_levels_t) ) &
+&                   WRITE(6,*) 'ERROR: t: not all levels found'
+
+        ncep_all_levels_present = &
+&                   (ncep_all_levels_present .AND. ALL(global_levels_rh))
+        IF ( .NOT. ALL(global_levels_rh) ) &
+&                   WRITE(6,*) 'ERROR: r: not all levels found'
+
+        ! We can't check w like the others, because it only seems to be included for messages
+        ! with pressure levels 100 mb and higher.  So, this is checked elsewhere
+
+!        ncep_all_levels_present = &
+!&                   (ncep_all_levels_present .AND. ALL(global_levels_w))
+!        IF ( .NOT. ALL(global_levels_w) ) &
+!&                   WRITE(6,*) 'ERROR: w: not all levels found'
+
+        RETURN
+
+    END FUNCTION ncep_all_levels_present
+
+
+    LOGICAL FUNCTION fv3_all_levels_present()
+    IMPLICIT NONE
+
+        ! Checks logical flags for each 3d variable, testing for
+        ! all true.
+        !
+        ! For sake of "situational awareness," this routine will
+        ! go through the logical flags of each 3d array of interest,
+        ! printing error message for each one with a problem, then
+        ! returning the error code after all that.
+
+        fv3_all_levels_present = .TRUE.
+
+        fv3_all_levels_present = &
+&                   (fv3_all_levels_present .AND. ALL(global_levels_u))
+        IF ( .NOT. ALL(global_levels_u) ) &
+&                   WRITE(6,*) 'ERROR: u: not all levels found'
+
+        fv3_all_levels_present = &
+&                   (fv3_all_levels_present .AND. ALL(global_levels_v))
+        IF ( .NOT. ALL(global_levels_v) ) &
+&                   WRITE(6,*) 'ERROR: v: not all levels found'
+
+        fv3_all_levels_present = &
+&                   (fv3_all_levels_present .AND. ALL(global_levels_t))
+        IF ( .NOT. ALL(global_levels_t) ) &
+&                   WRITE(6,*) 'ERROR: t: not all levels found'
+
+        fv3_all_levels_present = &
+&                   (fv3_all_levels_present .AND. ALL(global_levels_rh))
+        IF ( .NOT. ALL(global_levels_rh) ) &
+&                   WRITE(6,*) 'ERROR: r: not all levels found'
+
+        ! We can't check w like the others, because it only seems to be included for messages
+        ! with pressure levels 100 mb and higher.  So, this is checked elsewhere
+
+!        ncep_all_levels_present = &
+!&                   (ncep_all_levels_present .AND. ALL(global_levels_w))
+!        IF ( .NOT. ALL(global_levels_w) ) &
+!&                   WRITE(6,*) 'ERROR: w: not all levels found'
+
+        RETURN
+
+    END FUNCTION fv3_all_levels_present
+
+
+    LOGICAL FUNCTION ncep_all_expected_w_levels_present(expected_num_levels, millibars)
+        IMPLICIT NONE
+
+
+        ! It seems like the w messages are only available at 100mb and below, so I can't check
+        ! this variable easily like I can the other 3d variables.  I need to explictly go through
+        ! each value in the millibars array and, if the value is 100 or greaters, use its index to
+        ! determine if the corresponding value in the flag array is .TRUE. or .FALSE.
+
+            
+        INTEGER, INTENT(IN) :: expected_num_levels
+        INTEGER, INTENT(IN), DIMENSION(expected_num_levels) :: millibars
+    
+
+        INTEGER :: i, plevel
+        LOGICAL :: all_present
+        
+        all_present = .TRUE.
+        DO i=1,expected_num_levels
+            plevel = millibars(i)
+            IF (plevel .GE. 100) all_present = (all_present .AND. global_levels_w(i))
+        END DO
+            
+        ncep_all_expected_w_levels_present = all_present    
+    
+        RETURN
+    
+    END FUNCTION ncep_all_expected_w_levels_present
+
+    LOGICAL FUNCTION fv3_all_expected_w_levels_present(expected_num_levels, millibars)
+        IMPLICIT NONE
+
+
+        ! It seems like the w messages are only available at 100mb and below, so I can't check
+        ! this variable easily like I can the other 3d variables.  I need to explictly go through
+        ! each value in the millibars array and, if the value is 100 or greaters, use its index to
+        ! determine if the corresponding value in the flag array is .TRUE. or .FALSE.
+
+
+        INTEGER, INTENT(IN) :: expected_num_levels
+        INTEGER, INTENT(IN), DIMENSION(expected_num_levels) :: millibars
+
+
+        INTEGER :: i, plevel
+        LOGICAL :: all_present
+
+        all_present = .TRUE.
+        DO i=1,expected_num_levels
+            plevel = millibars(i)
+            IF (plevel .GE. 100) all_present = (all_present .AND. global_levels_w(i))
+        END DO
+
+        fv3_all_expected_w_levels_present = all_present
+
+        RETURN
+
+    END FUNCTION fv3_all_expected_w_levels_present
+
+
+    
+    LOGICAL FUNCTION message_is_good(igrib, check_anl, check_fcst, check_grib2)
+        IMPLICIT NONE
+
+        ! This function is intended to drive several checks on a 
+        ! grib message and, as requirements change, this function 
+        ! may become more complex.
+        !
+        ! For now, in January 2019, it checks for message being
+        ! grib version 2, and, depending on the settings of
+        ! check_anl and check_fcst, checks whether the message
+        ! is of the specified type
+        !
+        ! August 2019 - the checking for GRIB2 message is now conditional
+        !
+        ! It is assumed that igrib refers to an already acccessed
+        ! and active grib message
+
+        INTEGER, INTENT(IN) :: igrib
+        LOGICAL, INTENT(IN) :: check_anl, check_fcst, check_grib2
+       
+        LOGICAL :: fcst_anl_good 
+
+        message_is_good = .TRUE.
+
+
+        IF (check_grib2) THEN
+            IF (.NOT. message_is_grib2(igrib)) THEN
+                WRITE(6,*) 'Grib message not GRIB2'
+                message_is_good = .FALSE.
+                RETURN
+            ENDIF
+        ENDIF
+
+        CALL anl_fcst_check(igrib, check_anl, check_fcst, fcst_anl_good)
+        IF (.NOT. fcst_anl_good) THEN
+            WRITE(6,*) 'Failed fcst/anl check'
+            message_is_good = .FALSE.
+            RETURN
+        ENDIF
+
+    END FUNCTION message_is_good
+
+
+
+    LOGICAL FUNCTION message_is_forecast(igrib)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        INTEGER, INTENT(IN) :: igrib
+        ! igrib is an id for a valid, already-fetched, grib message
+
+        CHARACTER(LEN=8) :: data_type  ! Looking for 'fc' or 'an'
+
+        message_is_forecast = .TRUE.
+        CALL grib_get(igrib, 'dataType', data_type)
+        IF (TRIM(data_type) .NE. 'fc') message_is_forecast = .FALSE.
+      
+
+        RETURN
+
+    END FUNCTION message_is_forecast
+
+    LOGICAL FUNCTION message_is_analysis(igrib)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        INTEGER, INTENT(IN) :: igrib
+        ! igrib is an id for a valid, already-fetched, grib message
+
+        CHARACTER(LEN=8) :: data_type  ! Looking for 'fc' or 'an'
+
+        message_is_analysis = .TRUE.
+        CALL grib_get(igrib, 'dataType', data_type)
+        IF (data_type .NE. 'an') message_is_analysis = .FALSE.
+      
+
+        RETURN
+
+    END FUNCTION message_is_analysis
+
+
+    SUBROUTINE anl_fcst_check(igrib, check_for_anl, check_for_fcst, result)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        ! If either (but not both) the check_for_anl or check_for_fcst 
+        ! flag is true, then check that message is good for that
+
+        INTEGER, INTENT(IN) :: igrib
+        ! igrib is an id for a valid, already-fetched, grib message
+        LOGICAL, INTENT(IN) :: check_for_anl, check_for_fcst
+        LOGICAL, INTENT(OUT) :: result
+
+        ! Only one of the two checks can be true
+        IF (check_for_anl .AND. check_for_fcst) THEN
+            WRITE(0,*) 'Only one of fcst and anl check can be requested'
+            CALL EXIT(RETURN_CODE_UTILITY_ERROR)
+        ENDIF
+
+        ! It's possible and valid for this subroutine to be called where
+        ! both flags are False.  That's OK, just set result to True.  No
+        ! request was made, so the test passes
+        IF (.NOT.(check_for_anl .OR. check_for_fcst)) THEN
+            result = .TRUE.
+            RETURN
+        ENDIF
+
+        ! OK, if we're here, exactly one of the checks is true
+        IF (check_for_anl) THEN
+            result = .TRUE.
+            IF (.NOT. message_is_analysis(igrib)) THEN
+                result = .FALSE.
+                RETURN
+            ENDIF
+        ENDIF 
+        
+        IF (check_for_fcst) THEN
+            result = .TRUE.
+            IF (.NOT. message_is_forecast(igrib)) THEN
+                result = .FALSE.
+                RETURN
+            ENDIF
+        ENDIF 
+        
+
+    END SUBROUTINE anl_fcst_check
+
+
+
+
+
+    LOGICAL FUNCTION message_is_grib2(igrib)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        INTEGER, INTENT(IN) :: igrib
+        ! igrib is an id for a valid, already-fetched, grib message
+
+        INTEGER :: edition_number
+         
+        message_is_grib2 = .TRUE.
+        CALL grib_get(igrib, 'editionNumber', edition_number)
+        IF (edition_number .NE. 2) message_is_grib2 = .FALSE.
+      
+
+        RETURN
+    END FUNCTION message_is_grib2
+   
+
+
+    SUBROUTINE build_millibars_array(ifile, expected_num_levels,  &
+&                                    millibars, return_code)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        INTEGER, INTENT(IN) :: ifile, expected_num_levels
+        INTEGER, INTENT(OUT), DIMENSION(expected_num_levels) :: millibars
+        INTEGER, INTENT(OUT) :: return_code
+
+
+
+        ! This routine is for the old GFS (pre-FV3) GRIB files, where it is assumed
+        ! that all isobaric-level variables will have the same number of levels.
+        !
+        ! Goes through each message in the already-open GRIBFILE
+        ! indicated by "ifile" and creates a sorted array of all of the
+        ! pressures found in isobaric level types.  When we're done,
+        ! we have an inventory of all of the pressure levels expected by
+        ! 3d isobaric-level variables.
+        !
+        ! Note that the returned millibars array is NOT sorted.  I can't
+        ! think of a reason right now why that would be necessary.
+        !
+        ! If, in creating the inventory, we end up with fewer or more 
+        ! levels than expected by "expected_num_levels" we return with
+        ! an error 
+
+        INTEGER :: level_count    ! Keep track of number of new levels found
+        LOGICAL :: end_of_file
+        INTEGER :: igrib, iret
+
+        CHARACTER(LEN=GRIB_ATTRIBUTE_STR_LEN) :: gribmsg_leveltype, &
+&                                                gribmsg_shortname
+        INTEGER :: gribmsg_level
+
+
+
+        return_code = RETURN_CODE_NORMAL
+        level_count = 0 
+
+        millibars = -999
+
+        !PRINT *, 'millibars: ', millibars
+
+        ! Iterate through the entire file, keeping track of what we've found
+        end_of_file = .FALSE.
+        DO WHILE (.NOT. end_of_file)
+            CALL grib_new_from_file(ifile, igrib, iret)
+
+
+            IF (iret .eq. GRIB_END_OF_FILE) THEN
+                end_of_file = .TRUE.
+            ELSE
+                CALL grib_get(igrib, 'shortName', gribmsg_shortname)
+                CALL grib_get(igrib, 'typeOfLevel', gribmsg_leveltype)
+                CALL grib_get(igrib, 'level', gribmsg_level)
+
+                ! We're only interested in the isobaric level messages
+                IF ( TRIM(gribmsg_leveltype) == 'isobaricInhPa' ) THEN
+
+
+                    CALL grib_get(igrib, 'level', gribmsg_level)
+                    !PRINT *, 'level: ', gribmsg_level
+
+
+                    ! If this level is not already in the array, update
+                    ! the counter (make sure we haven't exceeded expected
+                    ! number of levels), then add to array.
+                    IF (myfindloc(expected_num_levels, &
+&                                     millibars, VALUE=gribmsg_level) .EQ. 0) THEN
+                        level_count = level_count + 1
+                        IF (level_count .LE. expected_num_levels) THEN
+                            millibars(level_count) = gribmsg_level
+                        ELSE
+                            WRITE(6,"('ERROR: Found more than ', I3, ' levels')") &
+&                                     expected_num_levels
+                            return_code = RETURN_CODE_GRIBFILE_ERROR
+                            RETURN
+                        ENDIF
+
+                    ENDIF
+
+
+                ENDIF
+            END IF
+
+
+        END DO   ! End of main loop for reading each GRIB message
+
+        ! Make sure we found the number if isobaric levels that we expected
+        IF (level_count .LT. expected_num_levels) THEN
+            WRITE(6,"('ERROR: Only found  ', I3, ' levels')") level_count
+           return_code = RETURN_CODE_GRIBFILE_ERROR
+           RETURN
+        ENDIF
+
+        ! OK, if we made it here, we should have a full millibars array
+        ! containing all of the isobaric levels we found.  NOTE THAT I
+        ! AM NOT BOTHERING TO SORT THIS - I DON'T SEE A REASON FOR THAT
+        ! SO FAR.
+
+        RETURN
+
+        
+    END SUBROUTINE build_millibars_array    
+
+
+
+
+
+
+    SUBROUTINE build_fv3_millibars_array(ifile, expected_num_levels,  &
+&                                    millibars, return_code)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        INTEGER, INTENT(IN) :: ifile, expected_num_levels
+        INTEGER, INTENT(OUT), DIMENSION(expected_num_levels) :: millibars
+        INTEGER, INTENT(OUT) :: return_code
+
+        ! Goes through each message in the already-open GRIBFILE
+        ! indicated by "ifile" and creates a sorted array of all of the
+        ! pressures found in isobaric level types.  When we're done,
+        ! we have an inventory of all of the pressure levels expected by
+        ! 3d isobaric-level variables.
+        !
+        ! Note that the returned millibars array is NOT sorted.  I can't
+        ! think of a reason right now why that would be necessary.
+        !
+        ! If, in creating the inventory, we end up with fewer or more
+        ! levels than expected by "expected_num_levels" we return with
+        ! an error
+        !
+        ! MODIFICATION - DJM - 2019-07-27 - New NCEP FV3 files have additional
+        ! pressure levels at 15 mb and 40 mb, but the variables at those
+        ! levels are not exactly those at the more standard levels.  The
+        ! logic of this routine previously added these levels to the list of
+        ! millibars, but then other routines would fail because they wouldn't
+        ! find expected variables at these levels.  So, I have modified this
+        ! routine to simple ignore pressure levels of 15 and 40 millibars.
+        ! This isn't necessarily the most robust way to do things, but it is
+        ! what it is right now.
+
+        INTEGER :: level_count    ! Keep track of number of new levels found
+        LOGICAL :: end_of_file
+        INTEGER :: igrib, iret
+
+        CHARACTER(LEN=GRIB_ATTRIBUTE_STR_LEN) :: gribmsg_leveltype, &
+&                                                gribmsg_shortname
+        INTEGER :: gribmsg_level
+
+
+
+        return_code = RETURN_CODE_NORMAL
+        level_count = 0
+
+        millibars = -999
+
+        !PRINT *, 'millibars: ', millibars
+
+        ! Iterate through the entire file, keeping track of what we've found
+        end_of_file = .FALSE.
+        DO WHILE (.NOT. end_of_file)
+            CALL grib_new_from_file(ifile, igrib, iret)
+
+
+            IF (iret .eq. GRIB_END_OF_FILE) THEN
+                end_of_file = .TRUE.
+            ELSE
+                CALL grib_get(igrib, 'shortName', gribmsg_shortname)
+                CALL grib_get(igrib, 'typeOfLevel', gribmsg_leveltype)
+                CALL grib_get(igrib, 'level', gribmsg_level)
+
+                ! We're only interested in the isobaric level messages
+                IF ( TRIM(gribmsg_leveltype) == 'isobaricInhPa' ) THEN
+
+
+                    CALL grib_get(igrib, 'level', gribmsg_level)
+                    !PRINT *, 'level: ', gribmsg_level
+
+                    IF ( (gribmsg_level .EQ. 15) .OR. (gribmsg_level .EQ. 40) ) THEN
+                        ! Bypass this message - we are ignoring 15 and 40 mb
+                        ! pressure levels.  See comments above
+                        CONTINUE
+                    ELSE
+
+                        ! If this level is not already in the array, update
+                        ! the counter (make sure we haven't exceeded expected
+                        ! number of levels), then add to array.
+                        IF (myfindloc(expected_num_levels, &
+&                                     millibars, VALUE=gribmsg_level) .EQ. 0) THEN
+                            level_count = level_count + 1
+                            IF (level_count .LE. expected_num_levels) THEN
+                                millibars(level_count) = gribmsg_level
+                            ELSE
+                                WRITE(6,"('ERROR: Found more than ', I3, ' levels')") &
+&                                     expected_num_levels
+                                return_code = RETURN_CODE_GRIBFILE_ERROR
+                                RETURN
+                            ENDIF
+
+                        ENDIF
+                    ENDIF
+
+
+
+
+                ENDIF
+            END IF
+
+
+        END DO   ! End of main loop for reading each GRIB message
+
+        ! Make sure we found the number of isobaric levels that we expected
+        IF (level_count .LT. expected_num_levels) THEN
+            WRITE(6,"('ERROR: Only found  ', I3, ' levels')") level_count
+           return_code = RETURN_CODE_GRIBFILE_ERROR
+           RETURN
+        ENDIF
+
+        ! OK, if we made it here, we should have a full millibars array
+        ! containing all of the isobaric levels we found.  NOTE THAT I
+        ! AM NOT BOTHERING TO SORT THIS - I DON'T SEE A REASON FOR THAT
+        ! SO FAR.
+
+        RETURN
+
+
+    END SUBROUTINE build_fv3_millibars_array
+
+
+
+
+
+
+
+    INTEGER FUNCTION myfindloc(n, array, value)
+
+
+        INTEGER, INTENT(IN) :: n
+        INTEGER, INTENT(IN), DIMENSION(n) :: array
+        INTEGER, INTENT(IN) :: value 
+ 
+        ! My own implementation of FINDLOC, which is part of the 
+        ! Fortran 2008 standard
+
+        ! Returns index of first instance of "value" in the array.  Returns
+        ! zero if not found.  Assumes single-dimension INTEGER array 
+
+        ! Default value
+
+        INTEGER :: i
+        LOGICAL :: found
+
+        found = .FALSE.
+        myfindloc = 0
+        DO i=1,SIZE(array)
+            IF (array(i) .EQ. value) THEN
+                myfindloc = i
+                EXIT
+            ENDIF
+        END DO
+        
+        RETURN
+         
+
+
+    END FUNCTION myfindloc
+
+
+
+
+
+    SUBROUTINE write_err(shortname, level)
+
+        IMPLICIT NONE
+
+        CHARACTER(LEN=GRIB_ATTRIBUTE_STR_LEN), INTENT(IN) :: shortname 
+        INTEGER, INTENT(IN) :: level
+
+        WRITE(6,*) 'ERROR: Bad gribmsg_shortname: ', TRIM(shortname),&
+&                  ', gribmsg_level: ', level
+
+    END SUBROUTINE write_err
+
+
+    INTEGER FUNCTION cgutils_check_ecmwf_grib(filepath, expected_num_levels, &
+&                                              check_fcst, check_anl, check_grib2) &
+&                                      RESULT(return_code)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        CHARACTER(LEN=256), INTENT(IN) :: filepath
+        INTEGER, INTENT(IN) :: expected_num_levels
+        LOGICAL, INTENT(IN) :: check_fcst, check_anl, check_grib2
+
+        ! return_code: possible values defined in module header, RETURN_CODE_*
+
+        INTEGER :: ifile, igrib, iret, grib_centre
+
+        CHARACTER(LEN=GRIB_ATTRIBUTE_STR_LEN) :: gribmsg_leveltype, &
+&                                                gribmsg_shortname
+        INTEGER :: gribmsg_level
+
+        LOGICAL :: end_of_file
+        !!LOGICAL :: fcst_anl_good
+
+
+        ! Start with return_code 0.  Anything wrong will change it
+        return_code = RETURN_CODE_NORMAL
+
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+        IF (iret == 0) THEN
+            ! Use first record to detect centre, which is assumed constant
+            ! amongst all messages
+            CALL grib_new_from_file(ifile, igrib, iret)
+            CALL grib_get(igrib, 'centre', grib_centre)
+            CALL grib_close_file(ifile)
+        ELSE
+            WRITE(0,*) 'ERROR: problem opening GRIB file: '//TRIM(filepath)
+            return_code = RETURN_CODE_UTILITY_ERROR
+            RETURN
+        END IF
+
+
+        IF (grib_centre .NE. GRIB_CENTRE_ECMWF) THEN
+            WRITE(6,*) 'ERROR: Unexpected grib_centre: ', grib_centre
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF
+
+
+        CALL allocate_and_init_flags(expected_num_levels)
+
+        ! Iterate through the entire file, keeping track of what we've found
+        end_of_file = .FALSE.
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+
+        DO WHILE (.NOT. end_of_file)
+            CALL grib_new_from_file(ifile, igrib, iret)
+            IF (iret .eq. GRIB_END_OF_FILE) THEN
+                end_of_file = .TRUE.
+            ELSE
+
+              
+                CALL grib_get(igrib, 'shortName', gribmsg_shortname)
+                CALL grib_get(igrib, 'typeOfLevel', gribmsg_leveltype)
+                CALL grib_get(igrib, 'level', gribmsg_level)
+
+                ! Check general attributes of this message.  Note that
+                ! all messages will be checked, even those we might not
+                ! care about.  As of 2019-01-17, the checks are for GRIB2
+                ! and for fcst/anl if specified by "check_anl" or "check_fcst"
+                IF (.NOT. message_is_good(igrib, &
+&                             check_anl, check_fcst, check_grib2)) THEN
+                    CALL write_err(gribmsg_shortname, gribmsg_level)
+                    return_code = RETURN_CODE_GRIBFILE_ERROR
+                    RETURN
+                ENDIF
+
+                ! Filter for 3D variables of interest
+                IF ( TRIM(gribmsg_leveltype) == 'hybrid' ) THEN
+
+
+                    CALL grib_get(igrib, 'level', gribmsg_level)
+
+                    ! Check that there aren't more levels than expected
+                    IF (gribmsg_level .GT. expected_num_levels) THEN
+                        WRITE(6,*) 'ERROR: gribmsg_shortname: ', &
+&                                  TRIM(gribmsg_shortname), &
+&                                  ', gribmsg_level: ', gribmsg_level, &
+&                                  ', expected max levels: ', &
+&                                  expected_num_levels
+                        return_code = RETURN_CODE_GRIBFILE_ERROR
+                        RETURN
+                    ENDIF
+
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('u')
+                            global_levels_u(gribmsg_level) = .TRUE.
+                        CASE('v')
+                            global_levels_v(gribmsg_level) = .TRUE.
+                        CASE('t')
+                            global_levels_t(gribmsg_level) = .TRUE.
+                        CASE('q')
+                            global_levels_q(gribmsg_level) = .TRUE.
+                        CASE('etadot')
+                            global_levels_etadot(gribmsg_level) = .TRUE.
+                    END SELECT
+
+
+                !!!!!!   ! End of section for 3d variables of interest
+
+                ! Filter for 2d variables of interest with level type
+                ! of "surface"
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'surface' ) THEN
+
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('sp')
+                            global_2dvar_ps = .TRUE.
+
+                        ! For snow depth, I've been seeing both 'sd' and
+                        ! 'sde' used in the CTBTO ECMWF files
+                        CASE('sd')
+                            global_2dvar_sd = .TRUE.
+                        CASE('sde')     
+                            global_2dvar_sd = .TRUE.
+
+                        CASE('tcc')
+                            global_2dvar_tcc = .TRUE.
+                        CASE('lsp')
+                            global_2dvar_lsprec = .TRUE.
+                        CASE('acpcp')
+                            global_2dvar_convprec = .TRUE.
+                        CASE('sshf')
+                            global_2dvar_shf = .TRUE.
+                        CASE('ssr')
+                            global_2dvar_sr = .TRUE.
+                        CASE('ewss')
+                            global_2dvar_ewss = .TRUE.
+                        CASE('nsss')
+                            global_2dvar_nsss = .TRUE.
+                        CASE('z')
+                            global_2dvar_oro = .TRUE.
+                        CASE('sdor')
+                            global_2dvar_excessoro = .TRUE.
+                        CASE('lsm')
+                            global_2dvar_lsm = .TRUE.
+
+
+                        ! GRIB 1 variables found in the EFYYMMDDHH files
+                        ! Note that these are also found in GRIB2 files
+                        ! under different level types, except for cp, which
+                        ! is acpcp in GRIB2.  If there is ever a mixture
+                        ! (which I think is unlikely) in a GRIB file, then
+                        ! the world will likely end and this won't work
+                        ! correctly.
+                        CASE('cp')
+                            global_2dvar_convprec = .TRUE.
+                        CASE('10u')
+                            global_2dvar_u10 = .TRUE.
+                        CASE('10v')
+                            global_2dvar_v10 = .TRUE.
+                        CASE('2t')
+                            global_2dvar_t2 = .TRUE.
+                        CASE('2d')
+                            global_2dvar_td2 = .TRUE.
+                        CASE('msl')
+                            global_2dvar_msl = .TRUE.
+
+               
+
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "surface" variables of interest
+
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'heightAboveGround' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('10u')
+                            global_2dvar_u10 = .TRUE.
+                        CASE('10v')
+                            global_2dvar_v10 = .TRUE.
+                        CASE('2t')
+                            global_2dvar_t2 = .TRUE.
+                        CASE('2d')
+                            global_2dvar_td2 = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "heightAboveGround" 
+                !!!!!!!! variables of interest
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'meanSea' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('msl')
+                            global_2dvar_msl = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "meanSea" 
+                !!!!!!!! variables of interest
+
+
+                END IF ! Selection of message types (3d, surface, etc.)
+
+
+            END IF
+
+
+
+
+        END DO   ! End of main loop for reading each GRIB message
+        CALL grib_close_file(ifile)
+
+        ! This function will have the side effect of printing any
+        ! problems to stdout.  If false, we force exit
+        IF (ecmwf_all_levels_present()) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            CALL delete_levels_flags
+            RETURN
+        ENDIF
+
+        IF (ecmwf_all_2dvars_present()) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF
+
+
+        RETURN
+
+    END FUNCTION cgutils_check_ecmwf_grib
+
+
+
+    INTEGER FUNCTION cgutils_check_ncep_grib2(filepath, expected_num_levels, &
+&                                              check_fcst, check_anl, check_grib2) &
+&                                      RESULT(return_code)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        CHARACTER(LEN=256), INTENT(IN) :: filepath
+        INTEGER, INTENT(IN) :: expected_num_levels
+        LOGICAL, INTENT(IN) :: check_fcst, check_anl, check_grib2
+
+        ! return_code: possible values defined in module header, RETURN_CODE_*
+
+        INTEGER :: ifile, igrib, iret, grib_centre
+
+        CHARACTER(LEN=GRIB_ATTRIBUTE_STR_LEN) :: gribmsg_leveltype, &
+&                                                gribmsg_shortname
+        INTEGER :: gribmsg_level, plevel_idx
+
+        LOGICAL :: end_of_file
+
+        INTEGER, ALLOCATABLE, DIMENSION(:) :: millibars
+
+
+        ! Start with return_code 0.  Anything wrong will change it
+        return_code = RETURN_CODE_NORMAL
+
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+        IF (iret == 0) THEN
+            ! Use first record to detect centre, which is assumed constant
+            ! amongst all messages
+            CALL grib_new_from_file(ifile, igrib, iret)
+            CALL grib_get(igrib, 'centre', grib_centre)
+            CALL grib_close_file(ifile)
+        ELSE
+            WRITE(0,*) 'ERROR: problem opening GRIB file: '//TRIM(filepath)
+            return_code = RETURN_CODE_UTILITY_ERROR
+            RETURN
+        END IF
+
+
+        IF (grib_centre .NE. GRIB_CENTRE_NCEP) THEN
+            WRITE(6,*) 'ERROR: Unexpected grib_centre: ', grib_centre
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF
+
+
+
+        ! Pass through the file once in order to build an inventory of
+        ! isobaric pressure levels so that later we can make sure that
+        ! each 3D variable has each of these pressure levels.  While
+        ! we're doing the inventory we can check that the number of
+        ! isobaric levels found is indeed what we expect.
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+        IF (iret == 0) THEN
+            ALLOCATE(millibars(expected_num_levels))
+            CALL build_millibars_array(ifile, expected_num_levels,  &
+&                                      millibars, return_code)
+            CALL grib_close_file(ifile)
+        ELSE
+            WRITE(0,*) 'ERROR: problem opening GRIB file: '//TRIM(filepath)
+            return_code = RETURN_CODE_UTILITY_ERROR
+            RETURN
+        END IF
+
+        ! Check that build_millibars_array() had no errors
+        ! We will assume that any error messages were written from the
+        ! subroutine 
+        IF (return_code .NE. RETURN_CODE_NORMAL) THEN
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF 
+
+        ! At this point, millibars should be an array of all the pressure
+        ! levels (isobaricInhPa) that we found.  
+        !PRINT *, 'millibars: ', millibars
+
+        CALL allocate_and_init_flags(expected_num_levels)
+
+
+        ! Now, make our primary checking pass through the GRIB file. 
+
+        ! For 3D isobaric-level variables, as we read a level, we will
+        ! find its index in millibars array, and use that to index the
+        ! correct flag element in our logical array for each 3D variable
+
+        ! Iterate through the entire file, keeping track of what we've found
+        end_of_file = .FALSE.
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+
+        DO WHILE (.NOT. end_of_file)
+            CALL grib_new_from_file(ifile, igrib, iret)
+            IF (iret .eq. GRIB_END_OF_FILE) THEN
+                end_of_file = .TRUE.
+            ELSE
+
+              
+                CALL grib_get(igrib, 'shortName', gribmsg_shortname)
+                CALL grib_get(igrib, 'typeOfLevel', gribmsg_leveltype)
+                CALL grib_get(igrib, 'level', gribmsg_level)
+
+                ! Check general attributes of this message.  Note that
+                ! all messages will be checked, even those we might not
+                ! care about.  As of 2019-01-17, the checks are for GRIB2
+                ! and for fcst/anl if specified by "check_anl" or "check_fcst"
+                IF (.NOT. message_is_good(igrib, &
+&                             check_anl, check_fcst, check_grib2)) THEN
+                    CALL write_err(gribmsg_shortname, gribmsg_level)
+                    return_code = RETURN_CODE_GRIBFILE_ERROR
+                    RETURN
+                ENDIF
+                
+                
+                
+                
+                
+                
+                
+                    !PRINT *, 'shortname, leveltype, level, plevel_idx: ', &
+                    !&        gribmsg_shortname, gribmsg_leveltype, &
+                    !&        gribmsg_level, plevel_idx
+                    
+                    
+                    
+                    
+                    
+                    
+                ! Filter for 3D variables of interest
+                IF ( TRIM(gribmsg_leveltype) == 'isobaricInhPa' ) THEN
+
+
+                    CALL grib_get(igrib, 'level', gribmsg_level)
+
+                    ! Unsorted array "millibars," created above, tells us what index we
+                    ! should use to access the retrieved level flag as we maintain an
+                    ! inventory of what we found, and what we didn't
+                    ! Use my function myfindloc() to find the index for a specified pressure
+                    ! level.
+
+                    plevel_idx = myfindloc(expected_num_levels, millibars, gribmsg_level)
+                    IF (plevel_idx .LE. 0) THEN
+                        CALL write_err(gribmsg_shortname, gribmsg_level)
+                        return_code = RETURN_CODE_GRIBFILE_ERROR
+                        RETURN
+                    ENDIF
+
+
+                    !PRINT *, 'shortname, leveltype, level, plevel_idx: ', &
+                    !&        gribmsg_shortname, gribmsg_leveltype, &
+                    !&        gribmsg_level, plevel_idx
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('u')
+                            global_levels_u(plevel_idx) = .TRUE.
+                        CASE('v')
+                            global_levels_v(plevel_idx) = .TRUE.
+                        CASE('t')
+                            global_levels_t(plevel_idx) = .TRUE.
+                        CASE('r')
+                            global_levels_rh(plevel_idx) = .TRUE.
+                        CASE('w')
+                            global_levels_w(plevel_idx) = .TRUE.
+                    END SELECT
+
+                !!!!!!   ! End of section for 3d variables of interest
+
+
+
+
+
+                ! Filter for 2d variables of interest with level type
+                ! of "surface"
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'surface' ) THEN
+
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('sp')
+                            global_2dvar_ps = .TRUE.
+                        CASE('sdwe')     
+                            global_2dvar_sd = .TRUE.
+                        CASE('tp')
+                            global_2dvar_lsprec = .TRUE.
+                        CASE('acpcp')
+                            global_2dvar_convprec = .TRUE.                            
+                        CASE('orog')
+                            global_2dvar_oro = .TRUE.                            
+                        CASE('lsm')
+                            global_2dvar_lsm = .TRUE.                            
+                        CASE('hpbl')
+                            global_2dvar_hmix = .TRUE.          
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "surface" variables of interest
+
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'heightAboveGround' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('10u')
+                            global_2dvar_u10 = .TRUE.
+                        CASE('10v')
+                            global_2dvar_v10 = .TRUE.
+                        CASE('2t')
+                            global_2dvar_t2 = .TRUE.
+                        CASE('2d')
+                            global_2dvar_td2 = .TRUE.
+                            
+                        ! 2m RH is different depending on version of gribapi - in 1.14 it's just r, so
+                        ! one needs to also make sure that level is 2, but in later gribapi it seems to be
+                        ! 2r
+                        CASE('r')
+                            IF (gribmsg_level .EQ. 2) global_2dvar_rh2 = .TRUE.
+                        CASE('2r')
+                            global_2dvar_rh2 = .TRUE.
+                    END SELECT
+
+                !!!!!!!! End of section for 2d "heightAboveGround" 
+                !!!!!!!! variables of interest
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'meanSea' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('prmsl')
+                            global_2dvar_msl = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "meanSea" 
+                !!!!!!!! variables of interest
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'Low cloud layer' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('tcc')
+                            global_2dvar_tcc = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "Low cloud layer" 
+                !!!!!!!! variables of interest
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'sigma' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('t')
+                            global_2dvar_tsig1 = .TRUE.
+                        CASE('u')
+                            global_2dvar_usig1 = .TRUE.
+                        CASE('v')
+                            global_2dvar_vsig1 = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "sigma" 
+                !!!!!!!! variables of interest
+
+
+
+
+
+
+
+                END IF ! Selection of message types (3d, surface, etc.)
+
+
+            END IF
+
+
+
+
+        END DO   ! End of main loop for reading each GRIB message
+        CALL grib_close_file(ifile)
+
+
+
+
+
+        ! Check that all the 3d levels we flagged in the above loop resulted
+        ! in all flags being true
+        
+        ! This function will have the side effect of printing any
+        ! problems to stdout.  If false, we force exit
+        IF (ncep_all_levels_present()) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            CALL delete_levels_flags
+            RETURN
+        ENDIF
+
+        ! The w (vertical wind) variable has to be handled differently because it
+        ! is only available in the NCEP files for 100 mb and higher pressure levels
+        IF (ncep_all_expected_w_levels_present(expected_num_levels, &
+&                                              millibars)) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            WRITE(6,*) 'ERROR: Not all expected w levels are present'
+            CALL delete_levels_flags
+            RETURN
+        ENDIF
+
+
+
+        IF (ncep_all_2dvars_present()) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF
+
+
+        ! Be sure to deallocate our allocatable arrays
+        DEALLOCATE(millibars)
+        CALL delete_levels_flags
+
+
+
+
+        RETURN
+
+    END FUNCTION cgutils_check_ncep_grib2
+
+
+    INTEGER FUNCTION cgutils_check_fv3_grib2(filepath, expected_num_levels, &
+&                                              check_fcst, check_anl, check_grib2) &
+&                                      RESULT(return_code)
+
+        USE grib_api
+
+        IMPLICIT NONE
+
+        CHARACTER(LEN=256), INTENT(IN) :: filepath
+        INTEGER, INTENT(IN) :: expected_num_levels
+        LOGICAL, INTENT(IN) :: check_fcst, check_anl, check_grib2
+
+        ! return_code: possible values defined in module header, RETURN_CODE_*
+
+        INTEGER :: ifile, igrib, iret, grib_centre
+
+        CHARACTER(LEN=GRIB_ATTRIBUTE_STR_LEN) :: gribmsg_leveltype, &
+&                                                gribmsg_shortname
+        INTEGER :: gribmsg_level, plevel_idx
+
+        LOGICAL :: end_of_file
+
+        INTEGER, ALLOCATABLE, DIMENSION(:) :: millibars
+
+
+        ! Start with return_code 0.  Anything wrong will change it
+        return_code = RETURN_CODE_NORMAL
+
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+        IF (iret == 0) THEN
+            ! Use first record to detect centre, which is assumed constant
+            ! amongst all messages
+            CALL grib_new_from_file(ifile, igrib, iret)
+            CALL grib_get(igrib, 'centre', grib_centre)
+            CALL grib_close_file(ifile)
+        ELSE
+            WRITE(0,*) 'ERROR: problem opening GRIB file: '//TRIM(filepath)
+            return_code = RETURN_CODE_UTILITY_ERROR
+            RETURN
+        END IF
+
+
+        IF (grib_centre .NE. GRIB_CENTRE_NCEP) THEN
+            WRITE(6,*) 'ERROR: Unexpected grib_centre: ', grib_centre
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF
+
+
+
+        ! Pass through the file once in order to build an inventory of
+        ! isobaric pressure levels so that later we can make sure that
+        ! each 3D variable has each of these pressure levels.  While
+        ! we're doing the inventory we can check that the number of
+        ! isobaric levels found is indeed what we expect.
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+        IF (iret == 0) THEN
+            ALLOCATE(millibars(expected_num_levels))
+            CALL build_fv3_millibars_array(ifile, expected_num_levels,  &
+&                                      millibars, return_code)
+            CALL grib_close_file(ifile)
+        ELSE
+            WRITE(0,*) 'ERROR: problem opening GRIB file: '//TRIM(filepath)
+            return_code = RETURN_CODE_UTILITY_ERROR
+            RETURN
+        END IF
+
+        ! Check that build_millibars_array() had no errors
+        ! We will assume that any error messages were written from the
+        ! subroutine 
+        IF (return_code .NE. RETURN_CODE_NORMAL) THEN
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF
+
+        ! At this point, millibars should be an array of all the pressure
+        ! levels (isobaricInhPa) that we found.  
+        !PRINT *, 'millibars: ', millibars
+
+        CALL allocate_and_init_flags(expected_num_levels)
+
+
+        ! Now, make our primary checking pass through the GRIB file. 
+
+        ! For 3D isobaric-level variables, as we read a level, we will
+        ! find its index in millibars array, and use that to index the
+        ! correct flag element in our logical array for each 3D variable
+
+        ! Iterate through the entire file, keeping track of what we've found
+        end_of_file = .FALSE.
+        CALL grib_open_file(ifile, filepath, 'r', iret)
+
+        DO WHILE (.NOT. end_of_file)
+            CALL grib_new_from_file(ifile, igrib, iret)
+            IF (iret .eq. GRIB_END_OF_FILE) THEN
+                end_of_file = .TRUE.
+            ELSE
+
+
+                CALL grib_get(igrib, 'shortName', gribmsg_shortname)
+                CALL grib_get(igrib, 'typeOfLevel', gribmsg_leveltype)
+                CALL grib_get(igrib, 'level', gribmsg_level)
+
+                ! Check general attributes of this message.  Note that
+                ! all messages will be checked, even those we might not
+                ! care about.  As of 2019-01-17, the checks are for GRIB2
+                ! and for fcst/anl if specified by "check_anl" or "check_fcst"
+                IF (.NOT. message_is_good(igrib, &
+&                             check_anl, check_fcst, check_grib2)) THEN
+                    CALL write_err(gribmsg_shortname, gribmsg_level)
+                    return_code = RETURN_CODE_GRIBFILE_ERROR
+                    RETURN
+                ENDIF
+
+
+                    !PRINT *, 'shortname, leveltype, level, plevel_idx: ', &
+                    !&        gribmsg_shortname, gribmsg_leveltype, &
+                    !&        gribmsg_level, plevel_idx
+
+
+                ! Filter for 3D variables of interest
+                IF ( TRIM(gribmsg_leveltype) == 'isobaricInhPa' ) THEN
+
+
+                    CALL grib_get(igrib, 'level', gribmsg_level)
+
+                    ! Unsorted array "millibars," created above, tells us what index we
+                    ! should use to access the retrieved level flag as we maintain an
+                    ! inventory of what we found, and what we didn't
+                    ! Use my function myfindloc() to find the index for a specified pressure
+                    ! level.
+
+
+
+                    ! This is annoyingly "special case" code. Because of new FV3 files, "partial"
+                    ! levels are present in the GRIB files.  Other routines compute the
+                    ! list of "valid" pressure levels, but as we go through and process
+                    ! each message, as we're doing here, if it's on one of the invalid
+                    ! pressure levels then we simply want to ignore it.  If it's a valid
+                    ! pressure level, and it's a variable we're interested in, we mark it
+                    ! as present on that pressure level
+
+                    plevel_idx = myfindloc(expected_num_levels, millibars, gribmsg_level)
+                    IF (plevel_idx .LE. 0) THEN
+                        !CALL write_err(gribmsg_shortname, gribmsg_level)
+                        !return_code = RETURN_CODE_GRIBFILE_ERROR
+                        !RETURN
+                        CONTINUE
+                    ELSE
+
+
+                        !PRINT *, 'shortname, leveltype, level, plevel_idx: ', &
+                        !&        gribmsg_shortname, gribmsg_leveltype, &
+                        !&        gribmsg_level, plevel_idx
+
+                        SELECT CASE(gribmsg_shortname)
+                            CASE('u')
+                                global_levels_u(plevel_idx) = .TRUE.
+                            CASE('v')
+                                global_levels_v(plevel_idx) = .TRUE.
+                            CASE('t')
+                                global_levels_t(plevel_idx) = .TRUE.
+                            CASE('r')
+                                global_levels_rh(plevel_idx) = .TRUE.
+                            CASE('w')
+                                global_levels_w(plevel_idx) = .TRUE.
+                        END SELECT
+                    ENDIF
+
+                !!!!!!   ! End of section for 3d variables of interest
+
+
+
+
+
+                ! Filter for 2d variables of interest with level type
+                ! of "surface"
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'surface' ) THEN
+
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('sp')
+                            global_2dvar_ps = .TRUE.
+                        CASE('sdwe')
+                            global_2dvar_sd = .TRUE.
+                        CASE('tp')
+                            global_2dvar_lsprec = .TRUE.
+                        CASE('acpcp')
+                            global_2dvar_convprec = .TRUE.                       
+                        CASE('orog')
+                            global_2dvar_oro = .TRUE.                            
+                        CASE('lsm')
+                            global_2dvar_lsm = .TRUE.                            
+                        CASE('hpbl')
+                            global_2dvar_hmix = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "surface" variables of interest
+
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'heightAboveGround' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('10u')
+                            global_2dvar_u10 = .TRUE.
+                        CASE('10v')
+                            global_2dvar_v10 = .TRUE.
+                        CASE('2t')
+                            global_2dvar_t2 = .TRUE.
+                        CASE('2d')
+                            global_2dvar_td2 = .TRUE.
+
+                        ! 2m RH is different depending on version of gribapi - in 1.14 it's just r, so
+                        ! one needs to also make sure that level is 2, but in later gribapi it seems to be
+                        ! 2r
+                        CASE('r')
+                            IF (gribmsg_level .EQ. 2) global_2dvar_rh2 = .TRUE.
+                        CASE('2r')
+                            global_2dvar_rh2 = .TRUE.
+                    END SELECT
+
+                !!!!!!!! End of section for 2d "heightAboveGround" 
+                !!!!!!!! variables of interest
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'meanSea' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('prmsl')
+                            global_2dvar_msl = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "meanSea" 
+                !!!!!!!! variables of interest
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'Low cloud layer' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('tcc')
+                            global_2dvar_tcc = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "Low cloud layer" 
+                !!!!!!!! variables of interest
+
+                ELSE IF ( TRIM(gribmsg_leveltype) == 'sigma' ) THEN
+
+                    SELECT CASE(gribmsg_shortname)
+                        CASE('t')
+                            global_2dvar_tsig1 = .TRUE.
+                        CASE('u')
+                            global_2dvar_usig1 = .TRUE.
+                        CASE('v')
+                            global_2dvar_vsig1 = .TRUE.
+                    END SELECT
+
+
+                !!!!!!!! End of section for 2d "sigma" 
+                !!!!!!!! variables of interest
+
+
+                END IF ! Selection of message types (3d, surface, etc.)
+
+
+            END IF
+
+
+
+
+        END DO   ! End of main loop for reading each GRIB message
+        CALL grib_close_file(ifile)
+
+
+
+
+
+        ! Check that all the 3d levels we flagged in the above loop resulted
+        ! in all flags being true
+
+        ! This function will have the side effect of printing any
+        ! problems to stdout.  If false, we force exit
+        IF (ncep_all_levels_present()) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            CALL delete_levels_flags
+            RETURN
+        ENDIF
+
+        ! The w (vertical wind) variable has to be handled differently because it
+        ! is only available in the NCEP files for 100 mb and higher pressure levels
+        IF (fv3_all_expected_w_levels_present(expected_num_levels, &
+&                                              millibars)) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            WRITE(6,*) 'ERROR: Not all expected w levels are present'
+            CALL delete_levels_flags
+            RETURN
+        ENDIF
+
+
+
+        IF (fv3_all_2dvars_present()) THEN
+            return_code = RETURN_CODE_NORMAL
+        ELSE
+            return_code = RETURN_CODE_GRIBFILE_ERROR
+            RETURN
+        ENDIF
+
+
+        ! Be sure to deallocate our allocatable arrays
+        DEALLOCATE(millibars)
+        CALL delete_levels_flags
+
+
+
+
+        RETURN
+
+    END FUNCTION cgutils_check_fv3_grib2
+
+
+
+
+
+END MODULE cgutils
+
Index: flexpart_code/checkgrib/checkGRIB.F90
===================================================================
--- flexpart_code/checkgrib/checkGRIB.F90	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
+++ flexpart_code/checkgrib/checkGRIB.F90	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
@@ -0,0 +1,198 @@
+PROGRAM checkGrib
+
+    USE cgutils
+
+    IMPLICIT NONE
+
+    INTEGER :: i, stat
+    INTEGER :: num_filepaths, expected_num_levels
+    CHARACTER(LEN=16) :: source_str, levels_str
+    CHARACTER(LEN=256), DIMENSION(100) :: filepaths
+    LOGICAL :: check_fcst, check_anl, check_grib2
+
+    INTEGER :: return_code
+
+
+    CALL get_command_args(source_str, levels_str, &
+&                         filepaths, num_filepaths, &
+&                         check_fcst, check_anl, check_grib2)
+    !WRITE(6,*) '----- got args ------'
+    !WRITE(6,*) 'source_str: ', TRIM(source_str)
+    !WRITE(6,*) 'levels_str: ', TRIM(levels_str)
+    !WRITE(6,*) 'filepaths:'
+    !DO i=1,num_filepaths
+    !    WRITE(6,*) TRIM(filepaths(i))
+    !END DO 
+    !WRITE(6,*) '----- got args ------'
+
+    ! Convert string to integer
+    READ(levels_str,*,IOSTAT=stat) expected_num_levels
+
+    DO i=1,num_filepaths
+
+        IF ( TRIM(source_str) .EQ. 'ECMWF' ) THEN
+
+            return_code = cgutils_check_ecmwf_grib(filepaths(i), &
+&                                                   expected_num_levels, &
+&                                                   check_fcst, check_anl, check_grib2)
+        ELSE IF ( TRIM(source_str) .EQ. 'NCEP' ) THEN
+            return_code = cgutils_check_ncep_grib2(filepaths(i), &
+&                                                   expected_num_levels, &
+&                                                   check_fcst, check_anl, check_grib2)
+        ELSE IF ( TRIM(source_str) .EQ. 'NCEPFV3' ) THEN
+            return_code = cgutils_check_fv3_grib2(filepaths(i), &
+&                                                   expected_num_levels, &
+&                                                   check_fcst, check_anl, check_grib2)
+
+        ELSE
+
+            WRITE(6,*) 'Invalid source string: ', TRIM(source_str)
+            CALL EXIT(2)
+
+        ENDIF
+
+        !PRINT *, 'return_code: ', return_code
+        IF (return_code .EQ. 0) THEN
+            WRITE(6,*) 'Passed: ', TRIM(filepaths(i))
+        ELSE 
+            WRITE(6,*) 'Failed on: ', TRIM(filepaths(i))
+            CALL EXIT(return_code)
+        ENDIF
+    END DO 
+
+
+
+END PROGRAM checkGrib
+
+
+SUBROUTINE get_command_args(source_arg, levels_arg, &
+&          filepaths, num_filepaths, &
+&          check_fcst, check_anl, check_grib2)
+
+    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+    ! This subroutine makes an effort to retrieve the command line arguments
+    ! in string format.  It makes no effort to verify their values
+    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+    CHARACTER(LEN=16), INTENT(OUT) :: source_arg, levels_arg
+    CHARACTER(LEN=256), DIMENSION(100), INTENT(OUT) :: filepaths
+    INTEGER, INTENT(OUT) :: num_filepaths
+    LOGICAL, INTENT(OUT) :: check_fcst, check_anl, check_grib2
+
+    INTEGER :: arg_idx
+
+    INTEGER :: num_cli_args
+    CHARACTER(LEN=256) :: arg_str
+    LOGICAL :: required_flags_processed
+
+    ! Logic in the following loop will set this to True only if
+    ! the required args are storing something other than 'NUL'
+    required_flags_processed = .FALSE.
+    source_arg = 'NUL'
+    levels_arg = 'NUL'
+
+    ! Default values
+    check_fcst = .FALSE.
+    check_anl = .FALSE.
+    check_grib2 = .FALSE.
+
+
+    num_cli_args = IARGC()
+    ! Abort if there were no args
+    IF (num_cli_args .LT. 1) THEN
+        WRITE(0,*) 'No arguments provided'
+        CALL print_usage 
+        CALL EXIT(2)
+    ENDIF
+
+    ! We expect commandline flags, usually with arguments, followed
+    ! by a variable-size list of filepaths
+    num_filepaths = 0
+    arg_idx = 1
+    DO WHILE (arg_idx .LE. num_cli_args)
+
+
+        CALL GETARG(arg_idx, arg_str)
+
+        SELECT CASE ( TRIM(arg_str) )
+            CASE ('--source')
+                arg_idx = arg_idx + 1
+                CALL GETARG(arg_idx, source_arg)
+                IF (.NOT. ( (source_arg .EQ. 'NUL') .OR. &
+&                           (levels_arg .EQ. 'NUL') ) ) THEN
+                    required_flags_processed = .TRUE.
+                ENDIF
+                
+            CASE ('--levels')
+                arg_idx = arg_idx + 1
+                CALL GETARG(arg_idx, levels_arg)
+                IF (.NOT. ( (source_arg .EQ. 'NUL') .OR. &
+&                           (levels_arg .EQ. 'NUL') ) ) THEN
+                    required_flags_processed = .TRUE.
+                ENDIF
+
+            CASE ('--checkfcst')
+                check_fcst = .TRUE.
+
+            CASE ('--checkanl')
+                check_anl = .TRUE.
+
+            CASE ('--checkgrib2')
+                check_grib2 = .TRUE.
+
+
+            CASE default
+                ! We should only be here if required flags have been
+                ! processed, in which case we assume any remaining tokens
+                ! are paths to GRIB files
+                !
+                ! Note - an additional sanity check might be to see if the
+                ! first two characters of the token are '--', in which case
+                ! someone might have slipped in another command flag at a
+                ! time when we're only expecting pathnames.  But, I chose
+                ! not to implement that at this time.  I figure the utility
+                ! will barf when it tries to open an invalid file.
+                IF (.NOT. required_flags_processed) THEN
+                    WRITE(0,*) 'args missing or out of sync'
+                    CALL print_usage 
+                    CALL EXIT(2)
+                ENDIF
+                num_filepaths = num_filepaths + 1
+                filepaths(num_filepaths) = TRIM(arg_str)
+                
+        END SELECT
+
+        !PRINT *, TRIM(arg_str)
+        arg_idx = arg_idx + 1
+    END DO
+
+    ! Let's make sure that at least one filepath was counted
+    IF (num_filepaths .LT. 1) THEN
+        WRITE(0,*) 'No GRIB files were specified'
+        CALL print_usage 
+        CALL EXIT(2)
+    ENDIF
+
+    ! Let's make sure that both checkfcst and checkanl weren't specified
+    IF (check_fcst .AND. check_anl) THEN
+        WRITE(0,*) 'Only one of --checkfcst and --checkanl can be specified'
+        CALL print_usage 
+        CALL EXIT(2)
+    ENDIF
+
+
+
+END SUBROUTINE get_command_args
+
+SUBROUTINE print_usage
+
+    WRITE(0,*) ' '
+    WRITE(0,*) 'Usage: '
+    WRITE(0,*) ' '
+    WRITE(0,*) 'checkgrib --source [ECMWF | NCEP | NCEPFV3] --levels <int> '
+    WRITE(0,*) '          [ --checkfcst | --checkanl | --checkgrib2 ]'
+    WRITE(0,*) '          path1 path2 ...'
+    WRITE(0,*) ' '
+
+END SUBROUTINE print_usage
+
Index: flexpart_code/checkgrib/test/.gitignore
===================================================================
--- flexpart_code/checkgrib/test/.gitignore	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
+++ flexpart_code/checkgrib/test/.gitignore	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
@@ -0,0 +1,1 @@
+gribfiles
Index: flexpart_code/checkgrib/test/checkgrib_test.py
===================================================================
--- flexpart_code/checkgrib/test/checkgrib_test.py	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
+++ flexpart_code/checkgrib/test/checkgrib_test.py	(revision 941195203f079ad9ae7ec538a34d372530ee821d)
@@ -0,0 +1,1890 @@
+#!/usr/bin/env python
+
+"""
+Don Morton
+Boreal Scientific Computing LLC
+Fairbanks, Alaska, USA
+Don.Morton@borealscicomp.com
+"""
+
+
+
+import argparse
+import fnmatch
+import logging
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import uuid
+
+import numpy as np
+
+
+
+# Set up module logging
+LOGGING_LEVEL = logging.WARNING
+
+LOGGING_FORMAT = '%(levelname)s %(filename)s:%(funcName)s:%(lineno)d: %(message)s'
+
+MY_LOGGER = logging.getLogger()
+MY_LOGGER.setLevel(LOGGING_LEVEL)
+
+console_handler = logging.StreamHandler()
+console_handler.setLevel(LOGGING_LEVEL)
+
+logformatter = logging.Formatter(LOGGING_FORMAT)
+console_handler.setFormatter(logformatter)
+
+MY_LOGGER.addHandler(console_handler)
+#MY_LOGGER.propagate = False
+
+
+#---------------------  Define path prefixes to grib files  ------------
+#########   Paths to GRIB Files
+#### TO DO - put a note in here about where to get these files - put them
+#### on my web server.  Right now they're versioned, but that's almost
+###  1 GByte.
+ECMWF_PREFIX = 'gribfiles/ecmwf0p5'
+NCEP_PREFIX = 'gribfiles/ncep0p5'
+NCEPFV3_PREFIX = 'gribfiles/ncepfv30p5'
+
+
+
+### DJM - 2019-07-30 - using these paths will take a bit of work,
+### especially with the new ncep directory structure (with the 0.5/ and
+### 0.5.fv3/ subdirectories).  So, code will need to be adjusted in order
+### for this to work.
+#XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+#If you're testing at CTBTO, these would be better paths to the same files
+#CTBTO_PREFIX = '/ops/data/atm'
+#ECMWF_PREFIX = CTBTO_PREFIX + '/ecmwf/2019/01/11/0.5'
+#NCEP_PREFIX = CTBTO_PREFIX + '/ncep/2019/01/08/0.5'
+#-----------------------------------------------------------------------
+
+
+
+
+# If you have these files available in the specified prefix, you should be
+# good to go   
+ECMWF0P5_GRIB2_FCST_TESTFILE = ECMWF_PREFIX + '/EN19011109'  #  forecast file
+ECMWF0P5_GRIB2_ANL_TESTFILE = ECMWF_PREFIX + '/EN19011112'   #  analysis file
+ECMWF0P5_EF_FCST_TESTFILE = ECMWF_PREFIX + '/EF19073103'  #  forecast file
+
+
+NCEP0P5_GRIB2_TESTFILE1 = NCEP_PREFIX + '/GD19010812'
+NCEP0P5_GRIB2_TESTFILE2 = NCEP_PREFIX + '/GD19010815'
+NCEP0P5_GRIB2_TESTFILE3 = NCEP_PREFIX + '/GD19010818'
+NCEP0P5_GRIB2_TESTFILE4 = NCEP_PREFIX + '/GD19010821'
+
+NCEPFV30P5_GRIB2_TESTFILE1 = NCEPFV3_PREFIX + '/GD19060800'
+NCEPFV30P5_GRIB2_TESTFILE2 = NCEPFV3_PREFIX + '/GD19060803'
+NCEPFV30P5_GRIB2_TESTFILE2 = NCEPFV3_PREFIX + '/GD19060806'
+
+def main():
+
+    # Currently set up to be in the parent directory. 
+    CHECKGRIB_DIR = os.path.abspath(os.pardir)
+    CHECKGRIB = CHECKGRIB_DIR + '/checkGRIB'
+
+    # Compile flexpart2ctbt
+    print("Compiling checkGRIB in dir: " + str(CHECKGRIB_DIR))
+    compile_passed = test_compile_checkgrib(checkgrib_dir=CHECKGRIB_DIR)
+
+    print("compile_passed: %s" % compile_passed)
+
+    if not compile_passed:
+        print("Whooaa, Nellie, no sense continuing if you can't even compile!")
+        sys.exit()
+
+
+
+
+    passed_count = 0
+    failed_count = 0
+
+
+
+
+    #----------------------
+    # General tests
+    #----------------------
+
+    print("--------test_missing_source_cli_arg_detected--------")
+    passed = test_missing_source_cli_arg_detected(checkgrib=CHECKGRIB, 
+                           gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+    print("--------test_invalid_source_cli_arg_detected--------")
+    passed = test_invalid_source_cli_arg_detected(checkgrib=CHECKGRIB, 
+                           gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+    print("--------test_missing_levels_cli_arg_detected--------")
+    passed = test_missing_levels_cli_arg_detected(checkgrib=CHECKGRIB, 
+                           gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_checkanl_and_checkfcst_detected--------")
+    passed = test_checkanl_and_checkfcst_detected(checkgrib=CHECKGRIB, 
+                           gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_reports_no_cli_args--------")
+    passed = test_reports_no_cli_args(checkgrib=CHECKGRIB) 
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_reports_no_path_args--------")
+    passed = test_reports_no_path_args(checkgrib=CHECKGRIB) 
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+
+
+
+
+
+    #----------------------
+    # ECMWF-specific tests
+    #----------------------
+
+    print("--------test_successful_ecmwfgrib2_check--------")
+    passed = test_successful_ecmwfgrib2_check(checkgrib=CHECKGRIB, 
+                                gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_successful_ecmwfgrib_ef_check--------")
+    passed = test_successful_ecmwfgrib_ef_check(checkgrib=CHECKGRIB, 
+                                ef_gribfile=ECMWF0P5_EF_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_reports_ecmwfgrib_ef_fails_with_checkgrib2--------")
+    passed = test_reports_ecmwfgrib_ef_fails_with_checkgrib2(checkgrib=CHECKGRIB, 
+                                ef_gribfile=ECMWF0P5_EF_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+    print("--------test_reports_unexpected_ecmwf_grib_center--------")
+    passed = test_reports_unexpected_ecmwf_grib_centre(checkgrib=CHECKGRIB,
+                                   ncep_gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_reports_unexpected_ecmwf_level--------")
+    passed = test_reports_unexpected_ecmwf_level(checkgrib=CHECKGRIB, 
+                                   ecmwf_gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_reports_missing_ecmwf_levels--------")
+    passed = test_reports_missing_ecmwf_levels(checkgrib=CHECKGRIB, 
+                                   ecmwf_gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_reports_failed_ecmwf_anl_check--------")
+    passed = test_reports_failed_ecmwf_anl_check(checkgrib=CHECKGRIB, 
+                           ecmwf_fcst_gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_reports_failed_ecmwf_fcst_check--------")
+    passed = test_reports_failed_ecmwf_fcst_check(checkgrib=CHECKGRIB, 
+                           ecmwf_anl_gribfile=ECMWF0P5_GRIB2_ANL_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ecmwf_detects_missing_level_14--------")
+    passed = test_ecmwf_detects_missing_level_14(checkgrib=CHECKGRIB, 
+                           ecmwf_gribfile=ECMWF0P5_GRIB2_ANL_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+    print("--------test_ecmwf_detects_missing_level_14_etadot--------")
+    passed = test_ecmwf_detects_missing_level_14_etadot(checkgrib=CHECKGRIB, 
+                           ecmwf_gribfile=ECMWF0P5_GRIB2_ANL_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ecmwf_detects_missing_var_lsp--------")
+    passed = test_ecmwf_detects_missing_var_lsp(checkgrib=CHECKGRIB, 
+                           ecmwf_gribfile=ECMWF0P5_GRIB2_ANL_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ecmwf_detects_grib1_nsss--------")
+    passed = test_ecmwf_detects_grib1_nsss(checkgrib=CHECKGRIB, 
+                           ecmwf_gribfile=ECMWF0P5_GRIB2_ANL_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+
+    # The following two tests will never pass with current CTBTO ECMWF
+    # files because the files have mixed anl and fcst messages
+    """
+    print("--------test_ecmwf_successful_fcst_check--------")
+    passed = test_ecmwf_successful_fcst_check(checkgrib=CHECKGRIB, 
+                           ecmwf_fcst_gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ecmwf_successful_anl_check--------")
+    passed = test_ecmwf_successful_anl_check(checkgrib=CHECKGRIB, 
+                           ecmwf_anl_gribfile=ECMWF0P5_GRIB2_ANL_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+    """
+
+    #----------------------
+    # NCEP-specific tests
+    #----------------------
+
+
+    print("--------test_reports_unexpected_ncep_grib_center--------")
+    passed = test_reports_unexpected_ncep_grib_centre(checkgrib=CHECKGRIB,
+                                   ecmwf_gribfile=ECMWF0P5_GRIB2_FCST_TESTFILE)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_successful_ncepgrib2_check--------")
+    passed = test_successful_ncepgrib2_check(checkgrib=CHECKGRIB, 
+                                gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_detects_ncep_more_pressure_levels_than_expected------")
+    passed = test_detects_ncep_more_pressure_levels_than_expected(
+                                checkgrib=CHECKGRIB, 
+                                gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+    print("--------test_detects_ncep_fewer_pressure_levels_than_expected------")
+    passed = test_detects_ncep_fewer_pressure_levels_than_expected(
+                                checkgrib=CHECKGRIB, 
+                                gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncep_detects_missing_level_850------")
+    passed = test_ncep_detects_missing_level_850(
+                                checkgrib=CHECKGRIB, 
+                                ncep_gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncep_detects_missing_level_100_r------")
+    passed = test_ncep_detects_missing_level_100_r(
+                                checkgrib=CHECKGRIB, 
+                                ncep_gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncep_detects_missing_var_10u------")
+    passed = test_ncep_detects_missing_var_10u(
+                                checkgrib=CHECKGRIB, 
+                                ncep_gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncep_detects_missing_var_tsig1------")
+    passed = test_ncep_detects_missing_var_tsig1(
+                                checkgrib=CHECKGRIB, 
+                                ncep_gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+
+    print("--------test_ncep_detects_grib1_2t------")
+    passed = test_ncep_detects_grib1_2t(
+                                checkgrib=CHECKGRIB, 
+                                ncep_gribfile=NCEP0P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+
+
+    #----------------------
+    # NCEPFV3-specific tests
+    #----------------------
+
+    print("--------test_successful_ncepfv3grib2_check--------")
+    passed = test_successful_ncepfv3grib2_check(checkgrib=CHECKGRIB,
+                                gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+ 
+    if passed:
+        passed_count += 1
+    else:
+        failed_count +=1
+        print('FV3 TEST FAILED - ABORT')
+        sys.exit()
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_detects_ncepfv3_more_pressure_levels_than_expected------")
+    passed = test_detects_ncepfv3_more_pressure_levels_than_expected(
+                                checkgrib=CHECKGRIB, 
+                                gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+    print("--------test_detects_ncepfv3_fewer_pressure_levels_than_expected------")
+    passed = test_detects_ncepfv3_fewer_pressure_levels_than_expected(
+                                checkgrib=CHECKGRIB, 
+                                gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncepfv3_detects_missing_level_850------")
+    passed = test_ncepfv3_detects_missing_level_850(
+                                checkgrib=CHECKGRIB, 
+                                ncepfv3_gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncepfv3_detects_missing_level_100_r------")
+    passed = test_ncepfv3_detects_missing_level_100_r(
+                                checkgrib=CHECKGRIB, 
+                                ncepfv3_gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncepfv3_detects_missing_var_10u------")
+    passed = test_ncepfv3_detects_missing_var_10u(
+                                checkgrib=CHECKGRIB, 
+                                ncepfv3_gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncepfv3_detects_missing_var_tsig1------")
+    passed = test_ncepfv3_detects_missing_var_tsig1(
+                                checkgrib=CHECKGRIB, 
+                                ncepfv3_gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+    print("--------test_ncepfv3_detects_grib1_2t------")
+    passed = test_ncepfv3_detects_grib1_2t(
+                                checkgrib=CHECKGRIB, 
+                                ncepfv3_gribfile=NCEPFV30P5_GRIB2_TESTFILE1)
+    if passed: passed_count += 1
+    else: failed_count +=1 
+    print("passed: %s" % passed)
+    print("-----------------------------------------------")
+
+
+
+
+    # Summary of tests passed/failed
+    print("\n************************")
+    print("Passed tests: %d" % passed_count)
+    print("Failed tests: %d" % failed_count)
+    print("************************\n")
+
+
+
+
+
+
+
+#############    General test functions    #####################
+
+
+def test_compile_checkgrib(checkgrib_dir=None):
+
+    """
+    Removes any existing copy of the binary, compiles, and tests
+    for presence, returning a True if successful, otherwise False
+
+    Expects executable to be named "gribcheck"
+
+    Assumes that Makefile in the compile directory is correct for the
+    environment
+    """
+
+    the_command = 'cd ' + checkgrib_dir + ' ; '
+    the_command += 'make clean > /dev/null 2>&1; '
+    the_command += 'make > /dev/null 2>&1; '
+    subprocess.call(the_command, shell=True)
+
+    execname = checkgrib_dir + '/checkGRIB'
+    if os.path.isfile(execname) and os.access(execname, os.X_OK):
+        success = True
+    else:
+        success = False
+
+    return success
+
+
+def test_missing_source_cli_arg_detected(checkgrib=None, gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks that utility exits with code 2 if the --source arg is missing
+    """
+
+    the_command = [checkgrib,  
+                              "--levels", "3",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 2:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+def test_invalid_source_cli_arg_detected(checkgrib=None, gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks that utility exits with code 2 if the --source arg is invalid
+    """
+
+    the_command = [checkgrib,  
+                              "--levels", "3",
+                              "--source", "THIS_IS_INVALID!",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 2:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+
+
+
+
+def test_missing_levels_cli_arg_detected(checkgrib=None, gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks that utility exits with code 2 if the --levels arg is missing
+    """
+
+    the_command = [checkgrib,  
+                              "--source", "ECMWF",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 2:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+def test_checkanl_and_checkfcst_detected(checkgrib=None, gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks that utility exits with code 2 if both the --checkfcst and
+    --checkanl flags are specified
+    """
+
+    the_command = [checkgrib,  
+                              "--source", "ECMWF",
+                              "--levels", "137",
+                              "--checkanl", "--checkfcst",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 2:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+
+def test_reports_no_cli_args(checkgrib=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+
+    Checks that utility exits with code 2 if no CLI args were specified
+    """
+
+    the_command = [checkgrib]  
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 2:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_reports_no_path_args(checkgrib=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+
+    Checks that utility exits with code 2 if no gribfile args were specified
+    """
+
+    the_command = [checkgrib,  
+                              "--source", "ECMWF",
+                              "--levels", "137"]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 2:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+
+
+
+
+
+
+
+
+
+
+#############    ECMWF test functions    #####################
+
+
+
+def test_reports_unexpected_ecmwf_grib_centre(checkgrib=None, 
+                                              ncep_gribfile=None):
+
+    """ 
+    Calls checkgrib for ECMWF source but file is actually NCEP
+
+    Expects an error message and return code of 1
+    """ 
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              ncep_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+def test_reports_missing_ecmwf_levels(checkgrib=None, 
+                                     ecmwf_gribfile=None):
+
+    """ 
+    Calls checkgrib for ECMWF specifying number of levels greater
+    than the expected 137
+
+    Expects an error message and return code of 1
+    """ 
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "147",
+                              ecmwf_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+
+
+
+
+def test_successful_ecmwfgrib2_check(checkgrib=None, gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to gribfile to check
+
+    Runs through a no-brainer successful test, expecting a 0 return code
+    """
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 0:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+def test_successful_ecmwfgrib_ef_check(checkgrib=None, ef_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ef_gribfile: full path to EF gribfile to check
+
+    Runs through a no-brainer successful test, expecting a 0 return code
+    The EF files contain both GRIB1 and GRIB2.  This tests that it will
+    still pass, as long as --checkgrib2 is not used
+    """
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              ef_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 0:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_reports_ecmwfgrib_ef_fails_with_checkgrib2(checkgrib=None, ef_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ef_gribfile: full path to EF gribfile to check
+
+    Tests that EF check fails if checkgrib2 specified,
+    expecting a 1 return code
+    The EF files contain both GRIB1 and GRIB2.  This tests that it will
+    still pass, as long as --checkgrib2 is not used
+    """
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--checkgrib2",
+                              "--levels", "137",
+                              ef_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+
+
+def test_reports_unexpected_ecmwf_level(checkgrib=None, ecmwf_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks an ECMWF file with "--levels 3", so exits when it encounters
+    a level higher than that.  Expected return_code is 1
+    """
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "3",
+                              ecmwf_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+def test_ecmwf_detects_missing_level_14(checkgrib=None, ecmwf_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks an ECMWF file that has had vertical level 14 removed
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+    the_command = 'grib_copy -w level!=14 '
+    the_command += str(ecmwf_gribfile) + ' ' + str(tempgribpath)
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+def test_ecmwf_detects_missing_var_lsp(checkgrib=None, ecmwf_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks an ECMWF file that has had the lsp variable removed
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+    the_command = 'grib_copy -w shortName!=lsp '
+    the_command += str(ecmwf_gribfile) + ' ' + str(tempgribpath)
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+
+
+
+
+
+
+def test_ecmwf_detects_missing_level_14_etadot(checkgrib=None, 
+                                               ecmwf_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks an ECMWF file that has had vertical level 14 removed
+    just for etadot
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+
+    the_command = "count=`grib_ls -p count -w level=14,shortName=etadot "
+    the_command += ecmwf_gribfile + " | sed -n 3p` ;"
+    the_command += " grib_copy -w count!=$count "
+    the_command += str(ecmwf_gribfile) + ' ' + str(tempgribpath)
+
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    MY_LOGGER.info('the_command: ' + str(the_command))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+def test_ecmwf_detects_grib1_nsss(checkgrib=None, 
+                                        ecmwf_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to NCEP gribfile to check
+
+    Checks an ECMWF file that has message variable nsss 
+    changed from GRIB2 to GRIB1
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+    the_command = "grib_set -s edition=1 -w shortName=nsss "
+    the_command += str(ecmwf_gribfile) + ' ' + str(tempgribpath)
+
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    MY_LOGGER.info('the_command: ' + str(the_command))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--checkgrib2",
+                              "--levels", "137",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def test_reports_failed_ecmwf_anl_check(checkgrib=None, 
+                                        ecmwf_fcst_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to ECMWF fcst gribfile to check
+
+    Checks a fcst ECMWF file with "--checkanl", so exits when it encounters
+    fcst.  Expected return_code is 1
+    """
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              "--checkanl",
+                              ecmwf_fcst_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_reports_failed_ecmwf_fcst_check(checkgrib=None, 
+                                        ecmwf_anl_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to ECMWF anl gribfile to check
+
+    Checks a anl ECMWF file with "--checkfcst", so exits when it encounters
+    anl.  Expected return_code is 1
+    """
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              "--checkfcst",
+                              ecmwf_anl_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_ecmwf_successful_fcst_check(checkgrib=None, ecmwf_fcst_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ecmwf_fcst_gribfile: full path to ECMWF fcst gribfile to check
+
+    Checks a fcst ECMWF file with "--checkfcst", so exits normally.
+    Expected return_code is 0
+    """
+
+    ERROR_MSG = """
+    ****************************************************************
+    WARNING: With current CTBTO ECMWF files, this test will always
+             fail, because the files have mixed anl and fcst messages.
+
+             YOU SHOULD NOT USE THIS TEST UNTIL THE SITUATION IS RESOLVED
+
+    BuhBye
+    """
+
+    print(ERROR_MSG)
+    sys.exit()
+
+
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              "--checkfcst",
+                              ecmwf_fcst_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 0:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+
+def test_ecmwf_successful_anl_check(checkgrib=None, ecmwf_anl_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ecmwf_anl_gribfile: full path to ECMWF anl gribfile to check
+
+    Checks a anl ECMWF file with "--checkanl", so exits normally.
+    Expected return_code is 0
+    """
+
+    ERROR_MSG = """
+    ****************************************************************
+    WARNING: With current CTBTO ECMWF files, this test will always
+             fail, because the files have mixed anl and fcst messages.
+
+             YOU SHOULD NOT USE THIS TEST UNTIL THE SITUATION IS RESOLVED
+
+    BuhBye
+    """
+
+    print(ERROR_MSG)
+    sys.exit()
+
+
+
+    the_command = [checkgrib, "--source", "ECMWF", 
+                              "--levels", "137",
+                              "--checkanl",
+                              ecmwf_anl_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 0:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+
+
+#############    NCEP test functions    #####################
+
+def test_reports_unexpected_ncep_grib_centre(checkgrib=None, 
+                                             ecmwf_gribfile=None):
+
+    """ 
+    Calls checkgrib for NCEP source but file is actually ECMWF
+
+    Expects an error message and return code of 1
+    """ 
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--levels", "30",
+                              ecmwf_gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+def test_successful_ncepgrib2_check(checkgrib=None, 
+                                    gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to gribfile to check
+
+    Runs through a no-brainer successful test, expecting a 0 return code
+    """
+
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--checkgrib2",
+                              "--levels", "31",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 0:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_reports_ncep_bad_level_structure(checkgrib=None, 
+                                          ncep_gribfile=None):
+
+    """ 
+    Tests that if the pressure level structure of the NCEP file does
+    not agree with expected number of levels, an error will be reported
+
+    Expects a return code of 1
+    """ 
+
+    print("Not yet implemented...")
+
+
+
+
+def test_detects_ncep_more_pressure_levels_than_expected(
+                                    checkgrib=None, 
+                                    gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to gribfile to check
+
+    Tests that a return code of 1 will result if checkgrib finds
+    more pressure levels than specified in command line
+    """
+
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--levels", "30",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_detects_ncep_fewer_pressure_levels_than_expected(
+                                    checkgrib=None, 
+                                    gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to gribfile to check
+
+    Tests that a return code of 1 will result if checkgrib finds
+    fewer pressure levels than specified in command line
+    """
+
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--levels", "32",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_ncep_detects_missing_level_850(checkgrib=None, ncep_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncep_gribfile: full path to NCEP gribfile to check
+
+    Checks an NCEP file that has had vertical level 850 removed
+    Expected return_code is 1
+
+    Note, as currently implemented, checkgrib will not see any 850 mb
+    levels, and so it won't add them into expected inventory.  It will
+    fail because it won't see the 31 levels it expects.
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+    the_command = 'grib_copy -w level!=850 '
+    the_command += str(ncep_gribfile) + ' ' + str(tempgribpath)
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+def test_ncep_detects_missing_level_100_r(checkgrib=None, ncep_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncep_gribfile: full path to NCEP gribfile to check
+
+    Checks an NCEP file that has had vertical level 100 removed
+    for variable r
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+    the_command = "count=`grib_ls -p count -w level=100,shortName=r "
+    the_command += ncep_gribfile + " | sed -n 3p` ;"
+    the_command += " grib_copy -w count!=$count "
+    the_command += str(ncep_gribfile) + ' ' + str(tempgribpath)
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+def test_ncep_detects_missing_var_10u(checkgrib=None, ncep_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncep_gribfile: full path to NCEP gribfile to check
+
+    Checks an NCEP file that has had 10m U removed
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+    the_command = 'grib_copy -w shortName!=10u '
+    the_command += str(ncep_gribfile) + ' ' + str(tempgribpath)
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+def test_ncep_detects_missing_var_tsig1(checkgrib=None, ncep_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncep_gribfile: full path to NCEP gribfile to check
+
+    Checks an NCEP file that has had sigma level t removed
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+
+    the_command = "count=`grib_ls -p count -w typeOfLevel=sigma,shortName=t "
+    the_command += ncep_gribfile + " | sed -n 3p` ;"
+    the_command += " grib_copy -w count!=$count "
+    the_command += str(ncep_gribfile) + ' ' + str(tempgribpath)
+
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+def test_ncep_detects_grib1_2t(checkgrib=None, ncep_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncep_gribfile: full path to NCEP gribfile to check
+
+    Checks an NCEP file that has had the 2t (2m T) message changed from
+    GRIB2 to GRIB1
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+    the_command = "grib_set -s edition=1,packingType=grid_simple "
+    the_command += " -w shortName=2t "
+    the_command += str(ncep_gribfile) + ' ' + str(tempgribpath)
+
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEP", 
+                              "--checkgrib2",
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+
+#############    NCEPFV3 test functions    #####################
+
+def test_successful_ncepfv3grib2_check(checkgrib=None,
+                                    gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to gribfile to check
+
+    Runs through a no-brainer successful test, expecting a 0 return code
+    """
+
+    the_command = [checkgrib, "--source", "NCEPFV3",
+                              "--checkgrib2",
+                              "--levels", "31",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 0:
+        passed = True
+    else:
+        passed = False
+
+    return passed
+
+
+def test_detects_ncepfv3_more_pressure_levels_than_expected(
+                                    checkgrib=None, 
+                                    gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to gribfile to check
+
+    Tests that a return code of 1 will result if checkgrib finds
+    more pressure levels than specified in command line
+    """
+
+    the_command = [checkgrib, "--source", "NCEPFV3", 
+                              "--levels", "30",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+
+
+def test_detects_ncepfv3_fewer_pressure_levels_than_expected(
+                                    checkgrib=None, 
+                                    gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    gribfile: full path to gribfile to check
+
+    Tests that a return code of 1 will result if checkgrib finds
+    fewer pressure levels than specified in command line
+    """
+
+    the_command = [checkgrib, "--source", "NCEPFV3", 
+                              "--levels", "32",
+                              gribfile]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+    
+    return passed
+
+def test_ncepfv3_detects_missing_level_850(checkgrib=None, ncepfv3_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncepfv3_gribfile: full path to NCEP FV3 gribfile to check
+
+    Checks an NCEP FV3 file that has had vertical level 850 removed
+    Expected return_code is 1
+
+    Note, as currently implemented, checkgrib will not see any 850 mb
+    levels, and so it won't add them into expected inventory.  It will
+    fail because it won't see the 31 levels it expects.
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+    the_command = 'grib_copy -w level!=850 '
+    the_command += str(ncepfv3_gribfile) + ' ' + str(tempgribpath)
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEPFV3", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+def test_ncepfv3_detects_missing_level_100_r(checkgrib=None, ncepfv3_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncepfv3_gribfile: full path to NCEP FV3 gribfile to check
+
+    Checks an NCEP FV3 file that has had vertical level 100 removed
+    for variable r
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+    the_command = "count=`grib_ls -p count -w level=100,shortName=r "
+    the_command += ncepfv3_gribfile + " | sed -n 3p` ;"
+    the_command += " grib_copy -w count!=$count "
+    the_command += str(ncepfv3_gribfile) + ' ' + str(tempgribpath)
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEPFV3", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+def test_ncepfv3_detects_missing_var_10u(checkgrib=None, ncepfv3_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncepfv3_gribfile: full path to NCEP FV3 gribfile to check
+
+    Checks an NCEP FV3 file that has had 10m U removed
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+    the_command = 'grib_copy -w shortName!=10u '
+    the_command += str(ncepfv3_gribfile) + ' ' + str(tempgribpath)
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEPFV3", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+def test_ncepfv3_detects_missing_var_tsig1(checkgrib=None, ncepfv3_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncepfv3_gribfile: full path to NCEP FV3 gribfile to check
+
+    Checks an NCEP FV3 file that has had sigma level t removed
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+
+    the_command = "count=`grib_ls -p count -w typeOfLevel=sigma,shortName=t "
+    the_command += ncepfv3_gribfile + " | sed -n 3p` ;"
+    the_command += " grib_copy -w count!=$count "
+    the_command += str(ncepfv3_gribfile) + ' ' + str(tempgribpath)
+
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEPFV3", 
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+def test_ncepfv3_detects_grib1_2t(checkgrib=None, ncepfv3_gribfile=None):
+
+    """
+    checkgrib: full path to checkgrib executable
+    ncepfv3_gribfile: full path to NCEP FV3 gribfile to check
+
+    Checks an NCEP FV3 file that has had the 2t (2m T) message changed from
+    GRIB2 to GRIB1
+    Expected return_code is 1
+    """
+
+    # Create a temporary gribfile 
+    tempgribpath = '/tmp/' + str(uuid.uuid4()) + '.gr2'
+
+    the_command = "grib_set -s edition=1,packingType=grid_simple "
+    the_command += " -w shortName=2t "
+    the_command += str(ncepfv3_gribfile) + ' ' + str(tempgribpath)
+
+
+    MY_LOGGER.info('Creating tempgribpath: ' + str(tempgribpath))
+    subprocess.call(the_command, shell=True)
+
+    # Use it
+    the_command = [checkgrib, "--source", "NCEPFV3", 
+                              "--checkgrib2",
+                              "--levels", "31",
+                              tempgribpath]
+
+    process = subprocess.Popen(the_command)
+    process.communicate()[0]
+    return_code = process.returncode
+
+    MY_LOGGER.info('return_code: ' + str(return_code))
+    if return_code == 1:
+        passed = True
+    else:
+        passed = False
+
+    # Remove the temporary gribfile
+    MY_LOGGER.info('Removing tempgribpath: ' + str(tempgribpath))
+    os.remove(tempgribpath)
+    
+    return passed
+
+
+
+
+
+if __name__=="__main__":
+
+    main()
+
+
